Skip to content

Commit 4a114db

Browse files
committed
feat: add token pkg and support apikey, x header
1 parent 29d6874 commit 4a114db

File tree

15 files changed

+1096
-1
lines changed

15 files changed

+1096
-1
lines changed

.golangci.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,9 @@ issues:
4646
- path: _test\.go
4747
linters:
4848
- errcheck
49-
- gosec
49+
- gosec
50+
51+
# FIXME temporarily suppress this.
52+
- path: "auth/strategies/(token|bearer)/"
53+
linters:
54+
- dupl

auth/strategies/token/cached.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package token
2+
3+
import (
4+
"context"
5+
"net/http"
6+
7+
"github.com/shaj13/go-guardian/auth"
8+
"github.com/shaj13/go-guardian/errors"
9+
"github.com/shaj13/go-guardian/store"
10+
)
11+
12+
// CachedStrategyKey export identifier for the cached bearer strategy,
13+
// commonly used when enable/add strategy to go-guardian authenticator.
14+
const CachedStrategyKey = auth.StrategyKey("Token.Cached.Strategy")
15+
16+
// AuthenticateFunc declare custom function to authenticate request using token.
17+
// The authenticate function invoked by Authenticate Strategy method when
18+
// The token does not exist in the cahce and the invocation result will be cached, unless an error returned.
19+
// Use NoOpAuthenticate instead to refresh/mangae token directly using cache or Append function.
20+
type AuthenticateFunc func(ctx context.Context, r *http.Request, token string) (auth.Info, error)
21+
22+
// New return new auth.Strategy.
23+
// The returned strategy, caches the invocation result of authenticate function, See AuthenticateFunc.
24+
// Use NoOpAuthenticate to refresh/mangae token directly using cache or Append function, See NoOpAuthenticate.
25+
func New(auth AuthenticateFunc, c store.Cache, opts ...auth.Option) auth.Strategy {
26+
if auth == nil {
27+
panic("Authenticate Function required and can't be nil")
28+
}
29+
30+
if c == nil {
31+
panic("Cache object required and can't be nil")
32+
}
33+
34+
cached := &cachedToken{
35+
authFunc: auth,
36+
cache: c,
37+
typ: Bearer,
38+
parser: AuthorizationParser(string(Bearer)),
39+
}
40+
41+
for _, opt := range opts {
42+
opt.Apply(cached)
43+
}
44+
45+
return cached
46+
}
47+
48+
type cachedToken struct {
49+
parser Parser
50+
typ Type
51+
cache store.Cache
52+
authFunc AuthenticateFunc
53+
}
54+
55+
func (c *cachedToken) Authenticate(ctx context.Context, r *http.Request) (auth.Info, error) {
56+
token, err := c.parser.Token(r)
57+
if err != nil {
58+
return nil, err
59+
}
60+
61+
info, ok, err := c.cache.Load(token, r)
62+
63+
if err != nil {
64+
return nil, err
65+
}
66+
67+
// if token not found invoke user authenticate function
68+
if !ok {
69+
info, err = c.authFunc(ctx, r, token)
70+
if err == nil {
71+
// cache result
72+
err = c.cache.Store(token, info, r)
73+
}
74+
}
75+
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
if _, ok := info.(auth.Info); !ok {
81+
return nil, errors.NewInvalidType((*auth.Info)(nil), info)
82+
}
83+
84+
return info.(auth.Info), nil
85+
}
86+
87+
func (c *cachedToken) Append(token string, info auth.Info, r *http.Request) error {
88+
return c.cache.Store(token, info, r)
89+
}
90+
91+
func (c *cachedToken) Revoke(token string, r *http.Request) error {
92+
return c.cache.Delete(token, r)
93+
}
94+
95+
func (c *cachedToken) Challenge(realm string) string { return challenge(realm, c.typ) }
96+
97+
// NoOpAuthenticate implements Authenticate function, it return nil, auth.ErrNOOP,
98+
// commonly used when token refreshed/mangaed directly using cache or Append function,
99+
// and there is no need to parse token and authenticate request.
100+
func NoOpAuthenticate(ctx context.Context, r *http.Request, token string) (auth.Info, error) {
101+
return nil, auth.ErrNOOP
102+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package token
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
11+
"github.com/shaj13/go-guardian/auth"
12+
"github.com/shaj13/go-guardian/store"
13+
)
14+
15+
func TestNewCahced(t *testing.T) {
16+
table := []struct {
17+
name string
18+
panic bool
19+
expectedErr bool
20+
cache store.Cache
21+
authFunc AuthenticateFunc
22+
info interface{}
23+
token string
24+
}{
25+
{
26+
name: "it return error when cache load return error",
27+
expectedErr: true,
28+
panic: false,
29+
cache: make(mockCache),
30+
token: "error",
31+
authFunc: NoOpAuthenticate,
32+
info: nil,
33+
},
34+
{
35+
name: "it return error when user authenticate func return error",
36+
expectedErr: true,
37+
cache: make(mockCache),
38+
authFunc: NoOpAuthenticate,
39+
panic: false,
40+
info: nil,
41+
},
42+
{
43+
name: "it return error when cache store return error",
44+
expectedErr: true,
45+
cache: make(mockCache),
46+
authFunc: func(_ context.Context, _ *http.Request, _ string) (auth.Info, error) { return nil, nil },
47+
token: "store-error",
48+
panic: false,
49+
info: nil,
50+
},
51+
{
52+
name: "it return error when cache return invalid type",
53+
expectedErr: true,
54+
cache: make(mockCache),
55+
authFunc: func(_ context.Context, _ *http.Request, _ string) (auth.Info, error) { return nil, nil },
56+
panic: false,
57+
info: "sample-data",
58+
token: "valid",
59+
},
60+
{
61+
name: "it return user when token cached",
62+
expectedErr: false,
63+
cache: make(mockCache),
64+
authFunc: NoOpAuthenticate,
65+
panic: false,
66+
info: auth.NewDefaultUser("1", "1", nil, nil),
67+
token: "valid-user",
68+
},
69+
{
70+
name: "it panic when Authenticate func nil",
71+
expectedErr: false,
72+
panic: true,
73+
info: nil,
74+
},
75+
{
76+
name: "it panic when Cache nil",
77+
expectedErr: false,
78+
authFunc: NoOpAuthenticate,
79+
panic: true,
80+
info: nil,
81+
},
82+
}
83+
84+
for _, tt := range table {
85+
t.Run(tt.name, func(t *testing.T) {
86+
if tt.panic {
87+
assert.Panics(t, func() {
88+
New(tt.authFunc, tt.cache)
89+
})
90+
return
91+
}
92+
93+
strategy := New(tt.authFunc, tt.cache)
94+
r, _ := http.NewRequest("GET", "/", nil)
95+
r.Header.Set("Authorization", "Bearer "+tt.token)
96+
_ = tt.cache.Store(tt.token, tt.info, r)
97+
info, err := strategy.Authenticate(r.Context(), r)
98+
if tt.expectedErr {
99+
assert.Error(t, err)
100+
return
101+
}
102+
assert.Equal(t, tt.info, info)
103+
})
104+
}
105+
}
106+
107+
func TestCahcedTokenAppend(t *testing.T) {
108+
cache := make(mockCache)
109+
strategy := &cachedToken{cache: cache}
110+
info := auth.NewDefaultUser("1", "2", nil, nil)
111+
strategy.Append("test-append", info, nil)
112+
cachedInfo, ok, _ := cache.Load("test-append", nil)
113+
assert.True(t, ok)
114+
assert.Equal(t, info, cachedInfo)
115+
}
116+
117+
func TestCahcedTokenChallenge(t *testing.T) {
118+
strategy := &cachedToken{
119+
typ: Bearer,
120+
}
121+
122+
got := strategy.Challenge("Test Realm")
123+
expected := `Bearer realm="Test Realm", title="Bearer Token Based Authentication Scheme"`
124+
125+
assert.Equal(t, expected, got)
126+
}
127+
128+
func BenchmarkCachedToken(b *testing.B) {
129+
r, _ := http.NewRequest("GET", "/", nil)
130+
r.Header.Set("Authorization", "Bearer token")
131+
132+
cache := make(mockCache)
133+
cache.Store("token", auth.NewDefaultUser("benchmark", "1", nil, nil), r)
134+
135+
strategy := New(NoOpAuthenticate, cache)
136+
137+
b.ResetTimer()
138+
b.RunParallel(func(pb *testing.PB) {
139+
for pb.Next() {
140+
_, err := strategy.Authenticate(r.Context(), r)
141+
if err != nil {
142+
b.Error(err)
143+
}
144+
}
145+
})
146+
}
147+
148+
type mockCache map[string]interface{}
149+
150+
func (m mockCache) Load(key string, _ *http.Request) (interface{}, bool, error) {
151+
if key == "error" {
152+
return nil, false, fmt.Errorf("Load Error")
153+
}
154+
v, ok := m[key]
155+
return v, ok, nil
156+
}
157+
158+
func (m mockCache) Store(key string, value interface{}, _ *http.Request) error {
159+
if key == "store-error" {
160+
return fmt.Errorf("Store Error")
161+
}
162+
m[key] = value
163+
return nil
164+
}
165+
func (m mockCache) Keys() []string { return nil }
166+
167+
func (m mockCache) Delete(key string, _ *http.Request) error {
168+
return nil
169+
}

0 commit comments

Comments
 (0)