domain

package
v0.29.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 27, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package domain defines authentication and authorization domain models and business logic.

It provides client-based authentication with policy-based authorization. Clients authenticate using secrets and are authorized via capability-based policies that control access to resource paths.

Package domain defines authentication and authorization domain models. Implements capability-based access control with clients, tokens, policies, and audit logging.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrClientNotFound indicates a client with the specified ID was not found.
	ErrClientNotFound = errors.Wrap(errors.ErrNotFound, "client not found")

	// ErrTokenNotFound indicates a token with the specified ID was not found.
	ErrTokenNotFound = errors.Wrap(errors.ErrNotFound, "token not found")

	// ErrInvalidCredentials indicates the provided credentials are invalid.
	// This error is returned for both non-existent clients and incorrect secrets
	// to prevent user enumeration attacks.
	ErrInvalidCredentials = errors.Wrap(errors.ErrUnauthorized, "invalid credentials")

	// ErrClientInactive indicates the client exists but is not active.
	// Inactive clients cannot authenticate or issue tokens.
	ErrClientInactive = errors.Wrap(errors.ErrForbidden, "client is inactive")

	// ErrClientLocked indicates the client is temporarily locked due to too many failed
	// authentication attempts. The client must wait for the lockout period to expire.
	ErrClientLocked = errors.Wrap(errors.ErrLocked, "client is locked")

	// ErrSignatureInvalid indicates the audit log HMAC signature verification failed.
	// This typically means the audit log data has been tampered with after creation.
	ErrSignatureInvalid = errors.Wrap(errors.ErrInvalidInput, "audit log signature is invalid")

	// ErrSignatureMissing indicates the audit log does not have a cryptographic signature.
	// This is expected for legacy logs created before signature implementation.
	ErrSignatureMissing = errors.Wrap(errors.ErrNotFound, "audit log signature is missing")

	// ErrKekNotFoundForLog indicates the KEK referenced by an audit log signature
	// was not found in the KEK chain. This should not occur if KEK retention policy
	// is properly enforced (ON DELETE RESTRICT constraint).
	ErrKekNotFoundForLog = errors.Wrap(
		errors.ErrNotFound,
		"kek not found for audit log signature verification",
	)
)

Authentication and authorization errors.

Functions

func HashTokenPlain added in v0.29.0

func HashTokenPlain(plain string) string

HashTokenPlain computes the canonical hex-encoded SHA-256 digest of a plaintext token. This is the single source of truth for how tokens are hashed both at issuance (storing the hash) and at authentication (looking up by hash). Drifting from this function on either side will silently invalidate every issued token.

func IsValidCapability added in v0.28.0

func IsValidCapability(cap Capability) bool

IsValidCapability checks if a capability is valid and exists in the system. Used for strict validation in policy parsing.

func MintClientSecret added in v0.29.0

func MintClientSecret() (plain string, err error)

MintClientSecret creates a fresh client secret and returns the plaintext value. The caller is responsible for hashing it under the chosen password-hash policy before persistence.

func MintToken added in v0.29.0

func MintToken() (plain, hash string, err error)

MintToken creates a fresh authentication token and returns the plaintext value alongside the canonical hash that should be persisted. Tokens are 32 random bytes, base64-URL encoded; the hash is HashTokenPlain(plain).

Types

type AuditLog

type AuditLog struct {
	ID         uuid.UUID
	RequestID  uuid.UUID
	ClientID   uuid.UUID
	Capability Capability
	Path       string
	Metadata   map[string]any
	Signature  []byte     // HMAC-SHA256 signature (32 bytes) for tamper detection
	KekID      *uuid.UUID // KEK used for signing (NULL for legacy unsigned logs)
	IsSigned   bool       // True if signed, false for legacy logs
	CreatedAt  time.Time
}

AuditLog records authorization decisions for compliance and security monitoring. Captures client identity, requested resource path, required capability, and metadata. Used to track access patterns and investigate security incidents.

Cryptographic Integrity: All audit logs are signed with HMAC-SHA256 using KEK-derived signing keys to detect tampering. The Signature field contains the 32-byte HMAC, KekID references the KEK used for signing, and IsSigned distinguishes signed logs from legacy unsigned logs created before the feature.

