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