Skip to content

Commit d4e6508

Browse files
committed
feat: cache result of basic
1 parent a84f360 commit d4e6508

File tree

6 files changed

+231
-0
lines changed

6 files changed

+231
-0
lines changed

auth/info.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ type Info interface {
1111
Groups() []string
1212
// Extensions can contain any additional information.
1313
Extensions() map[string][]string
14+
// SetGroups set the names of the groups the user is a member of.
15+
SetGroups(groups []string)
16+
// SetExtensions to contain additional information.
17+
SetExtensions(exts map[string][]string)
1418
}
1519

1620
// DefaultUser implement Info interface and provides a simple user information.

auth/strategies/basic/basic.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import (
77
"errors"
88
"net/http"
99

10+
"golang.org/x/crypto/bcrypt"
11+
1012
"github.com/shaj13/go-guardian/auth"
13+
gerrors "github.com/shaj13/go-guardian/errors"
14+
"github.com/shaj13/go-guardian/store"
1115
)
1216

1317
// ErrMissingPrams is returned by Authenticate Strategy method,
@@ -18,6 +22,10 @@ var ErrMissingPrams = errors.New("basic: Request missing BasicAuth")
1822
// commonly used when enable/add strategy to go-passport authenticator.
1923
const StrategyKey = auth.StrategyKey("Basic.Strategy")
2024

25+
// ExtensionKey represents a key for the hashed password in info extensions.
26+
// Typically used when basic strategy cache the authentication decisions.
27+
const ExtensionKey = "x-go-guardian-basic-hash"
28+
2129
// Authenticate declare custom function to authenticate request using user credentials.
2230
// the authenticate function invoked by Authenticate Strategy method after extracting user credentials
2331
// to compare against DB or ather service, if extracting user credentials from request failed a nil info
@@ -44,3 +52,72 @@ func (auth Authenticate) credentials(r *http.Request) (string, string, error) {
4452

4553
return user, pass, nil
4654
}
55+
56+
type cachedBasic struct {
57+
cache store.Cache
58+
authFunc Authenticate
59+
}
60+
61+
func (c *cachedBasic) authenticate(ctx context.Context, r *http.Request, userName, password string) (auth.Info, error) { // nolint:lll
62+
v, ok, err := c.cache.Load(userName, r)
63+
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
// if info not found invoke user authenticate function
69+
if !ok {
70+
return c.authenticatAndHash(ctx, r, userName, password)
71+
}
72+
73+
if _, ok := v.(auth.Info); !ok {
74+
return nil, gerrors.NewInvalidType((*auth.Info)(nil), v)
75+
}
76+
77+
info := v.(auth.Info)
78+
ext := info.Extensions()
79+
hash, ok := ext[ExtensionKey]
80+
81+
if !ok {
82+
return c.authenticatAndHash(ctx, r, userName, password)
83+
}
84+
85+
err = bcrypt.CompareHashAndPassword([]byte(hash[0]), []byte(password))
86+
return info, err
87+
}
88+
89+
func (c *cachedBasic) authenticatAndHash(ctx context.Context, r *http.Request, userName, password string) (auth.Info, error) { //nolint:lll
90+
info, err := c.authFunc(ctx, r, userName, password)
91+
if err != nil {
92+
return nil, err
93+
}
94+
95+
ext := info.Extensions()
96+
if ext == nil {
97+
ext = make(map[string][]string)
98+
}
99+
100+
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
101+
102+
// if failed to hash password silently and return user info
103+
if err != nil {
104+
return info, nil
105+
}
106+
107+
ext[ExtensionKey] = []string{string(hash)}
108+
info.SetExtensions(ext)
109+
110+
// cache result
111+
return info, c.cache.Store(userName, info, r)
112+
}
113+
114+
// New return new auth.Strategy.
115+
// The returned strategy, caches the invocation result of authenticate function.
116+
func New(auth Authenticate, cache store.Cache) auth.Strategy {
117+
cb := &cachedBasic{
118+
authFunc: auth,
119+
cache: cache,
120+
}
121+
122+
return Authenticate(cb.authenticate)
123+
}

