rewrite and new Hash interface
This commit is contained in:
parent
e75e02ab44
commit
40fe9931a8
3 changed files with 215 additions and 96 deletions
86
hash.go
Normal file
86
hash.go
Normal 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()
|
||||
}
|
||||
219
jwt.go
219
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,
|
||||
|
|
|
|||
48
jwt_test.go
48
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue