-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add token pkg and support apikey, x header
- Loading branch information
Showing
15 changed files
with
1,096 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.