initial commit
This commit is contained in:
commit
09e12282d0
4 changed files with 396 additions and 0 deletions
24
LICENSE
Normal file
24
LICENSE
Normal 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
1
README.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# jwt - a simple JSON Web Token library in Go
|
||||||
346
jwt.go
Normal file
346
jwt.go
Normal 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
25
jwt_test.go
Normal 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()
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue