Skip to content

Commit 81ed7eb

Browse files
committed
feat: support storing hashed client secrets
Adds a support to automatically hash client secrets before storing provided that the underlying store implements new SavingClientStore interface. Client store wrapper supporting hashing returns client info wrapped into structure that implements ClientPasswordVerifier using provided hashing algo for verifying. Hasher interface is added to allow any hashing algorythm to be used instead of default bcrypt.
1 parent bb6e415 commit 81ed7eb

File tree

6 files changed

+225
-2
lines changed

6 files changed

+225
-2
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ require (
2727
github.com/yudai/gojsondiff v1.0.0 // indirect
2828
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
2929
github.com/yudai/pp v2.0.1+incompatible // indirect
30+
golang.org/x/crypto v0.0.0-20220214200702-86341886e292
3031
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
3132
google.golang.org/appengine v1.6.6 // indirect
3233
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDf
126126
github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=
127127
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
128128
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
129+
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
129130
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
130131
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
131132
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

store.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
package oauth2
22

3-
import "context"
3+
import (
4+
"context"
5+
)
46

57
type (
68
// ClientStore the client information storage interface
79
ClientStore interface {
8-
// according to the ID for the client information
10+
// get client information by ID
11+
GetByID(ctx context.Context, id string) (ClientInfo, error)
12+
}
13+
14+
// SavingClientStore can save client information and retrieve it by ID
15+
SavingClientStore interface {
16+
// get client information by ID
917
GetByID(ctx context.Context, id string) (ClientInfo, error)
18+
// store client information
19+
Save(ctx context.Context, info ClientInfo) error
1020
}
1121

1222
// TokenStore the token information storage interface

store/client.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,8 @@ func (cs *ClientStore) Set(id string, cli oauth2.ClientInfo) (err error) {
4040
cs.data[id] = cli
4141
return
4242
}
43+
44+
// Save stores client information, implements the oauth2.SavingClientStore interface
45+
func (cs *ClientStore) Save(_ context.Context, cli oauth2.ClientInfo) (err error) {
46+
return cs.Set(cli.GetID(), cli)
47+
}

store/hash.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package store
2+
3+
import (
4+
"context"
5+
6+
"github.com/go-oauth2/oauth2/v4"
7+
"github.com/go-oauth2/oauth2/v4/errors"
8+
"github.com/go-oauth2/oauth2/v4/models"
9+
"golang.org/x/crypto/bcrypt"
10+
)
11+
12+
// Hasher is an interface for hashing and verifying client secrets.
13+
type Hasher interface {
14+
// Hash hashes the given secret and returns the hashed value.
15+
Hash(secret string) (string, error)
16+
// Verify checks if the hashed secret matches the given secret.
17+
Verify(hashedPassword, secret string) error
18+
}
19+
20+
// BcryptHasher is a Hasher implementation using bcrypt for hashing and verifying secrets.
21+
type BcryptHasher struct{}
22+
23+
func (b *BcryptHasher) Hash(secret string) (string, error) {
24+
hashed, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost)
25+
if err != nil {
26+
return "", err
27+
}
28+
return string(hashed), nil
29+
}
30+
31+
func (b *BcryptHasher) Verify(hashed, secret string) error {
32+
return bcrypt.CompareHashAndPassword([]byte(hashed), []byte(secret))
33+
}
34+
35+
// ClientInfoWithHash wraps an oauth2.ClientInfo and provides secret verification using a Hasher.
36+
type ClientInfoWithHash struct {
37+
wrapped oauth2.ClientInfo
38+
hasher Hasher
39+
}
40+
41+
// NewClientInfoWithHash creates a new instance of client info supporting hashed secret verification.
42+
func NewClientInfoWithHash(
43+
info oauth2.ClientInfo,
44+
hasher Hasher,
45+
) *ClientInfoWithHash {
46+
if info == nil {
47+
return nil
48+
}
49+
return &ClientInfoWithHash{
50+
wrapped: info,
51+
hasher: hasher,
52+
}
53+
}
54+
55+
// VerifyPassword verifies the given plain secret against the hashed secret.
56+
// It implements the oauth2.ClientPasswordVerifier interface.
57+
func (v *ClientInfoWithHash) VerifyPassword(secret string) bool {
58+
if secret == "" {
59+
return false
60+
}
61+
err := v.hasher.Verify(v.GetSecret(), secret)
62+
return err == nil
63+
}
64+
65+
// GetID returns the client ID.
66+
func (v *ClientInfoWithHash) GetID() string {
67+
return v.wrapped.GetID()
68+
}
69+
70+
// GetSecret returns the hashed client secret.
71+
func (v *ClientInfoWithHash) GetSecret() string {
72+
return v.wrapped.GetSecret()
73+
}
74+
75+
// GetDomain returns the client domain.
76+
func (v *ClientInfoWithHash) GetDomain() string {
77+
return v.wrapped.GetDomain()
78+
}
79+
80+
// GetUserID returns the user ID associated with the client.
81+
func (v *ClientInfoWithHash) GetUserID() string {
82+
return v.wrapped.GetUserID()
83+
}
84+
85+
// IsPublic returns true if the client is public.
86+
func (v *ClientInfoWithHash) IsPublic() bool {
87+
return v.wrapped.IsPublic()
88+
}
89+
90+
// ClientStoreWithHash is a wrapper around oauth2.SavingClientStore that hashes client secrets.
91+
type ClientStoreWithHash struct {
92+
underlying oauth2.SavingClientStore
93+
hasher Hasher
94+
}
95+
96+
// NewClientStoreWithBcrypt creates a new ClientStoreWithHash using bcrypt for hashing.
97+
//
98+
// It is a convenience function for creating a store with the default bcrypt hasher.
99+
// The store will hash client secrets using bcrypt before saving them and would
100+
// return secret information supporting secret verification against the hashed secret.
101+
func NewClientStoreWithBcrypt(store oauth2.SavingClientStore) *ClientStoreWithHash {
102+
return NewClientStoreWithHash(store, &BcryptHasher{})
103+
}
104+
105+
func NewClientStoreWithHash(underlying oauth2.SavingClientStore, hasher Hasher) *ClientStoreWithHash {
106+
if hasher == nil {
107+
hasher = &BcryptHasher{}
108+
}
109+
return &ClientStoreWithHash{
110+
underlying: underlying,
111+
hasher: hasher,
112+
}
113+
}
114+
115+
// GetByID retrieves client information by ID and returns a ClientInfoWithHash instance.
116+
func (w *ClientStoreWithHash) GetByID(ctx context.Context, id string) (oauth2.ClientInfo, error) {
117+
info, err := w.underlying.GetByID(ctx, id)
118+
if err != nil {
119+
return nil, err
120+
}
121+
rval := NewClientInfoWithHash(info, w.hasher)
122+
if rval == nil {
123+
return nil, errors.ErrInvalidClient
124+
}
125+
return rval, nil
126+
}
127+
128+
// Save hashes the client secret before saving it to the underlying store.
129+
func (w *ClientStoreWithHash) Save(
130+
ctx context.Context,
131+
info oauth2.ClientInfo,
132+
) error {
133+
if info == nil {
134+
return errors.ErrInvalidClient
135+
}
136+
if info.GetSecret() == "" {
137+
return errors.ErrInvalidClient
138+
}
139+
140+
hashed, err := w.hasher.Hash(info.GetSecret())
141+
if err != nil {
142+
return err
143+
}
144+
hashedInfo := models.Client{
145+
ID: info.GetID(),
146+
Secret: hashed,
147+
Domain: info.GetDomain(),
148+
UserID: info.GetUserID(),
149+
Public: info.IsPublic(),
150+
}
151+
return w.underlying.Save(ctx, &hashedInfo)
152+
}

store/hash_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package store_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/go-oauth2/oauth2/v4"
8+
"github.com/go-oauth2/oauth2/v4/models"
9+
"github.com/go-oauth2/oauth2/v4/store"
10+
. "github.com/smartystreets/goconvey/convey"
11+
)
12+
13+
func TestClientStoreWithHash(t *testing.T) {
14+
Convey("Test client store with hash - save", t, func() {
15+
hasher := &store.BcryptHasher{}
16+
memory := store.NewClientStore()
17+
store := store.NewClientStoreWithHash(memory, hasher)
18+
secret := "123456"
19+
err := store.Save(context.Background(), &models.Client{
20+
ID: "123",
21+
Secret: secret,
22+
Domain: "http://localhost",
23+
Public: false,
24+
UserID: "123",
25+
})
26+
So(err, ShouldBeNil)
27+
28+
Convey("get by id", func() {
29+
storedClient, err := store.GetByID(context.Background(), "123")
30+
31+
So(err, ShouldBeNil)
32+
So(storedClient.GetID(), ShouldEqual, "123")
33+
So(storedClient.GetSecret(), ShouldNotEqual, secret)
34+
35+
verifier := storedClient.(oauth2.ClientPasswordVerifier)
36+
37+
Convey("verify correct password - success", func() {
38+
So(verifier.VerifyPassword(secret), ShouldBeTrue)
39+
})
40+
41+
Convey("verify incorrect password - fail", func() {
42+
So(verifier.VerifyPassword("wrong"), ShouldBeFalse)
43+
})
44+
})
45+
})
46+
}
47+
48+
// check interfaces
49+
50+
var _ = (oauth2.ClientStore)((*store.ClientStoreWithHash)(nil))
51+
var _ = (oauth2.SavingClientStore)((*store.ClientStoreWithHash)(nil))
52+
53+
var _ = (oauth2.ClientInfo)((*store.ClientInfoWithHash)(nil))
54+
var _ = (oauth2.ClientPasswordVerifier)((*store.ClientInfoWithHash)(nil))

0 commit comments

Comments
 (0)