func (*AuditLog) Canonical added in v0.29.0

func (a *AuditLog) Canonical() ([]byte, error)

Canonical returns the deterministic byte representation of this audit log used for HMAC signing. Format: RequestID || ClientID || capability (length-prefixed) || path (length-prefixed) || metadata JSON (length-prefixed) || created_at as Unix nanoseconds (8 bytes, big-endian).

func (*AuditLog) HasValidSignature added in v0.9.0

func (a *AuditLog) HasValidSignature() bool

HasValidSignature checks if the audit log has complete signature data. Returns true only if the log is marked as signed, has a KEK ID, and contains a 32-byte HMAC signature.

func (*AuditLog) IsLegacy added in v0.9.0

func (a *AuditLog) IsLegacy() bool

IsLegacy returns true if this is an unsigned legacy audit log created before cryptographic integrity was implemented. Legacy logs have no signature, no KEK ID, and are marked as unsigned.

type AuditLogRepository added in v0.29.0

type AuditLogRepository interface {
	// Create stores a new audit log entry recording an authorization decision.
	// Returns error if the audit log ID already exists or database operation fails.
	Create(ctx context.Context, auditLog *AuditLog) error

	// Get retrieves a single audit log by ID. Returns error if not found.
	// Used for signature verification of specific audit logs.
	Get(ctx context.Context, id uuid.UUID) (*AuditLog, error)

	// ListCursor retrieves audit logs ordered by created_at descending (newest first) with cursor-based pagination
	// and optional time-based filtering. If afterID is provided, returns logs with ID greater than afterID (UUIDv7 ordering).
	// Accepts createdAtFrom and createdAtTo as optional filters (nil means no filter). Both boundaries are inclusive (>= and <=).
	// Accepts clientID as an optional filter (nil means no filter).
	// All timestamps are expected in UTC. Returns empty slice if no audit logs found. Limit is pre-validated (1-1000).
	ListCursor(
		ctx context.Context,
		afterID *uuid.UUID,
		limit int,
		createdAtFrom, createdAtTo *time.Time,
		clientID *uuid.UUID,
	) ([]*AuditLog, error)

	// DeleteOlderThan removes audit logs with created_at before the specified timestamp.
	// When dryRun is true, returns count via SELECT COUNT(*) without deletion. When false,
	// executes DELETE and returns affected rows. Supports transaction-aware operations via
	// context propagation. All timestamps are expected in UTC.
	DeleteOlderThan(ctx context.Context, olderThan time.Time, dryRun bool) (int64, error)
}

AuditLogRepository defines persistence operations for audit logs. Implementations must support transaction-aware operations via context propagation.

type Capability

type Capability string

Capability defines the types of operations that can be performed on resources. Capabilities are used in policy documents to control client authorization.

const (
	// ReadCapability allows reading resource data.
	ReadCapability Capability = "read"

	// WriteCapability allows creating or updating resource data.
	WriteCapability Capability = "write"

	// DeleteCapability allows removing resource data.
	DeleteCapability Capability = "delete"

	// EncryptCapability allows encrypting data using cryptographic keys.
	EncryptCapability Capability = "encrypt"

	// DecryptCapability allows decrypting data using cryptographic keys.
	DecryptCapability Capability = "decrypt"

	// RotateCapability allows rotating cryptographic keys.
	RotateCapability Capability = "rotate"
)

func ValidCapabilities added in v0.28.0

func ValidCapabilities() []Capability

ValidCapabilities returns a list of all defined capabilities in the system. Used for validation and UI generation.

type Client

type Client struct {
	ID             uuid.UUID        // Unique identifier (UUIDv7)
	Secret         string           //nolint:gosec // hashed client secret (not plaintext)
	Name           string           // Human-readable client name
	IsActive       bool             // Whether the client can authenticate
	Policies       []PolicyDocument // Authorization policies for this client
	FailedAttempts int              // Number of consecutive failed authentication attempts
	LockedUntil    *time.Time       // Time until which the client is locked (nil if not locked)
	CreatedAt      time.Time
}

