Skip to content

Commit

Permalink
feat: add token pkg and support apikey, x header
Browse files Browse the repository at this point in the history
  • Loading branch information
shaj13 committed Aug 5, 2020
1 parent 29d6874 commit 4a114db
Show file tree
Hide file tree
Showing 15 changed files with 1,096 additions and 1 deletion.
7 changes: 6 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,9 @@ issues:
- path: _test\.go
linters:
- errcheck
- gosec
- gosec

# FIXME temporarily suppress this.
- path: "auth/strategies/(token|bearer)/"
linters:
- dupl
102 changes: 102 additions & 0 deletions auth/strategies/token/cached.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package token

import (
"context"
"net/http"

"github.com/shaj13/go-guardian/auth"
"github.com/shaj13/go-guardian/errors"
"github.com/shaj13/go-guardian/store"
)

// CachedStrategyKey export identifier for the cached bearer strategy,
// commonly used when enable/add strategy to go-guardian authenticator.
const CachedStrategyKey = auth.StrategyKey("Token.Cached.Strategy")

// AuthenticateFunc declare custom function to authenticate request using token.
// The authenticate function invoked by Authenticate Strategy method when
// The token does not exist in the cahce and the invocation result will be cached, unless an error returned.
// Use NoOpAuthenticate instead to refresh/mangae token directly using cache or Append function.
type AuthenticateFunc func(ctx context.Context, r *http.Request, token string) (auth.Info, error)

// New return new auth.Strategy.
// The returned strategy, caches the invocation result of authenticate function, See AuthenticateFunc.
// Use NoOpAuthenticate to refresh/mangae token directly using cache or Append function, See NoOpAuthenticate.
func New(auth AuthenticateFunc, c store.Cache, opts ...auth.Option) auth.Strategy {
if auth == nil {
panic("Authenticate Function required and can't be nil")
}

if c == nil {
panic("Cache object required and can't be nil")
}

cached := &cachedToken{
authFunc: auth,
cache: c,
typ: Bearer,
parser: AuthorizationParser(string(Bearer)),
}

for _, opt := range opts {
opt.Apply(cached)
}

return cached
}

type cachedToken struct {
parser Parser
typ Type
cache store.Cache
authFunc 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
}

info, ok, err := c.cache.Load(token, r)

if err != nil {
return nil, err
}

// if token not found invoke user authenticate function
if !ok {
info, err = c.authFunc(ctx, r, token)
if err == nil {
// cache result
err = c.cache.Store(token, info, r)
}
}

if err != nil {
return nil, err
}

if _, ok := info.(auth.Info); !ok {
return nil, errors.NewInvalidType((*auth.Info)(nil), info)
}

return info.(auth.Info), nil
}

func (c *cachedToken) Append(token string, info auth.Info, r *http.Request) error {
return c.cache.Store(token, info, r)
}

func (c *cachedToken) Revoke(token string, r *http.Request) error {
return c.cache.Delete(token, r)
}

func (c *cachedToken) Challenge(realm string) string { return challenge(realm, c.typ) }

// NoOpAuthenticate implements Authenticate function, it return nil, auth.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, error) {
return nil, auth.ErrNOOP
}
169 changes: 169 additions & 0 deletions auth/strategies/token/cached_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package token

import (
"context"
"fmt"
"net/http"
"testing"

"github.com/stretchr/testify/assert"

"github.com/shaj13/go-guardian/auth"
"github.com/shaj13/go-guardian/store"
)

func TestNewCahced(t *testing.T) {
table := []struct {
name string
panic bool
expectedErr bool
cache store.Cache
authFunc AuthenticateFunc
info interface{}
token string
}{
{
name: "it return error when cache load return error",
expectedErr: true,
panic: false,
cache: make(mockCache),
token: "error",
authFunc: NoOpAuthenticate,
info: nil,
},
{
name: "it return error when user authenticate func return error",
expectedErr: true,
cache: make(mockCache),
authFunc: NoOpAuthenticate,
panic: false,
info: nil,
},
{
name: "it return error when cache store return error",
expectedErr: true,
cache: make(mockCache),
authFunc: func(_ context.Context, _ *http.Request, _ string) (auth.Info, error) { return nil, nil },
token: "store-error",
panic: false,
info: nil,
},
{
name: "it return error when cache return invalid type",
expectedErr: true,
cache: make(mockCache),
authFunc: func(_ context.Context, _ *http.Request, _ string) (auth.Info, error) { return nil, nil },
panic: false,
info: "sample-data",
token: "valid",
},
{
name: "it return user when token cached",
expectedErr: false,
cache: make(mockCache),
authFunc: NoOpAuthenticate,
panic: false,
info: auth.NewDefaultUser("1", "1", nil, nil),
token: "valid-user",
},
{
name: "it panic when Authenticate func nil",
expectedErr: false,
panic: true,
info: nil,
},
{
name: "it panic when Cache nil",
expectedErr: false,
authFunc: NoOpAuthenticate,
panic: true,
info: nil,
},
}

for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
if tt.panic {
assert.Panics(t, func() {
New(tt.authFunc, tt.cache)
})
return
}

strategy := New(tt.authFunc, tt.cache)
r, _ := http.NewRequest("GET", "/", nil)
r.Header.Set("Authorization", "Bearer "+tt.token)
_ = tt.cache.Store(tt.token, tt.info, r)
info, err := strategy.Authenticate(r.Context(), r)
if tt.expectedErr {
assert.Error(t, err)
return
}
assert.Equal(t, tt.info, info)
})
}
}

func TestCahcedTokenAppend(t *testing.T) {
cache := make(mockCache)
strategy := &cachedToken{cache: cache}
info := auth.NewDefaultUser("1", "2", nil, nil)
strategy.Append("test-append", info, nil)
cachedInfo, ok, _ := cache.Load("test-append", nil)
assert.True(t, ok)
assert.Equal(t, info, cachedInfo)
}

func TestCahcedTokenChallenge(t *testing.T) {
strategy := &cachedToken{
typ: Bearer,
}

got := strategy.Challenge("Test Realm")
expected := `Bearer realm="Test Realm", title="Bearer Token Based Authentication Scheme"`

assert.Equal(t, expected, got)
}

func BenchmarkCachedToken(b *testing.B) {
r, _ := http.NewRequest("GET", "/", nil)
r.Header.Set("Authorization", "Bearer token")

cache := make(mockCache)
cache.Store("token", auth.NewDefaultUser("benchmark", "1", nil, nil), r)

strategy := New(NoOpAuthenticate, cache)

b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, err := strategy.Authenticate(r.Context(), r)
if err != nil {
b.Error(err)
}
}
})
}

type mockCache map[string]interface{}

func (m mockCache) Load(key string, _ *http.Request) (interface{}, bool, error) {
if key == "error" {
return nil, false, fmt.Errorf("Load Error")
}
v, ok := m[key]
return v, ok, nil
}

func (m mockCache) Store(key string, value interface{}, _ *http.Request) error {
if key == "store-error" {
return fmt.Errorf("Store Error")
}
m[key] = value
return nil
}
func (m mockCache) Keys() []string { return nil }

func (m mockCache) Delete(key string, _ *http.Request) error {
return nil
}
Loading

0 comments on commit 4a114db

Please sign in to comment.