feat: authsession service
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
// Package local provides small in-process runtime implementations for
|
||||
// authsession ports that do not require network dependencies.
|
||||
package local
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"galaxy/authsession/internal/domain/common"
|
||||
"galaxy/authsession/internal/ports"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
challengeIDPrefix = "challenge-"
|
||||
deviceSessionIDPrefix = "device-session-"
|
||||
codeDigits = 6
|
||||
)
|
||||
|
||||
// Clock implements ports.Clock using the local system clock in UTC.
|
||||
type Clock struct{}
|
||||
|
||||
// Now returns the current system time normalized to UTC.
|
||||
func (Clock) Now() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
// IDGenerator implements ports.IDGenerator with cryptographically random
|
||||
// opaque identifiers.
|
||||
type IDGenerator struct{}
|
||||
|
||||
// NewChallengeID returns a fresh random challenge identifier.
|
||||
func (IDGenerator) NewChallengeID() (common.ChallengeID, error) {
|
||||
value, err := newOpaqueIDString(challengeIDPrefix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return common.ChallengeID(value), nil
|
||||
}
|
||||
|
||||
// NewDeviceSessionID returns a fresh random device-session identifier.
|
||||
func (IDGenerator) NewDeviceSessionID() (common.DeviceSessionID, error) {
|
||||
value, err := newOpaqueIDString(deviceSessionIDPrefix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return common.DeviceSessionID(value), nil
|
||||
}
|
||||
|
||||
// CodeGenerator implements ports.CodeGenerator with random 6-digit decimal
|
||||
// confirmation codes.
|
||||
type CodeGenerator struct{}
|
||||
|
||||
// Generate returns one fresh random 6-digit decimal code.
|
||||
func (CodeGenerator) Generate() (string, error) {
|
||||
var builder strings.Builder
|
||||
builder.Grow(codeDigits)
|
||||
|
||||
for idx := 0; idx < codeDigits; idx++ {
|
||||
digit, err := rand.Int(rand.Reader, big.NewInt(10))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("generate confirmation code: %w", err)
|
||||
}
|
||||
builder.WriteByte(byte('0' + digit.Int64()))
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
// CodeHasher implements ports.CodeHasher with bcrypt-backed hashes.
|
||||
type CodeHasher struct{}
|
||||
|
||||
// Hash returns the bcrypt hash of code.
|
||||
func (CodeHasher) Hash(code string) ([]byte, error) {
|
||||
if err := validateCode(code); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(code), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hash confirmation code: %w", err)
|
||||
}
|
||||
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
// Compare reports whether hash matches code.
|
||||
func (CodeHasher) Compare(hash []byte, code string) (bool, error) {
|
||||
if err := validateCode(code); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(hash) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
err := bcrypt.CompareHashAndPassword(hash, []byte(code))
|
||||
switch err {
|
||||
case nil:
|
||||
return true, nil
|
||||
case bcrypt.ErrMismatchedHashAndPassword:
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("compare confirmation code hash: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
func newOpaqueIDString(prefix string) (string, error) {
|
||||
randomBytes := make([]byte, 16)
|
||||
if _, err := rand.Read(randomBytes); err != nil {
|
||||
return "", fmt.Errorf("generate opaque identifier: %w", err)
|
||||
}
|
||||
|
||||
return prefix + base64.RawURLEncoding.EncodeToString(randomBytes), nil
|
||||
}
|
||||
|
||||
func validateCode(code string) error {
|
||||
switch {
|
||||
case strings.TrimSpace(code) == "":
|
||||
return fmt.Errorf("code must not be empty")
|
||||
case strings.TrimSpace(code) != code:
|
||||
return fmt.Errorf("code must not contain surrounding whitespace")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_ ports.Clock = Clock{}
|
||||
_ ports.IDGenerator = IDGenerator{}
|
||||
_ ports.CodeGenerator = CodeGenerator{}
|
||||
_ ports.CodeHasher = CodeHasher{}
|
||||
)
|
||||
Reference in New Issue
Block a user