Client represents an authentication client with associated authorization policies. Clients are used to authenticate API requests and enforce access control.

func (*Client) AttemptLogin added in v0.29.0

func (c *Client) AttemptLogin(
	plain string,
	compare func(plain, hashed string) bool,
	policy LockoutPolicy,
	now time.Time,
) LoginOutcome

AttemptLogin runs the login state machine for this client and returns the outcome.

Order of checks:

  1. If LockedUntil is in the future, return Locked (attempt is not counted).
  2. If the client is not active, return Inactive (attempt is not counted).
  3. If compare returns false, return BadSecret with incremented attempts and, when MaxAttempts > 0 and the threshold is reached, a fresh LockedUntil.
  4. Otherwise return Authenticated with FailedAttempts reset to 0 and LockedUntil nil.

The receiver is not mutated; the outcome is the single source of truth for the new (FailedAttempts, LockedUntil) tuple. The caller persists it.

func (*Client) IsAllowed

func (c *Client) IsAllowed(path string, capability Capability) bool

IsAllowed checks if the client's policies permit the given capability on the specified path. Uses case-sensitive path matching with wildcard support. Returns true if any policy matches the path and includes the capability.

Wildcard patterns:

  • "*" matches everything (admin mode)
  • "secret/*" matches any path starting with "secret/" (trailing wildcard - greedy)
  • "/v1/keys/*/rotate" matches "/v1/keys/payment/rotate" (single-segment wildcard)
  • "/v1/*/keys/*/rotate" matches "/v1/transit/keys/payment/rotate" (multiple wildcards)

Path matching rules:

  • Exact match: "secret" matches only "secret"
  • Trailing wildcard: "secret/*" matches "secret/app", "secret/app/db", etc.
  • Mid-path wildcard: "/v1/keys/*/rotate" matches exactly 4 segments with 3rd being any value
  • Case-sensitive: "Secret" does NOT match "secret"

type ClientRepository added in v0.29.0

type ClientRepository interface {
	// Create stores a new client in the repository.
	Create(ctx context.Context, client *Client) error

	// Update modifies an existing client in the repository.
	Update(ctx context.Context, client *Client) error

	// Get retrieves a client by ID. Returns ErrClientNotFound if not found.
	Get(ctx context.Context, clientID uuid.UUID) (*Client, error)

	// ListCursor retrieves clients ordered by ID descending (newest first) with cursor-based pagination.
	// If afterID is provided, returns clients with ID less than afterID (DESC order).
	// Returns empty slice if no clients found. Limit is pre-validated (1-1000).
	ListCursor(ctx context.Context, afterID *uuid.UUID, limit int) ([]*Client, error)

	// UpdateLockState atomically updates the failed attempt counter and lock expiry.
	// Pass failedAttempts=0 and lockedUntil=nil to reset the lock on successful auth.
	UpdateLockState(ctx context.Context, clientID uuid.UUID, failedAttempts int, lockedUntil *time.Time) error
}

ClientRepository defines persistence operations for authentication clients. Implementations must support transaction-aware operations via context propagation.

type CreateClientInput

type CreateClientInput struct {
	Name     string           // Human-readable name for identifying the client
	IsActive bool             // Whether the client can authenticate immediately after creation
	Policies []PolicyDocument // Authorization policies defining resource access permissions
}

CreateClientInput contains the parameters for creating a new authentication client. The client secret will be automatically generated and cannot be specified by the caller.

type CreateClientOutput

type CreateClientOutput struct {
	ID          uuid.UUID        // Unique identifier for the created client (UUIDv7)
	PlainSecret string           // Plain text secret for authentication (transmit securely, never log)
	Name        string           // Human-readable client name
	IsActive    bool             // Whether the client can authenticate
	Policies    []PolicyDocument // Authorization policies for this client
	CreatedAt   time.Time        // Creation timestamp
}

CreateClientOutput contains the result of creating a new client. SECURITY: The PlainSecret is only returned once and must be securely transmitted to the client. It will never be retrievable again after this response.

type Decision added in v0.29.0

type Decision int

