rewrite and new Hash interface

This commit is contained in:
ston1th 2016-12-29 11:08:25 +01:00
commit 40fe9931a8
3 changed files with 215 additions and 96 deletions

86
hash.go Normal file
View file

@ -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()
}

215
jwt.go
View file

@ -4,12 +4,9 @@ package jwt
import ( import (
"crypto/hmac" "crypto/hmac"
"crypto/rand" "crypto/rand"
"crypto/sha256"
"crypto/sha512"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"hash"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -20,33 +17,11 @@ const (
Header = "Authorization" Header = "Authorization"
// DefaultExpiry is the default token expiration time // DefaultExpiry is the default token expiration time
DefaultExpiry = time.Hour * 12 DefaultExpiry = time.Hour * 12
typ = "JWT" typ = "JWT"
keySize = 64 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 ( var (
ErrNoJWT = errors.New("no json web token") ErrNoJWT = errors.New("no json web token")
ErrUnsupportedAlg = errors.New("unsupported algoritm") ErrUnsupportedAlg = errors.New("unsupported algoritm")
@ -58,21 +33,9 @@ var (
ErrMissingEXP = errors.New("missing exp claim") ErrMissingEXP = errors.New("missing exp claim")
ErrMissingTokenParts = errors.New("missíng token parts") ErrMissingTokenParts = errors.New("missíng token parts")
ErrEmptySignature = errors.New("token signature is empty") 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 // JWT represents the JSON Web Token signing and blacklisting infrastructure
type JWT struct { type JWT struct {
key []byte 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 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. // If blacklisting is enabled, the JWT object leaks a goroutine to garbage-collect expired blacklisted tokens.
// Call the Stop() method to exit the goroutine. // 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 { if expiry <= 0 {
expiry = DefaultExpiry expiry = DefaultExpiry
} }
key := make([]byte, keySize) key := make([]byte, keySize)
_, err := rand.Read(key) rand.Read(key)
if err != nil {
return nil, err
}
jwt := &JWT{ jwt := &JWT{
key: key, key: key,
expiry: expiry, expiry: expiry,
@ -108,11 +68,11 @@ func New(expiry time.Duration, blacklist bool) (*JWT, error) {
jwt.stop = make(chan struct{}) jwt.stop = make(chan struct{})
go jwt.clean() go jwt.clean()
} }
return jwt, nil return jwt
} }
func (jwt *JWT) sum(token string, h func() hash.Hash) []byte { func (jwt *JWT) sum(token string, h Hash) []byte {
mac := hmac.New(h, jwt.key) mac := hmac.New(h.Hash, jwt.key)
mac.Write([]byte(token)) mac.Write([]byte(token))
return mac.Sum(nil) return mac.Sum(nil)
} }
@ -123,6 +83,9 @@ func (jwt *JWT) Invalidate(t *Token) error {
if !jwt.blacklist { if !jwt.blacklist {
return ErrBlacklistNotEnabled return ErrBlacklistNotEnabled
} }
if t == nil {
return ErrTokenIsNil
}
var ( var (
expOK bool expOK bool
exp int64 exp int64
@ -140,12 +103,12 @@ func (jwt *JWT) Invalidate(t *Token) error {
if !expOK { if !expOK {
return ErrMissingEXP return ErrMissingEXP
} }
if err := jwt.blacklisted(t.Signature); err != nil { if err := jwt.blacklisted(t.Sig()); err != nil {
return err return err
} }
jwt.Lock() jwt.Lock()
defer jwt.Unlock() defer jwt.Unlock()
jwt.list[t.Signature] = exp jwt.list[t.Sig()] = exp
return nil return nil
} }
@ -187,43 +150,11 @@ func (jwt *JWT) clean() {
// Sign will sign the provided token using the secret key // Sign will sign the provided token using the secret key
// This will overwrite existing 'exp' and 'nbf' claims // This will overwrite existing 'exp' and 'nbf' claims
func (jwt *JWT) Sign(t *Token) (err error) { func (jwt *JWT) Sign(t *Token) (err error) {
now := time.Now().UTC() if t == nil {
t.Claims["exp"] = now.Add(jwt.expiry).Unix() return ErrTokenIsNil
t.Claims["nbf"] = now.Unix()
h, err := json.Marshal(t.Header)
if err != nil {
return
} }
c, err := json.Marshal(t.Claims) header := t.Header()
if err != nil { switch t := header["typ"].(type) {
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: case string:
if t != typ { if t != typ {
return ErrNoJWT return ErrNoJWT
@ -231,10 +162,58 @@ func (jwt *JWT) Verify(t *Token) error {
default: default:
return ErrNoJWT return ErrNoJWT
} }
switch a := t.Header["alg"].(type) { var h Hash
switch a := header["alg"].(type) {
case string: case string:
hf = parseHash(a) h = ParseHash(a)
if hf == nil { 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 return ErrUnsupportedAlg
} }
default: default:
@ -262,7 +241,7 @@ func (jwt *JWT) Verify(t *Token) error {
nbfOK = true nbfOK = true
} }
if expOK && jwt.blacklist { if expOK && jwt.blacklist {
err := jwt.blacklisted(t.Signature) err := jwt.blacklisted(t.Sig())
if err != nil { if err != nil {
return err return err
} }
@ -273,7 +252,7 @@ func (jwt *JWT) Verify(t *Token) error {
if nbfOK && now < nbf { if nbfOK && now < nbf {
return ErrNBF 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 ErrInvalid
} }
return nil return nil
@ -304,7 +283,7 @@ func DecodeToken(token string) (t *Token, err error) {
if err != nil { if err != nil {
return return
} }
err = json.Unmarshal(header, &t.Header) err = json.Unmarshal(header, &t.header)
if err != nil { if err != nil {
return return
} }
@ -316,29 +295,57 @@ func DecodeToken(token string) (t *Token, err error) {
if err != nil { if err != nil {
return return
} }
t.RawSignature, err = base64.URLEncoding.DecodeString(parts[2]) t.rawSignature, err = base64.URLEncoding.DecodeString(parts[2])
t.Signature = parts[2] t.signature = parts[2]
t.Data = strings.Join(parts[:2], ".") t.data = strings.Join(parts[:2], ".")
t.Raw = token t.raw = token
return return
} }
// Token is the JWT token representation // Token is the JWT token representation
type Token struct { type Token struct {
Raw string raw string
Data string data string
Signature string signature string
RawSignature []byte rawSignature []byte
Exp int64 header map[string]interface{}
Header map[string]interface{}
Claims 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 // 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{ return &Token{
Header: map[string]interface{}{ header: map[string]interface{}{
"alg": hash.String(), "alg": h.Alg(),
"typ": typ, "typ": typ,
}, },
Claims: claims, Claims: claims,

View file

@ -1,25 +1,51 @@
package jwt package jwt
import ( import (
"errors"
"testing" "testing"
"time" "time"
) )
func TestValidate(t *testing.T) { func TestValidate(t *testing.T) {
jwt, _ := New(0, true) jwt := New(time.Second, true)
token := NewToken(HS256, map[string]interface{}{ token := NewToken(map[string]interface{}{
"sub": "1234567890", "sub": "1234567890",
"name": "John Doe", "name": "John Doe",
"admin": true, "admin": true,
"fizz": "buzz", "fizz": "buzz",
}) }, nil)
err := jwt.Sign(token) err := jwt.Sign(token)
t.Log(err, token) if err != nil {
nt, err := DecodeToken(token.Raw) t.Error(err)
t.Log(nt, err) }
time.Sleep(time.Second) nt, err := DecodeToken(token.String())
t.Log(jwt.Verify(nt)) if err != nil {
jwt.Invalidate(nt) t.Error(err)
t.Log(jwt.Verify(nt)) }
jwt.Stop() 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)
}
} }