From 40fe9931a82992e3d6b03a78f182f3b4885ba712 Mon Sep 17 00:00:00 2001 From: ston1th Date: Thu, 29 Dec 2016 11:08:25 +0100 Subject: [PATCH] rewrite and new Hash interface --- hash.go | 86 +++++++++++++++++++++ jwt.go | 219 +++++++++++++++++++++++++++------------------------- jwt_test.go | 48 +++++++++--- 3 files changed, 236 insertions(+), 117 deletions(-) create mode 100644 hash.go diff --git a/hash.go b/hash.go new file mode 100644 index 0000000..171848a --- /dev/null +++ b/hash.go @@ -0,0 +1,86 @@ +package jwt + +import ( + "crypto/sha256" + "crypto/sha512" + "hash" +) + +const ( + HS256Name = "HS256" + HS384Name = "HS384" + HS512Name = "HS512" +) + +// ParseHash returns the Hash type equal to the input string +func ParseHash(alg string) Hash { + switch alg { + case HS256Name: + return NewHS256() + case HS384Name: + return NewHS384() + case HS512Name: + return NewHS512() + } + return nil +} + +// Hash is the hashsum interface for signing the jwt +type Hash interface { + Hash() hash.Hash + Alg() string +} + +// HS256 implements the Hash interface with SHA256 +type HS256 struct{} + +// NewHS256 returns a new HS256 instance +func NewHS256() Hash { + return HS256{} +} + +// Alg returns the algorithm name "HS256" +func (HS256) Alg() string { + return HS256Name +} + +// Hash returns a new hash.Hash instance of the SHA256 algorithm +func (HS256) Hash() hash.Hash { + return sha256.New() +} + +// HS384 implements the Hash interface with SHA384 +type HS384 struct{} + +// NewHS384 returns a new HS384 instance +func NewHS384() Hash { + return HS384{} +} + +// Alg returns the algorithm name "HS384" +func (HS384) Alg() string { + return HS384Name +} + +// Hash returns a new hash.Hash instance of the SHA384 algorithm +func (HS384) Hash() hash.Hash { + return sha512.New384() +} + +// HS512 implements the Hash interface with SHA512 +type HS512 struct{} + +// NewHS512 returns a new HS512 instance +func NewHS512() Hash { + return HS512{} +} + +// Alg returns the algorithm name "HS512" +func (HS512) Alg() string { + return HS512Name +} + +// Hash returns a new hash.Hash instance of the SHA512 algorithm +func (HS512) Hash() hash.Hash { + return sha512.New() +} diff --git a/jwt.go b/jwt.go index ea1f14d..f4e42af 100644 --- a/jwt.go +++ b/jwt.go @@ -4,12 +4,9 @@ package jwt import ( "crypto/hmac" "crypto/rand" - "crypto/sha256" - "crypto/sha512" "encoding/base64" "encoding/json" "errors" - "hash" "strings" "sync" "time" @@ -20,33 +17,11 @@ const ( Header = "Authorization" // DefaultExpiry is the default token expiration time DefaultExpiry = time.Hour * 12 - typ = "JWT" - keySize = 64 + + 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") @@ -58,21 +33,9 @@ var ( ErrMissingEXP = errors.New("missing exp claim") ErrMissingTokenParts = errors.New("missíng token parts") ErrEmptySignature = errors.New("token signature is empty") + ErrTokenIsNil = errors.New("token is nil") ) -// 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 @@ -89,15 +52,12 @@ type JWT struct { // 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) { +func New(expiry time.Duration, blacklist bool) *JWT { if expiry <= 0 { expiry = DefaultExpiry } key := make([]byte, keySize) - _, err := rand.Read(key) - if err != nil { - return nil, err - } + rand.Read(key) jwt := &JWT{ key: key, expiry: expiry, @@ -108,11 +68,11 @@ func New(expiry time.Duration, blacklist bool) (*JWT, error) { jwt.stop = make(chan struct{}) go jwt.clean() } - return jwt, nil + return jwt } -func (jwt *JWT) sum(token string, h func() hash.Hash) []byte { - mac := hmac.New(h, jwt.key) +func (jwt *JWT) sum(token string, h Hash) []byte { + mac := hmac.New(h.Hash, jwt.key) mac.Write([]byte(token)) return mac.Sum(nil) } @@ -123,6 +83,9 @@ func (jwt *JWT) Invalidate(t *Token) error { if !jwt.blacklist { return ErrBlacklistNotEnabled } + if t == nil { + return ErrTokenIsNil + } var ( expOK bool exp int64 @@ -140,12 +103,12 @@ func (jwt *JWT) Invalidate(t *Token) error { if !expOK { return ErrMissingEXP } - if err := jwt.blacklisted(t.Signature); err != nil { + if err := jwt.blacklisted(t.Sig()); err != nil { return err } jwt.Lock() defer jwt.Unlock() - jwt.list[t.Signature] = exp + jwt.list[t.Sig()] = exp return nil } @@ -187,43 +150,11 @@ func (jwt *JWT) clean() { // 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 + if t == nil { + return ErrTokenIsNil } - 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) { + header := t.Header() + switch t := header["typ"].(type) { case string: if t != typ { return ErrNoJWT @@ -231,10 +162,58 @@ func (jwt *JWT) Verify(t *Token) error { default: return ErrNoJWT } - switch a := t.Header["alg"].(type) { + var h Hash + switch a := header["alg"].(type) { case string: - hf = parseHash(a) - if hf == nil { + h = ParseHash(a) + if h == nil { + return ErrUnsupportedAlg + } + default: + return ErrUnsupportedAlg + } + now := time.Now().UTC() + t.Claims["exp"] = now.Add(jwt.expiry).Unix() + t.Claims["nbf"] = now.Unix() + head, err := json.Marshal(header) + if err != nil { + return + } + claims, err := json.Marshal(t.Claims) + if err != nil { + return + } + t.data = strings.Join([]string{base64.URLEncoding.EncodeToString(head), base64.URLEncoding.EncodeToString(claims)}, ".") + t.raw = strings.Join([]string{t.data, base64.URLEncoding.EncodeToString(jwt.sum(t.data, h))}, ".") + return +} + +// Verify will verify the provided token using the secret key +func (jwt *JWT) Verify(t *Token) error { + if t == nil { + return ErrTokenIsNil + } + now := time.Now().UTC().Unix() + var ( + exp int64 + nbf int64 + expOK bool + nbfOK bool + h Hash + ) + header := t.Header() + switch t := header["typ"].(type) { + case string: + if t != typ { + return ErrNoJWT + } + default: + return ErrNoJWT + } + switch a := header["alg"].(type) { + case string: + h = ParseHash(a) + if h == nil { return ErrUnsupportedAlg } default: @@ -262,7 +241,7 @@ func (jwt *JWT) Verify(t *Token) error { nbfOK = true } if expOK && jwt.blacklist { - err := jwt.blacklisted(t.Signature) + err := jwt.blacklisted(t.Sig()) if err != nil { return err } @@ -273,7 +252,7 @@ func (jwt *JWT) Verify(t *Token) error { if nbfOK && now < nbf { return ErrNBF } - if !hmac.Equal(jwt.sum(t.Data, hf), t.RawSignature) { + if !hmac.Equal(jwt.sum(t.Data(), h), t.RawSig()) { return ErrInvalid } return nil @@ -304,7 +283,7 @@ func DecodeToken(token string) (t *Token, err error) { if err != nil { return } - err = json.Unmarshal(header, &t.Header) + err = json.Unmarshal(header, &t.header) if err != nil { return } @@ -316,29 +295,57 @@ func DecodeToken(token string) (t *Token, err error) { 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 + 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{} + raw string + data string + signature string + rawSignature []byte + header map[string]interface{} Claims map[string]interface{} } +// String returns the tokens encoded string +func (t *Token) String() string { + return t.raw +} + +// Sig returns the Tokens URLEncoded signature +func (t *Token) Sig() string { + return t.signature +} + +// RawSig returns the Tokens raw signature +func (t *Token) RawSig() []byte { + return t.rawSignature +} + +// Header returns the Tokens header +func (t *Token) Header() map[string]interface{} { + return t.header +} + +// Data returns the first two token fields +func (t *Token) Data() string { + return t.data +} + // NewToken returns a new *Token using the provided hash algorithm and claims -func NewToken(hash Hash, claims map[string]interface{}) *Token { +// If h is nil HS256 will be used +func NewToken(claims map[string]interface{}, h Hash) *Token { + if h == nil { + h = NewHS256() + } return &Token{ - Header: map[string]interface{}{ - "alg": hash.String(), + header: map[string]interface{}{ + "alg": h.Alg(), "typ": typ, }, Claims: claims, diff --git a/jwt_test.go b/jwt_test.go index 0d04ad5..ad55a63 100644 --- a/jwt_test.go +++ b/jwt_test.go @@ -1,25 +1,51 @@ package jwt import ( + "errors" "testing" "time" ) func TestValidate(t *testing.T) { - jwt, _ := New(0, true) - token := NewToken(HS256, map[string]interface{}{ + jwt := New(time.Second, true) + token := NewToken(map[string]interface{}{ "sub": "1234567890", "name": "John Doe", "admin": true, "fizz": "buzz", - }) + }, nil) 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() + if err != nil { + t.Error(err) + } + nt, err := DecodeToken(token.String()) + if err != nil { + t.Error(err) + } + err = jwt.Verify(nt) + if err != nil { + t.Error(err) + } + time.Sleep(time.Second * 2) + err = jwt.Verify(nt) + if err == nil { + t.Error(errors.New("token should be expired")) + } + + err = jwt.Invalidate(nt) + if err != nil { + t.Error(err) + } + err = jwt.Invalidate(nt) + if err == nil { + t.Error(errors.New("double invalidate")) + } + err = jwt.Verify(nt) + if err == nil { + t.Error(errors.New("token should be blacklisted")) + } + err = jwt.Stop() + if err != nil { + t.Error(err) + } }