Decision is the outcome variant of Client.AttemptLogin.

const (
	// DecisionAuthenticated indicates the secret matched on an active, unlocked client.
	DecisionAuthenticated Decision = iota
	// DecisionBadSecret indicates the client exists and is active, but the secret did not match.
	DecisionBadSecret
	// DecisionLocked indicates the client is within an active lockout window; no attempt was counted.
	DecisionLocked
	// DecisionInactive indicates the client has IsActive=false; no attempt was counted.
	DecisionInactive
)

type IssueTokenInput

type IssueTokenInput struct {
	ClientID     uuid.UUID
	ClientSecret string //nolint:gosec // authentication credential field
}

IssueTokenInput contains client credentials for token issuance requests. Used during authentication to verify client identity before generating tokens.

type IssueTokenOutput

type IssueTokenOutput struct {
	PlainToken string
	ExpiresAt  time.Time
}

IssueTokenOutput contains the newly issued authentication token and expiration. The PlainToken is only returned once and must be transmitted securely to the client.

type LockoutPolicy added in v0.29.0

type LockoutPolicy struct {
	MaxAttempts int
	Duration    time.Duration
}

LockoutPolicy configures how Client.AttemptLogin reacts to failures. MaxAttempts of zero disables locking; the counter is still incremented for visibility.

type LoginOutcome added in v0.29.0

type LoginOutcome struct {
	Decision       Decision
	FailedAttempts int
	LockedUntil    *time.Time
}

LoginOutcome is the result of Client.AttemptLogin. It carries the post-attempt state the caller must persist (FailedAttempts, LockedUntil); the Client receiver is not mutated.

type PolicyDocument

type PolicyDocument struct {
	Path         string       `json:"path"`         // Resource path pattern (supports "*" and "/*" wildcards)
	Capabilities []Capability `json:"capabilities"` // List of allowed operations on the resource
}

PolicyDocument defines access control rules for a specific resource path. Policies use prefix matching with wildcard support for flexible authorization.

type Token

type Token struct {
	ID        uuid.UUID  // Unique identifier (UUIDv7)
	TokenHash string     // SHA-256 hash of the token string
	ClientID  uuid.UUID  // ID of the client that owns this token
	ExpiresAt time.Time  // Token expiration timestamp
	RevokedAt *time.Time // Token revocation timestamp (nil if active)
	CreatedAt time.Time
}

Token represents an authentication token with expiration and revocation support. Tokens are stored as hashes and associated with a client for authentication.

type TokenRepository added in v0.29.0

type TokenRepository interface {
	// Create stores a new token in the repository.
	Create(ctx context.Context, token *Token) error

	// Update modifies an existing token in the repository.
	Update(ctx context.Context, token *Token) error

	// Get retrieves a token by ID. Returns ErrTokenNotFound if not found.
	Get(ctx context.Context, tokenID uuid.UUID) (*Token, error)

	// GetByTokenHash retrieves a token by its SHA-256 hash value.
	// Returns ErrTokenNotFound if no token matches the hash.
	GetByTokenHash(ctx context.Context, tokenHash string) (*Token, error)

	// RevokeByTokenID marks a specific token as revoked by setting its revoked_at timestamp.
	RevokeByTokenID(ctx context.Context, tokenID uuid.UUID) error

	// RevokeByClientID marks all active tokens for a specific client as revoked.
	RevokeByClientID(ctx context.Context, clientID uuid.UUID) error

	// PurgeExpiredAndRevoked permanently deletes tokens that are either expired or revoked
	// and were created before the specified timestamp. Returns the number of deleted tokens.
	PurgeExpiredAndRevoked(ctx context.Context, olderThan time.Time) (int64, error)
}

TokenRepository defines persistence operations for authentication tokens. Implementations must support transaction-aware operations via context propagation.

type UpdateClientInput

type UpdateClientInput struct {
	Name     string           // Updated human-readable name
	IsActive bool             // Updated active status (false prevents authentication)
	Policies []PolicyDocument // Updated authorization policies
}

UpdateClientInput contains the mutable fields for updating an existing client. The client ID and secret cannot be modified through updates.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL