added blacklist storage interface

This commit is contained in:
ston1th 2018-01-17 21:53:53 +01:00
commit 4c14116a41
3 changed files with 92 additions and 23 deletions

40
blacklist.go Normal file
View file

@ -0,0 +1,40 @@
// Copyright (C) 2018 Marius Schellenberger
package jwt
// Blacklist is the blacklisting storage interface
type Blacklist interface {
Add(string, int64)
Remove(string)
Check(string) bool
Map() MapBlacklist
}
// MapBlacklist implements the Blacklist interface
type MapBlacklist map[string]int64
// NewMapBlacklist
func NewMapBlacklist() MapBlacklist {
return make(MapBlacklist)
}
// Add adds a new token signature with expiration time to the blacklist
func (mb MapBlacklist) Add(sig string, exp int64) {
mb[sig] = exp
}
// Remove deletes a token signature from the blacklist
func (mb MapBlacklist) Remove(sig string) {
delete(mb, sig)
}
// Check returns true if a token signature is blacklisted and false otherwise
func (mb MapBlacklist) Check(sig string) (ok bool) {
_, ok = mb[sig]
return
}
// Map returns the blacklist in the form of a iterable map structure for cleanup
func (mb MapBlacklist) Map() MapBlacklist {
return mb
}

37
jwt.go
View file

@ -46,16 +46,15 @@ type JWT struct {
// protects list // protects list
sync.RWMutex sync.RWMutex
blacklist bool blacklist Blacklist
list map[string]int64 done chan struct{}
stop chan struct{}
} }
// New returns a new JWT object with the given expiry timeout. // 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 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 bool) *JWT { func New(expiry time.Duration, blacklist Blacklist) *JWT {
if expiry <= 0 { if expiry <= 0 {
expiry = DefaultExpiry expiry = DefaultExpiry
} }
@ -66,9 +65,8 @@ func New(expiry time.Duration, blacklist bool) *JWT {
expiry: expiry, expiry: expiry,
blacklist: blacklist, blacklist: blacklist,
} }
if blacklist { if blacklist != nil {
jwt.list = make(map[string]int64) jwt.done = make(chan struct{})
jwt.stop = make(chan struct{})
go jwt.clean() go jwt.clean()
} }
return jwt return jwt
@ -83,7 +81,7 @@ func (jwt *JWT) sum(token string, h Hash) []byte {
// Invalidate checks if a token is already blacklisted // Invalidate checks if a token is already blacklisted
// If the token is not blacklisted, it will get blacklisted // If the token is not blacklisted, it will get blacklisted
func (jwt *JWT) Invalidate(t *Token) error { func (jwt *JWT) Invalidate(t *Token) error {
if !jwt.blacklist { if jwt.blacklist == nil {
return ErrBlacklistNotEnabled return ErrBlacklistNotEnabled
} }
if t == nil { if t == nil {
@ -111,7 +109,7 @@ func (jwt *JWT) Invalidate(t *Token) error {
} }
jwt.Lock() jwt.Lock()
defer jwt.Unlock() defer jwt.Unlock()
jwt.list[t.Sig()] = exp jwt.blacklist.Add(t.Sig(), exp)
return nil return nil
} }
@ -122,28 +120,27 @@ func (jwt *JWT) blacklisted(sig string) error {
} }
jwt.RLock() jwt.RLock()
defer jwt.RUnlock() defer jwt.RUnlock()
_, ok := jwt.list[sig] if jwt.blacklist.Check(sig) {
if ok {
return ErrBlacklisted return ErrBlacklisted
} }
return nil return nil
} }
// clean will look for expired blacklisted tokens and removes them // clean looks for expired blacklisted tokens and removes them
func (jwt *JWT) clean() { func (jwt *JWT) clean() {
for { for {
t := time.NewTimer(time.Hour) t := time.NewTimer(time.Hour)
select { select {
case <-t.C: case <-t.C:
case <-jwt.stop: case <-jwt.done:
t.Stop() t.Stop()
return return
} }
now := time.Now().UTC().Unix() now := time.Now().UTC().Unix()
jwt.Lock() jwt.Lock()
for k, v := range jwt.list { for k, v := range jwt.blacklist.Map() {
if now > v { if now > v {
delete(jwt.list, k) jwt.blacklist.Remove(k)
} }
} }
jwt.Unlock() jwt.Unlock()
@ -243,7 +240,7 @@ func (jwt *JWT) Verify(t *Token) error {
nbf = int64(n) nbf = int64(n)
nbfOK = true nbfOK = true
} }
if expOK && jwt.blacklist { if jwt.blacklist != nil {
err := jwt.blacklisted(t.Sig()) err := jwt.blacklisted(t.Sig())
if err != nil { if err != nil {
return err return err
@ -264,14 +261,10 @@ func (jwt *JWT) Verify(t *Token) error {
// Stop will end the cleaner goroutinge execution // Stop will end the cleaner goroutinge execution
// Calling Stop twice or more will panic // Calling Stop twice or more will panic
func (jwt *JWT) Stop() error { func (jwt *JWT) Stop() error {
if !jwt.blacklist { if jwt.blacklist == nil {
return ErrBlacklistNotEnabled return ErrBlacklistNotEnabled
} }
select { close(jwt.done)
case jwt.stop <- struct{}{}:
close(jwt.stop)
default:
}
return nil return nil
} }

View file

@ -9,7 +9,7 @@ import (
) )
func TestValidate(t *testing.T) { func TestValidate(t *testing.T) {
jwt := New(time.Second, true) jwt := New(time.Second, NewMapBlacklist())
token := NewToken(map[string]interface{}{ token := NewToken(map[string]interface{}{
"sub": "1234567890", "sub": "1234567890",
"name": "John Doe", "name": "John Doe",
@ -55,3 +55,39 @@ func TestValidate(t *testing.T) {
t.Error(err) t.Error(err)
} }
} }
func TestNoBlacklist(t *testing.T) {
jwt := New(time.Second, nil)
token := NewToken(map[string]interface{}{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"fizz": "buzz",
}, nil)
err := jwt.Sign(token)
if err != nil {
t.Error(err)
}
_, err = DecodeToken("")
if err == nil {
t.Error(errors.New("token is empty"))
}
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(errors.New("blacklisting should be disabled"))
}
}