auth/strategies/basic/basic_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,114 @@ func Test(t *testing.T) {
5959
})
6060
}
6161
}
62+
63+
//nolint:goconst
64+
func TestNewCached(t *testing.T) {
65+
authFunc := func(ctx context.Context, r *http.Request, userName, password string) (auth.Info, error) {
66+
if userName == "auth-error" {
67+
return nil, fmt.Errorf("Invalid credentials")
68+
}
69+
70+
return auth.NewDefaultUser("test", "10", nil, nil), nil
71+
}
72+
73+
table := []struct {
74+
name string
75+
setCredentials func(r *http.Request)
76+
expectedErr bool
77+
}{
78+
{
79+
name: "it return user from cache",
80+
setCredentials: func(r *http.Request) { r.SetBasicAuth("predefined2", "test") },
81+
expectedErr: false,
82+
},
83+
{
84+
name: "it re-authenticate user when hash missing",
85+
setCredentials: func(r *http.Request) { r.SetBasicAuth("predefined3", "test") },
86+
expectedErr: false,
87+
},
88+
{
89+
name: "it return error when cache hold invalid user info",
90+
setCredentials: func(r *http.Request) { r.SetBasicAuth("predefined", "test") },
91+
expectedErr: true,
92+
},
93+
{
94+
name: "it return error when cache return error on load",
95+
setCredentials: func(r *http.Request) { r.SetBasicAuth("error", "test") },
96+
expectedErr: true,
97+
},
98+
{
99+
name: "it return error when cache return error on store",
100+
setCredentials: func(r *http.Request) { r.SetBasicAuth("store-error", "test") },
101+
expectedErr: true,
102+
},
103+
{
104+
name: "it return user info when request authenticated",
105+
setCredentials: func(r *http.Request) { r.SetBasicAuth("test", "test") },
106+
expectedErr: false,
107+
},
108+
{
109+
name: "it return error when request has invalid credentials",
110+
setCredentials: func(r *http.Request) { r.SetBasicAuth("auth-error", "unknown") },
111+
expectedErr: true,
112+
},
113+
{
114+
name: "it return error when request missing basic auth params",
115+
setCredentials: func(r *http.Request) { /* no op */ },
116+
expectedErr: true,
117+
},
118+
}
119+
120+
for _, tt := range table {
121+
t.Run(tt.name, func(t *testing.T) {
122+
r, _ := http.NewRequest("GET", "/", nil)
123+
tt.setCredentials(r)
124+
125+
cache := make(mockCache)
126+
cache["predefined"] = "invalid-type"
127+
cache["predefined2"] = auth.NewDefaultUser(
128+
"predefined2",
129+
"10",
130+
nil,
131+
map[string][]string{
132+
ExtensionKey: {"$2a$10$aj7RBUkAjknXMyqeLW0v3.FF0aarP4/MraQD7bsmvQ6YSQzxCyyKG"},
133+
},
134+
)
135+
cache["predefined3"] = auth.NewDefaultUser("predefined3", "10", nil, nil)
136+
137+
info, err := New(authFunc, cache).Authenticate(r.Context(), r)
138+
139+
if tt.expectedErr && err == nil {
140+
t.Errorf("%s: Expected error, got none", tt.name)
141+
return
142+
}
143+
144+
if !tt.expectedErr && info == nil {
145+
t.Errorf("%s: Expected info object, got nil: %v", tt.name, err)
146+
return
147+
}
148+
})
149+
}
150+
}
151+
152+
type mockCache map[string]interface{}
153+
154+
func (m mockCache) Load(key string, _ *http.Request) (interface{}, bool, error) {
155+
if key == "error" {
156+
return nil, false, fmt.Errorf("Load Error")
157+
}
158+
v, ok := m[key]
159+
return v, ok, nil
160+
}
161+
162+
func (m mockCache) Store(key string, value interface{}, _ *http.Request) error {
163+
if key == "store-error" {
164+
return fmt.Errorf("Store Error")
165+
}
166+
m[key] = value
167+
return nil
168+
}
169+
170+
func (m mockCache) Delete(key string, _ *http.Request) error {
171+
return nil
172+
}

auth/strategies/basic/example_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ import (
44
"context"
55
"fmt"
66
"net/http"
7+
"sync"
8+
9+
"github.com/golang/groupcache/lru"
710

811
"github.com/shaj13/go-guardian/auth"
912
"github.com/shaj13/go-guardian/errors"
13+
"github.com/shaj13/go-guardian/store"
1014
)
1115

1216
func Example() {
@@ -29,6 +33,33 @@ func Example() {
2933
// Invalid credentials
3034
}
3135

36+
func Example_second() {
37+
// This example show how to caches the result of basic auth.
38+
// With LRU cache
39+
cache := &store.LRU{
40+
Cache: lru.New(2),
41+
MU: &sync.Mutex{},
42+
}
43+
44+
strategy := New(exampleAuthFunc, cache)
45+
authenticator := auth.New()
46+
authenticator.EnableStrategy(StrategyKey, strategy)
47+
48+
// user request
49+
req, _ := http.NewRequest("GET", "/", nil)
50+
req.SetBasicAuth("test", "test")
51+
user, err := authenticator.Authenticate(req)
52+
fmt.Println(user.ID(), err)
53+
54+
req.SetBasicAuth("test", "1234")
55+
_, err = authenticator.Authenticate(req)
56+
fmt.Println(err.(errors.MultiError)[1])
57+
58+
// Output:
59+
// 10 <nil>
60+
// crypto/bcrypt: hashedPassword is not the hash of the given password
61+
}
62+
3263
func exampleAuthFunc(ctx context.Context, r *http.Request, userName, password string) (auth.Info, error) {
3364
// here connect to db or any other service to fetch user and validate it.
3465
if userName == "test" && password == "test" {

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ require (
66
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e
77
github.com/gorilla/sessions v1.2.0
88
github.com/stretchr/testify v1.5.1
9+
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
910
)

go.sum

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
1212
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
1313
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
1414
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
15+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
16+
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
17+
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
18+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
19+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
20+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
21+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
1522
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1623
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
1724
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=

0 commit comments

Comments
 (0)