moved sync out of JWT and added new secret reader

This commit is contained in:
ston1th 2018-01-17 22:33:58 +01:00
commit 9b4adfe863
3 changed files with 82 additions and 33 deletions

View file

@ -2,39 +2,60 @@
package jwt package jwt
import "sync"
// Blacklist is the blacklisting storage interface // Blacklist is the blacklisting storage interface
type Blacklist interface { type Blacklist interface {
Add(string, int64) Add(string, int64)
Remove(string) Remove(string)
Check(string) bool Check(string) bool
Map() MapBlacklist Map() BlacklistMap
} }
// MapBlacklist implements the Blacklist interface // BlacklistMap is the blacklist map structure
type MapBlacklist map[string]int64 type BlacklistMap map[string]int64
// NewMapBlacklist // MemBlacklist implements the Blacklist interface
func NewMapBlacklist() MapBlacklist { type MemBlacklist struct {
return make(MapBlacklist) // protects list
sync.RWMutex
list BlacklistMap
}
// NewMemBlacklist
func NewMemBlacklist() *MemBlacklist {
return &MemBlacklist{list: make(BlacklistMap)}
} }
// Add adds a new token signature with expiration time to the blacklist // Add adds a new token signature with expiration time to the blacklist
func (mb MapBlacklist) Add(sig string, exp int64) { func (mb MemBlacklist) Add(sig string, exp int64) {
mb[sig] = exp mb.Lock()
mb.list[sig] = exp
mb.Unlock()
} }
// Remove deletes a token signature from the blacklist // Remove deletes a token signature from the blacklist
func (mb MapBlacklist) Remove(sig string) { func (mb MemBlacklist) Remove(sig string) {
delete(mb, sig) mb.Lock()
delete(mb.list, sig)
mb.Unlock()
} }
// Check returns true if a token signature is blacklisted and false otherwise // Check returns true if a token signature is blacklisted and false otherwise
func (mb MapBlacklist) Check(sig string) (ok bool) { func (mb MemBlacklist) Check(sig string) (ok bool) {
_, ok = mb[sig] mb.RLock()
_, ok = mb.list[sig]
mb.RUnlock()
return return
} }
// Map returns the blacklist in the form of a iterable map structure for cleanup // Map returns the blacklist in the form of a iterable map structure for cleanup
func (mb MapBlacklist) Map() MapBlacklist { func (mb MemBlacklist) Map() (list BlacklistMap) {
return mb list = make(BlacklistMap)
mb.RLock()
for k, v := range mb.list {
list[k] = v
}
mb.RUnlock()
return
} }

37
jwt.go
View file

