feat: authsession service

This commit is contained in:
Ilia Denisov
2026-04-08 16:23:07 +02:00
committed by GitHub
parent 28f04916af
commit 86a68ed9d0
174 changed files with 31732 additions and 112 deletions
@@ -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{}
)