Skip to content

Commit

Permalink
Merge pull request #14 from plainq:auth_basics
Browse files Browse the repository at this point in the history
Auth Basics
  • Loading branch information
heartwilltell authored Feb 4, 2025
2 parents c6630d4 + 09c3f36 commit 9051b2d
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 17 deletions.
10 changes: 7 additions & 3 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ linters:
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()).
- errcheck # Checks for unchecked errors in go programs.
- errorlint # Finds code that will cause problems with the error wrapping scheme introduced in Go 1.13.
- exportloopref # Checks for pointers to enclosing loop variables.
- copyloopvar # Checks for loop variables that are copied in the loop body.
- gosimple # Linter for Go source code that specializes in simplifying code.
- gosec # Inspects source code for security problems.
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string.
Expand Down Expand Up @@ -118,6 +118,10 @@ linters-settings:
# Default: false
checkExported: false

gosec:
excludes:
- "G404" # Use of weak random number generator (math/rand instead of crypto/rand)

revive:
# Maximum number of open files at the same time.
# See https://github.com/mgechev/revive#command-line-flags
Expand Down Expand Up @@ -422,8 +426,8 @@ linters-settings:
disabled: false

# Warns when importing black-listed packages.
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#imports-blacklist
- name: imports-blacklist
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#imports-blocklist
- name: imports-blocklist
severity: warning
disabled: false
arguments:
Expand Down
74 changes: 74 additions & 0 deletions authkit/hashkit/hasher.go
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
}
28 changes: 28 additions & 0 deletions authkit/hashkit/hasher_test.go
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)
}
})
}
}
101 changes: 101 additions & 0 deletions authkit/jwtkit/jwt.go
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
}
9 changes: 9 additions & 0 deletions authkit/jwtkit/jwt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package jwtkit

import (
"testing"
)

func TestTokenManager(t *testing.T) {

}
15 changes: 15 additions & 0 deletions errkit/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@ const (

// ErrConnFailed shows that connection to a resource failed.
ErrConnFailed Error = "connection failed"

// ErrPasswordIncorrect indicates that the password is incorrect.
ErrPasswordIncorrect Error = "password incorrect"

// ErrTokenInvalid error means that given token is invalid or malformed.
ErrTokenInvalid Error = "invalid token"

// ErrTokenExpired error means that given token is already expired.
ErrTokenExpired Error = "expired token"

// ErrTokenNotBefore error means that given token is not valid yet.
ErrTokenNotBefore Error = "token is not valid yet"

// ErrTokenIssuedAt error means that given token is not valid at the current time.
ErrTokenIssuedAt Error = "token is not valid at the current time"
)

// Error type represents package level errors.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ require (
github.com/aws/aws-sdk-go v1.55.6 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cristalhq/jwt/v5 v5.4.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/golang/snappy v0.0.4 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cristalhq/jwt/v5 v5.4.0 h1:Wxi1TocFHaijyV608j7v7B9mPc4ZNjvWT3LKBO0d4QI=
github.com/cristalhq/jwt/v5 v5.4.0/go.mod h1:+b/BzaCWEpFDmXxspJ5h4SdJ1N/45KMjKOetWzmHvDA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down
34 changes: 22 additions & 12 deletions httpkit/http_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,29 +92,38 @@ type Config struct {
// TCP keep-alives.
disableKeepAlives bool

// tlsHandshakeTimeout
// tlsHandshakeTimeout controls the maximum amount of time to wait for a
// TLS handshake to complete.
tlsHandshakeTimeout time.Duration

// maxIdleConns controls the maximum number of idle (keep-alive)
// connections across all hosts. Zero means no limit.
maxIdleConns int

// maxIdleConnsPerHost controls
// maxIdleConnsPerHost controls the maximum number of idle (keep-alive)
// connections to keep per-host. Zero means no limit.
maxIdleConnsPerHost int

// idleConnTimeout
// idleConnTimeout controls the maximum amount of time an idle
// (keep-alive) connection will remain idle before closing
// itself.
idleConnTimeout time.Duration

// expectContinueTimeout
// expectContinueTimeout controls the amount of time to wait for a server's
// first response headers after fully writing the request headers if the
// request has an Expect header.
expectContinueTimeout time.Duration

// responseHeaderTimeout
// responseHeaderTimeout controls the amount of time to wait for a server's
// response headers after fully writing the request headers.
responseHeaderTimeout time.Duration

// writeBufferSize
// writeBufferSize controls the size of the write buffer for the underlying
// http.Transport.
writeBufferSize int

// readBufferSize
// readBufferSize controls the size of the read buffer for the underlying
// http.Transport.
readBufferSize int
}

Expand Down Expand Up @@ -193,7 +202,8 @@ func WithDialTimeout(timeout time.Duration) ClientOption {
return option
}

// WithKeepAliveDisabled sets the
// WithKeepAliveDisabled sets the DisableKeepAlives value to
// underlying http.Transport of http.Client.
func WithKeepAliveDisabled(disabled bool) ClientOption {
option := func(config *Config) {
config.disableKeepAlives = disabled
Expand All @@ -214,19 +224,19 @@ func WithKeepAliveTimeout(timeout time.Duration) ClientOption {

// WithMaxIdleConns sets the MaxIdleConns value to
// underlying http.Transport of http.Client.
func WithMaxIdleConns(max int) ClientOption {
func WithMaxIdleConns(maxn int) ClientOption {
option := func(config *Config) {
config.maxIdleConns = max
config.maxIdleConns = maxn
}

return option
}

// WithMaxIdleConnsPerHost sets the MaxIdleConnsPerHost value to
// underlying http.Transport of http.Client.
func WithMaxIdleConnsPerHost(max int) ClientOption {
func WithMaxIdleConnsPerHost(maxn int) ClientOption {
option := func(config *Config) {
config.maxIdleConnsPerHost = max
config.maxIdleConnsPerHost = maxn
}

return option
Expand Down
4 changes: 2 additions & 2 deletions idkit/id.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const (
// More about ULID: https://github.com/ulid/spec
func ULID() string {
t := time.Now().UTC()
e := ulid.Monotonic(rand.New(rand.NewSource(t.UnixNano())), 0) //nolint:gosec
e := ulid.Monotonic(rand.New(rand.NewSource(t.UnixNano())), 0)
id := ulid.MustNew(ulid.Timestamp(t), e)

return id.String()
Expand Down Expand Up @@ -61,7 +61,7 @@ func ValidateXID(id string) error {
func ParseXID(id string) (xid.ID, error) {
xID, err := xid.FromString(id)
if err != nil {
return [12]byte{}, ErrInvalidID
return xid.ID{}, ErrInvalidID
}

return xID, nil
Expand Down

0 comments on commit 9051b2d

Please sign in to comment.