@ -9,8 +9,8 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"io"
"strings" "strings"
"sync"
"time" "time"
) )
@ -19,11 +19,15 @@ 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
// KeySize is the secret key size
KeySize = 64
typ = "JWT" typ = "JWT"
keySize = 64
) )
// DefaultSecretReader is the default secret key generator
var DefaultSecretReader = rand.Reader
var ( var (
ErrNoJWT = errors.New("not a json web token") ErrNoJWT = errors.New("not a json web token")
ErrEmptyToken = errors.New("token is empty") ErrEmptyToken = errors.New("token is empty")
@ -37,6 +41,7 @@ var (
ErrMissingTokenParts = errors.New("missing token parts") ErrMissingTokenParts = errors.New("missing token parts")
ErrEmptySignature = errors.New("token signature is empty") ErrEmptySignature = errors.New("token signature is empty")
ErrTokenIsNil = errors.New("token is nil") ErrTokenIsNil = errors.New("token is nil")
ErrInvalidKeySize = errors.New("invalid secret key size")
) )
// JWT represents the JSON Web Token signing and blacklisting infrastructure // JWT represents the JSON Web Token signing and blacklisting infrastructure
@ -44,8 +49,6 @@ type JWT struct {
key []byte key []byte
expiry time.Duration expiry time.Duration
// protects list
sync.RWMutex
blacklist Blacklist blacklist Blacklist
done chan struct{} done chan struct{}
} }
@ -54,12 +57,22 @@ 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 Blacklist) *JWT { func New(expiry time.Duration, blacklist Blacklist, secret io.Reader) (*JWT, error) {
if expiry <= 0 { if expiry <= 0 {
expiry = DefaultExpiry expiry = DefaultExpiry
} }
key := make([]byte, keySize) if secret == nil {
rand.Read(key) secret = DefaultSecretReader
}
secret = io.LimitReader(secret, KeySize)
key := make([]byte, KeySize)
i, err := secret.Read(key)
if err != nil {
return nil, errors.New("secret reader error: " + err.Error())
}
if i < KeySize {
return nil, ErrInvalidKeySize
}
jwt := &JWT{ jwt := &JWT{
key: key, key: key,
expiry: expiry, expiry: expiry,
@ -69,7 +82,7 @@ func New(expiry time.Duration, blacklist Blacklist) *JWT {
jwt.done = make(chan struct{}) jwt.done = make(chan struct{})
go jwt.clean() go jwt.clean()
} }
return jwt return jwt, nil
} }
func (jwt *JWT) sum(token string, h Hash) []byte { func (jwt *JWT) sum(token string, h Hash) []byte {
@ -107,8 +120,6 @@ func (jwt *JWT) Invalidate(t *Token) error {
if err := jwt.blacklisted(t.Sig()); err != nil { if err := jwt.blacklisted(t.Sig()); err != nil {
return err return err
} }
jwt.Lock()
defer jwt.Unlock()
jwt.blacklist.Add(t.Sig(), exp) jwt.blacklist.Add(t.Sig(), exp)
return nil return nil
} }
@ -118,8 +129,6 @@ func (jwt *JWT) blacklisted(sig string) error {
if sig == "" { if sig == "" {
return ErrEmptySignature return ErrEmptySignature
} }
jwt.RLock()
defer jwt.RUnlock()
if jwt.blacklist.Check(sig) { if jwt.blacklist.Check(sig) {
return ErrBlacklisted return ErrBlacklisted
} }
@ -137,13 +146,11 @@ func (jwt *JWT) clean() {
return return
} }
now := time.Now().UTC().Unix() now := time.Now().UTC().Unix()
jwt.Lock()
for k, v := range jwt.blacklist.Map() { for k, v := range jwt.blacklist.Map() {
if now > v { if now > v {
jwt.blacklist.Remove(k) jwt.blacklist.Remove(k)
} }
} }
jwt.Unlock()
} }
} }

View file

@ -3,20 +3,24 @@
package jwt package jwt
import ( import (
"bytes"
"errors" "errors"
"testing" "testing"
"time" "time"
) )
func TestValidate(t *testing.T) { func TestValidate(t *testing.T) {
jwt := New(time.Second, NewMapBlacklist()) jwt, err := New(time.Second, NewMemBlacklist(), nil)
if err != nil {
t.Error(err)
}
token := NewToken(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) }, nil)
err := jwt.Sign(token) err = jwt.Sign(token)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -57,14 +61,17 @@ func TestValidate(t *testing.T) {
} }
func TestNoBlacklist(t *testing.T) { func TestNoBlacklist(t *testing.T) {
jwt := New(time.Second, nil) jwt, err := New(time.Second, nil, nil)
if err != nil {
t.Error(err)
}
token := NewToken(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) }, nil)
err := jwt.Sign(token) err = jwt.Sign(token)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -91,3 +98,17 @@ func TestNoBlacklist(t *testing.T) {
t.Error(errors.New("blacklisting should be disabled")) t.Error(errors.New("blacklisting should be disabled"))
} }
} }
func TestEmptySecretReader(t *testing.T) {
_, err := New(time.Second, nil, new(bytes.Buffer))
if err == nil {
t.Error(errors.New("error should be secret reader error"))
}
}
func TestInvalidSecretReader(t *testing.T) {
_, err := New(time.Second, nil, bytes.NewBufferString("123"))
if err != ErrInvalidKeySize {
t.Error(errors.New("error should be invalid key size"))
}
}