Skip to content

feat: support storing hashed client secrets #284

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ require (
github.com/yudai/gojsondiff v1.0.0 // indirect
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
github.com/yudai/pp v2.0.1+incompatible // indirect
golang.org/x/crypto v0.0.0-20220214200702-86341886e292
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
google.golang.org/appengine v1.6.6 // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDf
github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down
14 changes: 12 additions & 2 deletions store.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
package oauth2

import "context"
import (
"context"
)

type (
// ClientStore the client information storage interface
ClientStore interface {
// according to the ID for the client information
// get client information by ID
GetByID(ctx context.Context, id string) (ClientInfo, error)
}

// SavingClientStore can save client information and retrieve it by ID
SavingClientStore interface {
// get client information by ID
GetByID(ctx context.Context, id string) (ClientInfo, error)
// store client information
Save(ctx context.Context, info ClientInfo) error
}

// TokenStore the token information storage interface
Expand Down
5 changes: 5 additions & 0 deletions store/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,8 @@ func (cs *ClientStore) Set(id string, cli oauth2.ClientInfo) (err error) {
cs.data[id] = cli
return
}

// Save stores client information, implements the oauth2.SavingClientStore interface
func (cs *ClientStore) Save(_ context.Context, cli oauth2.ClientInfo) (err error) {
return cs.Set(cli.GetID(), cli)
}
152 changes: 152 additions & 0 deletions store/hash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package store

import (
"context"

"github.com/go-oauth2/oauth2/v4"
"github.com/go-oauth2/oauth2/v4/errors"
"github.com/go-oauth2/oauth2/v4/models"
"golang.org/x/crypto/bcrypt"
)

// Hasher is an interface for hashing and verifying client secrets.
type Hasher interface {
// Hash hashes the given secret and returns the hashed value.
Hash(secret string) (string, error)
// Verify checks if the hashed secret matches the given secret.
Verify(hashedPassword, secret string) error
}

// BcryptHasher is a Hasher implementation using bcrypt for hashing and verifying secrets.
type BcryptHasher struct{}

func (b *BcryptHasher) Hash(secret string) (string, error) {
hashed, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hashed), nil
}

func (b *BcryptHasher) Verify(hashed, secret string) error {
return bcrypt.CompareHashAndPassword([]byte(hashed), []byte(secret))
}

// ClientInfoWithHash wraps an oauth2.ClientInfo and provides secret verification using a Hasher.
type ClientInfoWithHash struct {
wrapped oauth2.ClientInfo
hasher Hasher
}

// NewClientInfoWithHash creates a new instance of client info supporting hashed secret verification.
func NewClientInfoWithHash(
info oauth2.ClientInfo,
hasher Hasher,
) *ClientInfoWithHash {
if info == nil {
return nil
}
return &ClientInfoWithHash{
wrapped: info,
hasher: hasher,
}
}

// VerifyPassword verifies the given plain secret against the hashed secret.
// It implements the oauth2.ClientPasswordVerifier interface.
func (v *ClientInfoWithHash) VerifyPassword(secret string) bool {
if secret == "" {
return false
}
err := v.hasher.Verify(v.GetSecret(), secret)
return err == nil
}

// GetID returns the client ID.
func (v *ClientInfoWithHash) GetID() string {
return v.wrapped.GetID()
}

// GetSecret returns the hashed client secret.
func (v *ClientInfoWithHash) GetSecret() string {
return v.wrapped.GetSecret()
}

// GetDomain returns the client domain.
func (v *ClientInfoWithHash) GetDomain() string {
return v.wrapped.GetDomain()
}

// GetUserID returns the user ID associated with the client.
func (v *ClientInfoWithHash) GetUserID() string {
return v.wrapped.GetUserID()
}

// IsPublic returns true if the client is public.
func (v *ClientInfoWithHash) IsPublic() bool {
return v.wrapped.IsPublic()
}

// ClientStoreWithHash is a wrapper around oauth2.SavingClientStore that hashes client secrets.
type ClientStoreWithHash struct {
underlying oauth2.SavingClientStore
hasher Hasher
}

// NewClientStoreWithBcrypt creates a new ClientStoreWithHash using bcrypt for hashing.
//
// It is a convenience function for creating a store with the default bcrypt hasher.
// The store will hash client secrets using bcrypt before saving them and would
// return secret information supporting secret verification against the hashed secret.
func NewClientStoreWithBcrypt(store oauth2.SavingClientStore) *ClientStoreWithHash {
return NewClientStoreWithHash(store, &BcryptHasher{})
}

func NewClientStoreWithHash(underlying oauth2.SavingClientStore, hasher Hasher) *ClientStoreWithHash {
if hasher == nil {
hasher = &BcryptHasher{}
}
return &ClientStoreWithHash{
underlying: underlying,
hasher: hasher,
}
}

// GetByID retrieves client information by ID and returns a ClientInfoWithHash instance.
func (w *ClientStoreWithHash) GetByID(ctx context.Context, id string) (oauth2.ClientInfo, error) {
info, err := w.underlying.GetByID(ctx, id)
if err != nil {
return nil, err
}
rval := NewClientInfoWithHash(info, w.hasher)
if rval == nil {
return nil, errors.ErrInvalidClient
}
return rval, nil
}

// Save hashes the client secret before saving it to the underlying store.
func (w *ClientStoreWithHash) Save(
ctx context.Context,
info oauth2.ClientInfo,
) error {
if info == nil {
return errors.ErrInvalidClient
}
if info.GetSecret() == "" {
return errors.ErrInvalidClient
}

hashed, err := w.hasher.Hash(info.GetSecret())
if err != nil {
return err
}
hashedInfo := models.Client{
ID: info.GetID(),
Secret: hashed,
Domain: info.GetDomain(),
UserID: info.GetUserID(),
Public: info.IsPublic(),
}
return w.underlying.Save(ctx, &hashedInfo)
}
54 changes: 54 additions & 0 deletions store/hash_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package store_test

import (
"context"
"testing"

"github.com/go-oauth2/oauth2/v4"
"github.com/go-oauth2/oauth2/v4/models"
"github.com/go-oauth2/oauth2/v4/store"
. "github.com/smartystreets/goconvey/convey"
)

func TestClientStoreWithHash(t *testing.T) {
Convey("Test client store with hash - save", t, func() {
hasher := &store.BcryptHasher{}
memory := store.NewClientStore()
store := store.NewClientStoreWithHash(memory, hasher)
secret := "123456"
err := store.Save(context.Background(), &models.Client{
ID: "123",
Secret: secret,
Domain: "http://localhost",
Public: false,
UserID: "123",
})
So(err, ShouldBeNil)

Convey("get by id", func() {
storedClient, err := store.GetByID(context.Background(), "123")

So(err, ShouldBeNil)
So(storedClient.GetID(), ShouldEqual, "123")
So(storedClient.GetSecret(), ShouldNotEqual, secret)

verifier := storedClient.(oauth2.ClientPasswordVerifier)

Convey("verify correct password - success", func() {
So(verifier.VerifyPassword(secret), ShouldBeTrue)
})

Convey("verify incorrect password - fail", func() {
So(verifier.VerifyPassword("wrong"), ShouldBeFalse)
})
})
})
}

// check interfaces

var _ = (oauth2.ClientStore)((*store.ClientStoreWithHash)(nil))
var _ = (oauth2.SavingClientStore)((*store.ClientStoreWithHash)(nil))

var _ = (oauth2.ClientInfo)((*store.ClientInfoWithHash)(nil))
var _ = (oauth2.ClientPasswordVerifier)((*store.ClientInfoWithHash)(nil))