feat: authsession service
This commit is contained in:
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user