From 09e12282d04859b3fb2c6b91f230a2ba67bb9fb7 Mon Sep 17 00:00:00 2001 From: ston1th Date: Tue, 6 Sep 2016 19:57:41 +0200 Subject: [PATCH] initial commit --- LICENSE | 24 ++++ README.md | 1 + jwt.go | 346 ++++++++++++++++++++++++++++++++++++++++++++++++++++ jwt_test.go | 25 ++++ 4 files changed, 396 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 jwt.go create mode 100644 jwt_test.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9e52d9b --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad89f76 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# jwt - a simple JSON Web Token library in Go diff --git a/jwt.go b/jwt.go new file mode 100644 index 0000000..03c545c --- /dev/null +++ b/jwt.go @@ -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, + } +} diff --git a/jwt_test.go b/jwt_test.go new file mode 100644 index 0000000..0d04ad5 --- /dev/null +++ b/jwt_test.go @@ -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() +}