initial commit

This commit is contained in:
ston1th 2016-09-06 19:57:41 +02:00
commit 09e12282d0
4 changed files with 396 additions and 0 deletions

24
LICENSE Normal file
View file

@ -0,0 +1,24 @@
Copyright (C) 2016 Marius Schellenberger
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* The names of the authors and/or contributors may not be used to
endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL ston1th BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

1
README.md Normal file
View file

@ -0,0 +1 @@
# jwt - a simple JSON Web Token library in Go

346
jwt.go Normal file
View file

@ -0,0 +1,346 @@
// Package jwt provides a easy to use JSON Web Token and blacklisting library
package jwt
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/json"
"errors"
"hash"
"strings"
"sync"
"time"
)
const (
// Header is the default HTTP Authorization header name
Header = "Authorization"
// DefaultExpiry is the default token expiration time
DefaultExpiry = time.Hour * 12
typ = "JWT"
keySize = 64
)
// Hash represents the three diffrernt hash types
type Hash int
const (
HS256 Hash = iota //SHA256
HS384 //SHA384
HS512 //SHA512
unsupported
)
// String returns the string representation of Hash
func (h Hash) String() string {
switch h {
case HS256:
return "HS256"
case HS384:
return "HS384"
case HS512:
return "HS512"
}
return ""
}
var (
ErrNoJWT = errors.New("no json web token")
ErrUnsupportedAlg = errors.New("unsupported algoritm")
ErrInvalid = errors.New("token validation failed")
ErrBlacklisted = errors.New("token blacklisted")
ErrBlacklistNotEnabled = errors.New("blacklisting is not enabled")
ErrNBF = errors.New("token not valid yet")
ErrEXP = errors.New("token expired")
ErrMissingEXP = errors.New("missing exp claim")
ErrMissingTokenParts = errors.New("missíng token parts")
ErrEmptySignature = errors.New("token signature is empty")
)
// parseHash returns the hash.Hash type equal to the input string
func parseHash(alg string) (h func() hash.Hash) {
switch alg {
case "HS256":
h = sha256.New
case "HS384":
h = sha512.New384
case "HS512":
h = sha512.New
}
return
}
// JWT represents the JSON Web Token signing and blacklisting infrastructure
type JWT struct {
key []byte
expiry time.Duration
// protects list
sync.RWMutex
blacklist bool
list map[string]int64
stop chan struct{}
}
// New returns a new JWT object with the given expiry timeout.
// If the timeout is less or equal to zero the default expiry (12 hours) is used.
// If blacklisting is enabled, the JWT object leaks a goroutine to garbage-collect expired blacklisted tokens.
// Call the Stop() method to exit the goroutine.
func New(expiry time.Duration, blacklist bool) (*JWT, error) {
if expiry <= 0 {
expiry = DefaultExpiry
}
key := make([]byte, keySize)
_, err := rand.Read(key)
if err != nil {
return nil, err
}
jwt := &JWT{
key: key,
expiry: expiry,
blacklist: blacklist,
}
if blacklist {
jwt.list = make(map[string]int64)
jwt.stop = make(chan struct{})
go jwt.clean()
}
return jwt, nil
}
func (jwt *JWT) sum(token string, h func() hash.Hash) []byte {
mac := hmac.New(h, jwt.key)
mac.Write([]byte(token))
return mac.Sum(nil)
}
// Invalidate checks if a token is already blacklisted
// If the token is not blacklisted, it will get blacklisted
func (jwt *JWT) Invalidate(t *Token) error {
if !jwt.blacklist {
return ErrBlacklistNotEnabled
}
var (
expOK bool
exp int64
)
switch n := t.Claims["exp"].(type) {
case json.Number:
if i, err := n.Int64(); err == nil {
exp = i
expOK = true
}
case float64:
exp = int64(n)
expOK = true
}
if !expOK {
return ErrMissingEXP
}
if err := jwt.blacklisted(t.Signature); err != nil {
return err
}
jwt.Lock()
defer jwt.Unlock()
jwt.list[t.Signature] = exp
return nil
}
// blacklisted checks if a token is blacklisted
func (jwt *JWT) blacklisted(sig string) error {
if sig == "" {
return ErrEmptySignature
}
jwt.RLock()
defer jwt.RUnlock()
_, ok := jwt.list[sig]
if ok {
return ErrBlacklisted
}
return nil
}
// clean will look for expired blacklisted tokens and removes them
func (jwt *JWT) clean() {
for {
t := time.NewTimer(time.Hour)
select {
case <-t.C:
case <-jwt.stop:
t.Stop()
return
}
now := time.Now().UTC().Unix()
jwt.Lock()
defer jwt.Unlock()
for k, v := range jwt.list {
if now > v {
delete(jwt.list, k)
}
}
}
}
// Sign will sign the provided token using the secret key
// This will overwrite existing 'exp' and 'nbf' claims
func (jwt *JWT) Sign(t *Token) (err error) {
now := time.Now().UTC()
t.Claims["exp"] = now.Add(jwt.expiry).Unix()
t.Claims["nbf"] = now.Unix()
h, err := json.Marshal(t.Header)
if err != nil {
return
}
c, err := json.Marshal(t.Claims)
if err != nil {
return
}
var hf func() hash.Hash
switch a := t.Header["alg"].(type) {
case string:
hf = parseHash(a)
if hf == nil {
return ErrUnsupportedAlg
}
default:
return ErrUnsupportedAlg
}
t.Data = strings.Join([]string{base64.URLEncoding.EncodeToString(h), base64.URLEncoding.EncodeToString(c)}, ".")
t.Raw = strings.Join([]string{t.Data, base64.URLEncoding.EncodeToString(jwt.sum(t.Data, hf))}, ".")
return
}
// Verify will verify the provided token using the secret key
func (jwt *JWT) Verify(t *Token) error {
now := time.Now().UTC().Unix()
var (
exp int64
nbf int64
expOK bool
nbfOK bool
hf func() hash.Hash
)
switch t := t.Header["typ"].(type) {
case string:
if t != typ {
return ErrNoJWT
}
default:
return ErrNoJWT
}
switch a := t.Header["alg"].(type) {
case string:
hf = parseHash(a)
if hf == nil {
return ErrUnsupportedAlg
}
default:
return ErrUnsupportedAlg
}
switch n := t.Claims["exp"].(type) {
case json.Number:
if i, err := n.Int64(); err == nil {
exp = i
expOK = true
}
case float64:
exp = int64(n)
expOK = true
}
switch n := t.Claims["nbf"].(type) {
case json.Number:
if i, err := n.Int64(); err == nil {
nbf = i
nbfOK = true
}
case float64:
nbf = int64(n)
nbfOK = true
}
if expOK && jwt.blacklist {
err := jwt.blacklisted(t.Signature)
if err != nil {
return err
}
}
if expOK && now > exp {
return ErrEXP
}
if nbfOK && now < nbf {
return ErrNBF
}
if !hmac.Equal(jwt.sum(t.Data, hf), t.RawSignature) {
return ErrInvalid
}
return nil
}
// Stop will end the cleaner goroutinge execution
// Calling Stop twice or more will panic
func (jwt *JWT) Stop() error {
if !jwt.blacklist {
return ErrBlacklistNotEnabled
}
select {
case jwt.stop <- struct{}{}:
close(jwt.stop)
default:
}
return nil
}
// DecodeToken decodes a raw string token into a *Token object
func DecodeToken(token string) (t *Token, err error) {
parts := strings.Split(token, ".")
if len(parts) < 3 {
return nil, ErrMissingTokenParts
}
t = new(Token)
header, err := base64.URLEncoding.DecodeString(parts[0])
if err != nil {
return
}
err = json.Unmarshal(header, &t.Header)
if err != nil {
return
}
claims, err := base64.URLEncoding.DecodeString(parts[1])
if err != nil {
return
}
err = json.Unmarshal(claims, &t.Claims)
if err != nil {
return
}
t.RawSignature, err = base64.URLEncoding.DecodeString(parts[2])
t.Signature = parts[2]
t.Data = strings.Join(parts[:2], ".")
t.Raw = token
return
}
// Token is the JWT token representation
type Token struct {
Raw string
Data string
Signature string
RawSignature []byte
Exp int64
Header map[string]interface{}
Claims map[string]interface{}
}
// NewToken returns a new *Token using the provided hash algorithm and claims
func NewToken(hash Hash, claims map[string]interface{}) *Token {
return &Token{
Header: map[string]interface{}{
"alg": hash.String(),
"typ": typ,
},
Claims: claims,
}
}

25
jwt_test.go Normal file
View file

@ -0,0 +1,25 @@
package jwt
import (
"testing"
"time"
)
func TestValidate(t *testing.T) {
jwt, _ := New(0, true)
token := NewToken(HS256, map[string]interface{}{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"fizz": "buzz",
})
err := jwt.Sign(token)
t.Log(err, token)
nt, err := DecodeToken(token.Raw)
t.Log(nt, err)
time.Sleep(time.Second)
t.Log(jwt.Verify(nt))
jwt.Invalidate(nt)
t.Log(jwt.Verify(nt))
jwt.Stop()
}