Skip to content

Commit

Permalink
chore: refactor token startegy and consolidate static and cache logic…
Browse files Browse the repository at this point in the history
… overlaps under single core
  • Loading branch information
shaj13 committed Feb 3, 2021
1 parent 61e7326 commit 1f53240
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 250 deletions.
96 changes: 30 additions & 66 deletions auth/strategies/token/cached.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,94 +6,58 @@ import (
"time"

"github.com/shaj13/go-guardian/v2/auth"
"github.com/shaj13/go-guardian/v2/auth/internal"
)

// AuthenticateFunc declare function signature to authenticate request using token.
// Any function that has the appropriate signature can be registered to the token strategy.
// AuthenticateFunc must return authenticated user info and token expiry time, otherwise error.
type AuthenticateFunc func(ctx context.Context, r *http.Request, token string) (auth.Info, time.Time, error)

// New return new token strategy that caches the invocation result of authenticate function.
func New(fn AuthenticateFunc, c auth.Cache, opts ...auth.Option) auth.Strategy {
cached := &cachedToken{
authFunc: fn,
verify: func(_ context.Context, _ *http.Request, _ auth.Info, _ string) error {
return nil
},
cache: c,
typ: Bearer,
parser: AuthorizationParser(string(Bearer)),
h: internal.PlainTextHasher{},
}

for _, opt := range opts {
opt.Apply(cached)
}
// NoOpAuthenticate implements AuthenticateFunc, it return nil, time.Time{}, ErrNOOP,
// commonly used when token refreshed/mangaed directly using cache or Append function,
// and there is no need to parse token and authenticate request.
func NoOpAuthenticate(ctx context.Context, r *http.Request, token string) (auth.Info, time.Time, error) {
return nil, time.Time{}, ErrNOOP
}

return cached
// New return new token strategy that caches the invocation result of authenticate function.
func New(fn AuthenticateFunc, ac auth.Cache, opts ...auth.Option) auth.Strategy {
c := new(cachedToken)
c.cache = ac
c.fn = fn
return newCore(c, opts...)
}

type cachedToken struct {
parser Parser
verify verify
typ Type
cache auth.Cache
authFunc AuthenticateFunc
h internal.Hasher
cache auth.Cache
fn AuthenticateFunc
}

func (c *cachedToken) Authenticate(ctx context.Context, r *http.Request) (auth.Info, error) {
token, err := c.parser.Token(r)
if err != nil {
return nil, err
}

hash := c.h.Hash(token)
i, ok := c.cache.Load(hash)

// if token not found invoke user authenticate function
if !ok {
var t time.Time
i, t, err = c.authFunc(ctx, r, token)
if err != nil {
return nil, err
func (c *cachedToken) authenticate(ctx context.Context, r *http.Request, hash, token string) (auth.Info, error) {
if v, ok := c.cache.Load(hash); ok {
info, ok := v.(auth.Info)
if !ok {
return nil, auth.NewTypeError("strategies/token:", (*auth.Info)(nil), v)
}
c.cache.StoreWithTTL(hash, i, time.Until(t))
}

info, ok := i.(auth.Info)

if !ok {
return nil, auth.NewTypeError("strategies/token:", (*auth.Info)(nil), i)
return info, nil
}

if err := c.verify(ctx, r, info, token); err != nil {
// token not found invoke user authenticate function
info, t, err := c.fn(ctx, r, token)
if err != nil {
return nil, err
}

c.cache.StoreWithTTL(hash, info, time.Until(t))
return info, nil
}

func (c *cachedToken) Append(token interface{}, info auth.Info) error {
if str, ok := token.(string); ok {
hash := c.h.Hash(str)
c.cache.Store(hash, info)
}
return auth.NewTypeError("strategies/token:", "str", token)
}

func (c *cachedToken) Revoke(token interface{}) error {
if str, ok := token.(string); ok {
hash := c.h.Hash(str)
c.cache.Delete(hash)
}
return auth.NewTypeError("strategies/token:", "str", token)
func (c *cachedToken) append(token string, info auth.Info) error {
c.cache.Store(token, info)
return nil
}

// NoOpAuthenticate implements AuthenticateFunc, it return nil, time.Time{}, ErrNOOP,
// commonly used when token refreshed/mangaed directly using cache or Append function,
// and there is no need to parse token and authenticate request.
func NoOpAuthenticate(ctx context.Context, r *http.Request, token string) (auth.Info, time.Time, error) {
return nil, time.Time{}, ErrNOOP
func (c *cachedToken) revoke(token string) error {
c.cache.Delete(token)
return nil
}
8 changes: 2 additions & 6 deletions auth/strategies/token/cached_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"github.com/stretchr/testify/assert"

"github.com/shaj13/go-guardian/v2/auth"
"github.com/shaj13/go-guardian/v2/auth/internal"
)

func TestNewCahced(t *testing.T) {
Expand Down Expand Up @@ -65,12 +64,9 @@ func TestNewCahced(t *testing.T) {

func TestCahcedTokenAppend(t *testing.T) {
cache := libcache.LRU.New(0)
strategy := &cachedToken{
cache: cache,
h: internal.PlainTextHasher{},
}
s := New(nil, cache)
info := auth.NewDefaultUser("1", "2", nil, nil)
strategy.Append("test-append", info)
auth.Append(s, "test-append", info)
cachedInfo, ok := cache.Load("test-append")
assert.True(t, ok)
assert.Equal(t, info, cachedInfo)
Expand Down
46 changes: 46 additions & 0 deletions auth/strategies/token/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package token

import (
"crypto"

"github.com/shaj13/go-guardian/v2/auth"
"github.com/shaj13/go-guardian/v2/auth/internal"
)

// SetType sets the authentication token type or scheme,
// used for HTTP WWW-Authenticate header.
//
// Deprecated: No longer used.
func SetType(t Type) auth.Option {
return auth.OptionFunc(func(v interface{}) {
})
}

// SetParser sets the strategy token parser.
func SetParser(p Parser) auth.Option {
return auth.OptionFunc(func(v interface{}) {
if v, ok := v.(*core); ok {
v.parser = p
}
})
}

// SetScopes sets the scopes to be used when verifying user access token.
func SetScopes(scopes ...Scope) auth.Option {
return auth.OptionFunc(func(v interface{}) {
if v, ok := v.(*core); ok {
v.verify = verifyScopes(scopes...)
}
})
}

// SetHash apply token hashing based on HMAC with h and key,
// To prevent precomputation and length extension attacks,
// and to mitigates hash map DOS attacks via collisions.
func SetHash(h crypto.Hash, key []byte) auth.Option {
return auth.OptionFunc(func(v interface{}) {
if v, ok := v.(*core); ok {
v.hasher = internal.NewHMACHasher(h, key)
}
})
}
32 changes: 32 additions & 0 deletions auth/strategies/token/options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package token

import (
"crypto"
"testing"

"github.com/stretchr/testify/assert"
)

func TestSetParser(t *testing.T) {
c := new(core)
p := XHeaderParser("")
opt := SetParser(p)
opt.Apply(c)
assert.True(t, c.parser != nil)
}

func TestSetScopes(t *testing.T) {
c := new(core)
opt := SetScopes(NewScope("admin", "", ""))
opt.Apply(c)
assert.True(t, c.verify != nil)
}

func TestSetHash(t *testing.T) {
const token = "token"
c := new(core)
opt := SetHash(crypto.SHA256, []byte("key"))
opt.Apply(c)
assert.True(t, c.hasher != nil)
assert.NotEqual(t, token, c.hasher.Hash(token))
}
110 changes: 41 additions & 69 deletions auth/strategies/token/static.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,65 +11,8 @@ import (
"sync"

"github.com/shaj13/go-guardian/v2/auth"
"github.com/shaj13/go-guardian/v2/auth/internal"
)

// Static implements auth.Strategy and define a synchronized map honor all predefined bearer tokens.
type static struct {
mu *sync.Mutex
tokens map[string]auth.Info
h internal.Hasher
ttype Type
verify verify
parser Parser
}

// Authenticate user request against predefined tokens by verifying request token existence in the static Map.
// Once token found auth.Info returned with a nil error,
// Otherwise, a nil auth.Info and ErrTokenNotFound returned.
func (s *static) Authenticate(ctx context.Context, r *http.Request) (auth.Info, error) {
token, err := s.parser.Token(r)
if err != nil {
return nil, err
}

s.mu.Lock()
defer s.mu.Unlock()
info, ok := s.tokens[token]

if !ok {
return nil, ErrTokenNotFound
}

if err := s.verify(ctx, r, info, token); err != nil {
return nil, err
}

return info, nil
}

// Append add new token to static store.
func (s *static) Append(token interface{}, info auth.Info) error {
s.mu.Lock()
defer s.mu.Unlock()
if str, ok := token.(string); ok {
hash := s.h.Hash(str)
s.tokens[hash] = info
}
return auth.NewTypeError("strategies/token:", "str", token)
}

// Revoke delete token from static store.
func (s *static) Revoke(token interface{}) error {
s.mu.Lock()
defer s.mu.Unlock()
if str, ok := token.(string); ok {
hash := s.h.Hash(str)
delete(s.tokens, hash)
}
return auth.NewTypeError("strategies/token:", "str", token)
}

// NewStaticFromFile returns static auth.Strategy, populated from a CSV file.
// The CSV file must contain records in one of following formats
// basic record: `token,username,userid`
Expand Down Expand Up @@ -142,24 +85,53 @@ func NewStaticFromFile(path string, opts ...auth.Option) (auth.Strategy, error)

// NewStatic returns static auth.Strategy, populated from a map.
func NewStatic(tokens map[string]auth.Info, opts ...auth.Option) auth.Strategy {
static := &static{
s := &static{
tokens: make(map[string]auth.Info, len(tokens)),
h: internal.PlainTextHasher{},
verify: func(_ context.Context, _ *http.Request, _ auth.Info, _ string) error {
return nil
},
mu: new(sync.Mutex),
ttype: Bearer,
parser: AuthorizationParser(string(Bearer)),
}

for _, opt := range opts {
opt.Apply(static)
}
c := newCore(s, opts...)

for k, v := range tokens {
_ = static.Append(k, v)
_ = c.Append(k, v)
}

return c
}

// Static implements strategy and define a synchronized map honor all predefined bearer tokens.
type static struct {
mu *sync.Mutex
tokens map[string]auth.Info
}

// authenticate user request against predefined tokens by verifying request token existence in the static Map.
// Once token found auth.Info returned with a nil error,
// Otherwise, a nil auth.Info and ErrTokenNotFound returned.
func (s *static) authenticate(ctx context.Context, r *http.Request, hash, _ string) (auth.Info, error) {
s.mu.Lock()
defer s.mu.Unlock()
info, ok := s.tokens[hash]

if !ok {
return nil, ErrTokenNotFound
}

return static
return info, nil
}

// Append add new token to static store.
func (s *static) append(token string, info auth.Info) error {
s.mu.Lock()
defer s.mu.Unlock()
s.tokens[token] = info
return nil
}

// Revoke delete token from static store.
func (s *static) revoke(token string) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.tokens, token)
return nil
}
Loading

0 comments on commit 1f53240

Please sign in to comment.