-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #14 from plainq:auth_basics
Auth Basics
- Loading branch information
Showing
10 changed files
with
261 additions
and
17 deletions.
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,74 @@ | ||
package hashkit | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
|
||
"github.com/plainq/servekit/errkit" | ||
"golang.org/x/crypto/bcrypt" | ||
) | ||
|
||
// Hasher holds logic of hashing and checking the password. | ||
type Hasher interface { | ||
// HashPassword takes password and return a hasher from it. | ||
HashPassword(pass string) (string, error) | ||
// CheckPassword takes password and perform comparison with | ||
// stored hashed password. | ||
CheckPassword(hash, pass string) error | ||
} | ||
|
||
// NewBCryptHasher returns a pointer to a new instance of BCryptHasher type. | ||
func NewBCryptHasher(opts ...Option) *BCryptHasher { | ||
h := BCryptHasher{cost: bcrypt.DefaultCost} | ||
for _, opt := range opts { | ||
opt(&h) | ||
} | ||
return &h | ||
} | ||
|
||
type Option func(hasher *BCryptHasher) | ||
|
||
// WithCost takes cost argument of type int and set the | ||
// given value to 'BCryptHasher.cost' field. | ||
// If provided cost exceed out of acceptable boundary | ||
// then min or max cost wil be set. | ||
func WithCost(cost int) Option { | ||
var finalCost int | ||
|
||
if cost < bcrypt.MinCost { | ||
finalCost = bcrypt.MinCost | ||
} | ||
|
||
if cost > bcrypt.MaxCost { | ||
finalCost = bcrypt.MaxCost | ||
} | ||
|
||
return func(h *BCryptHasher) { | ||
h.cost = finalCost | ||
} | ||
} | ||
|
||
// BCryptHasher implements Hasher interface. | ||
// Hashes passwords using bcrypt algorithm. | ||
type BCryptHasher struct { cost int } | ||
|
||
func (h *BCryptHasher) CheckPassword(hash, pass string) error { | ||
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pass)); err != nil { | ||
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { | ||
return errkit.ErrPasswordIncorrect | ||
} | ||
|
||
return fmt.Errorf("password checking error: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (h *BCryptHasher) HashPassword(pass string) (string, error) { | ||
hash, err := bcrypt.GenerateFromPassword([]byte(pass), h.cost) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to generate hash: %w", err) | ||
} | ||
|
||
return string(hash), nil | ||
} |
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,28 @@ | ||
package hashkit | ||
|
||
import ( | ||
"reflect" | ||
"testing" | ||
) | ||
|
||
func TestNewBCryptHasher(t *testing.T) { | ||
type tcase struct { | ||
cost int | ||
want BCryptHasher | ||
} | ||
|
||
tests := map[string]tcase{ | ||
"cost1": {1, BCryptHasher{cost: 4}}, | ||
"cost4": {4, BCryptHasher{cost: 4}}, | ||
"cost31": {31, BCryptHasher{cost: 31}}, | ||
"cost32": {32, BCryptHasher{cost: 31}}, | ||
} | ||
|
||
for name, tc := range tests { | ||
t.Run(name, func(t *testing.T) { | ||
if got := NewBCryptHasher(WithCost(tc.cost)); !reflect.DeepEqual(*got, tc.want) { | ||
t.Errorf("NewBCryptHasher() = %v, want %v", *got, tc.want) | ||
} | ||
}) | ||
} | ||
} |
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,101 @@ | ||
package jwtkit | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/cristalhq/jwt/v5" | ||
"github.com/plainq/servekit/errkit" | ||
) | ||
|
||
// TokenManager is an interface that holds the logic of token management. | ||
type TokenManager interface { | ||
// Sign takes a Token and signs it. | ||
Sign(token *Token) (string, error) | ||
|
||
// Verify takes a token string and verifies it. | ||
Verify(token string) error | ||
|
||
// ParseVerify takes a token string and parses and verifies it. | ||
ParseVerify(token string) (*Token, error) | ||
} | ||
|
||
// Token is a struct that holds the token and its claims. | ||
type Token struct { | ||
jwt.RegisteredClaims | ||
Meta map[string]any `json:"meta,omitempty"` | ||
|
||
raw *jwt.Token | ||
} | ||
|
||
// Raw returns the raw token. | ||
func (t *Token) Raw() *jwt.Token { return t.raw } | ||
|
||
// Metadata returns the metadata of the token. | ||
func (t *Token) Metadata() map[string]any { return t.Meta } | ||
|
||
// NewTokenManager creates a new implementation of TokenManager based on JWT. | ||
// It uses the given signer and verifier to sign and verify the token. | ||
func NewTokenManager(signer jwt.Signer, verifier jwt.Verifier) *TokenManagerJWT { | ||
builder := jwt.NewBuilder(signer, jwt.WithContentType("jwt")) | ||
|
||
tm := TokenManagerJWT{ | ||
builder: builder, | ||
verifier: verifier, | ||
} | ||
|
||
return &tm | ||
} | ||
|
||
// TokenManagerJWT is an implementation of TokenManager based on JWT. | ||
type TokenManagerJWT struct { | ||
builder *jwt.Builder | ||
verifier jwt.Verifier | ||
} | ||
|
||
func (m *TokenManagerJWT) Sign(token *Token) (string, error) { | ||
t, err := m.builder.Build(token) | ||
if err != nil { | ||
return "", fmt.Errorf("sign token: %w", err) | ||
} | ||
|
||
return t.String(), nil | ||
} | ||
|
||
func (m *TokenManagerJWT) Verify(token string) error { | ||
if _, err := m.ParseVerify(token); err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (m *TokenManagerJWT) ParseVerify(token string) (*Token, error) { | ||
raw, err := jwt.Parse([]byte(token), m.verifier) | ||
if err != nil { | ||
return nil, errors.Join(errkit.ErrTokenInvalid, fmt.Errorf("parse token: %w", err)) | ||
} | ||
|
||
t := Token{ | ||
raw: raw, | ||
} | ||
|
||
if err := raw.DecodeClaims(&t); err != nil { | ||
return nil, errors.Join(errkit.ErrTokenInvalid, fmt.Errorf("decode claims: %w", err)) | ||
} | ||
|
||
if !t.IsValidExpiresAt(time.Now()) { | ||
return nil, errors.Join(errkit.ErrTokenExpired, fmt.Errorf("token is expired")) | ||
} | ||
|
||
if !t.IsValidNotBefore(time.Now()) { | ||
return nil, errors.Join(errkit.ErrTokenNotBefore, fmt.Errorf("token is not valid yet")) | ||
} | ||
|
||
if !t.IsValidIssuedAt(time.Now()) { | ||
return nil, errors.Join(errkit.ErrTokenIssuedAt, fmt.Errorf("token is not valid at the current time")) | ||
} | ||
|
||
return &t, nil | ||
} |
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,9 @@ | ||
package jwtkit | ||
|
||
import ( | ||
"testing" | ||
) | ||
|
||
func TestTokenManager(t *testing.T) { | ||
|
||
} |
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
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
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