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
+201
View File
@@ -0,0 +1,201 @@
// Package common defines small shared domain primitives used by auth/session
// aggregates and integration models.
package common
import (
"bytes"
"crypto/ed25519"
"encoding/base64"
"errors"
"fmt"
"net/mail"
"strings"
)
// ChallengeID identifies one auth confirmation challenge owned by the service.
type ChallengeID string
// String returns ChallengeID as a plain string identifier.
func (id ChallengeID) String() string {
return string(id)
}
// IsZero reports whether ChallengeID does not contain a usable identifier.
func (id ChallengeID) IsZero() bool {
return strings.TrimSpace(string(id)) == ""
}
// Validate reports whether ChallengeID is non-empty and already normalized for
// domain use.
func (id ChallengeID) Validate() error {
return validateToken("challenge id", string(id))
}
// DeviceSessionID identifies one persisted device session.
type DeviceSessionID string
// String returns DeviceSessionID as a plain string identifier.
func (id DeviceSessionID) String() string {
return string(id)
}
// IsZero reports whether DeviceSessionID does not contain a usable identifier.
func (id DeviceSessionID) IsZero() bool {
return strings.TrimSpace(string(id)) == ""
}
// Validate reports whether DeviceSessionID is non-empty and already
// normalized for domain use.
func (id DeviceSessionID) Validate() error {
return validateToken("device session id", string(id))
}
// UserID identifies one user resolved through the user-service boundary.
type UserID string
// String returns UserID as a plain string identifier.
func (id UserID) String() string {
return string(id)
}
// IsZero reports whether UserID does not contain a usable identifier.
func (id UserID) IsZero() bool {
return strings.TrimSpace(string(id)) == ""
}
// Validate reports whether UserID is non-empty and already normalized for
// domain use.
func (id UserID) Validate() error {
return validateToken("user id", string(id))
}
// Email stores one already-normalized e-mail address used by the auth domain.
type Email string
// String returns Email as the stored canonical e-mail string.
func (e Email) String() string {
return string(e)
}
// IsZero reports whether Email does not contain a usable e-mail value.
func (e Email) IsZero() bool {
return strings.TrimSpace(string(e)) == ""
}
// Validate reports whether Email is non-empty, does not contain surrounding
// whitespace, and matches the same single-address syntax expected by the
// public gateway contract.
func (e Email) Validate() error {
raw := string(e)
if err := validateToken("email", raw); err != nil {
return err
}
parsedAddress, err := mail.ParseAddress(raw)
if err != nil || parsedAddress.Name != "" || parsedAddress.Address != raw {
return fmt.Errorf("email %q must be a single valid email address", raw)
}
return nil
}
// RevokeReasonCode stores one machine-readable revoke reason code.
type RevokeReasonCode string
// String returns RevokeReasonCode as its stored code value.
func (code RevokeReasonCode) String() string {
return string(code)
}
// IsZero reports whether RevokeReasonCode is empty.
func (code RevokeReasonCode) IsZero() bool {
return strings.TrimSpace(string(code)) == ""
}
// Validate reports whether RevokeReasonCode is non-empty and normalized for
// domain use.
func (code RevokeReasonCode) Validate() error {
return validateToken("revoke reason code", string(code))
}
// RevokeActorType stores one machine-readable actor type for revoke audit.
type RevokeActorType string
// String returns RevokeActorType as its stored type value.
func (actorType RevokeActorType) String() string {
return string(actorType)
}
// IsZero reports whether RevokeActorType is empty.
func (actorType RevokeActorType) IsZero() bool {
return strings.TrimSpace(string(actorType)) == ""
}
// Validate reports whether RevokeActorType is non-empty and normalized for
// domain use.
func (actorType RevokeActorType) Validate() error {
return validateToken("revoke actor type", string(actorType))
}
// ClientPublicKey stores one validated Ed25519 public key in parsed binary
// form inside the domain model.
type ClientPublicKey struct {
value ed25519.PublicKey
}
// NewClientPublicKey validates value and returns a defensive copy suitable for
// storing inside domain aggregates.
func NewClientPublicKey(value ed25519.PublicKey) (ClientPublicKey, error) {
key := ClientPublicKey{
value: bytes.Clone(value),
}
if err := key.Validate(); err != nil {
return ClientPublicKey{}, err
}
return key, nil
}
// String returns ClientPublicKey as the standard base64-encoded raw 32-byte
// Ed25519 public key string.
func (key ClientPublicKey) String() string {
if key.IsZero() {
return ""
}
return base64.StdEncoding.EncodeToString(key.value)
}
// IsZero reports whether ClientPublicKey does not contain key material.
func (key ClientPublicKey) IsZero() bool {
return len(key.value) == 0
}
// Validate reports whether ClientPublicKey contains exactly one Ed25519 public
// key.
func (key ClientPublicKey) Validate() error {
switch len(key.value) {
case 0:
return errors.New("client public key must not be empty")
case ed25519.PublicKeySize:
return nil
default:
return fmt.Errorf("client public key must contain exactly %d bytes", ed25519.PublicKeySize)
}
}
// PublicKey returns a defensive copy of the parsed Ed25519 public key.
func (key ClientPublicKey) PublicKey() ed25519.PublicKey {
return bytes.Clone(key.value)
}
func validateToken(name string, value string) error {
switch {
case strings.TrimSpace(value) == "":
return fmt.Errorf("%s must not be empty", name)
case strings.TrimSpace(value) != value:
return fmt.Errorf("%s must not contain surrounding whitespace", name)
default:
return nil
}
}
@@ -0,0 +1,133 @@
package common
import (
"crypto/ed25519"
"github.com/stretchr/testify/require"
"testing"
)
func TestChallengeIDValidate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
value ChallengeID
wantErr bool
}{
{name: "valid", value: ChallengeID("challenge-123")},
{name: "empty", value: ChallengeID(""), wantErr: true},
{name: "whitespace", value: ChallengeID(" challenge-123 "), wantErr: true},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.value.Validate()
if tt.wantErr && err == nil {
require.FailNow(t, "Validate() returned nil error")
}
if !tt.wantErr && err != nil {
require.Failf(t, "test failed", "Validate() returned error: %v", err)
}
})
}
}
func TestEmailValidate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
value Email
wantErr bool
}{
{name: "valid", value: Email("pilot@example.com")},
{name: "invalid", value: Email("pilot"), wantErr: true},
{name: "surrounding whitespace", value: Email(" pilot@example.com "), wantErr: true},
{name: "display name", value: Email("Pilot <pilot@example.com>"), wantErr: true},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.value.Validate()
if tt.wantErr && err == nil {
require.FailNow(t, "Validate() returned nil error")
}
if !tt.wantErr && err != nil {
require.Failf(t, "test failed", "Validate() returned error: %v", err)
}
})
}
}
func TestNewClientPublicKey(t *testing.T) {
t.Parallel()
raw := make(ed25519.PublicKey, ed25519.PublicKeySize)
for i := range raw {
raw[i] = byte(i)
}
key, err := NewClientPublicKey(raw)
if err != nil {
require.Failf(t, "test failed", "NewClientPublicKey() returned error: %v", err)
}
if key.IsZero() {
require.FailNow(t, "IsZero() = true, want false")
}
cloned := key.PublicKey()
if len(cloned) != ed25519.PublicKeySize {
require.Failf(t, "test failed", "PublicKey() length = %d, want %d", len(cloned), ed25519.PublicKeySize)
}
raw[0] = 99
if key.PublicKey()[0] == 99 {
require.FailNow(t, "PublicKey() was mutated through constructor input")
}
}
func TestClientPublicKeyValidate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
value ClientPublicKey
wantErr bool
}{
{name: "empty", value: ClientPublicKey{}, wantErr: true},
{
name: "short",
value: ClientPublicKey{value: make(ed25519.PublicKey, ed25519.PublicKeySize-1)},
wantErr: true,
},
{
name: "valid",
value: ClientPublicKey{value: make(ed25519.PublicKey, ed25519.PublicKeySize)},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.value.Validate()
if tt.wantErr && err == nil {
require.FailNow(t, "Validate() returned nil error")
}
if !tt.wantErr && err != nil {
require.Failf(t, "test failed", "Validate() returned error: %v", err)
}
})
}
}