application

package
v0.0.0-...-63fb40d Latest Latest
Warning

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

Go to latest
Published: May 30, 2026 License: MIT Imports: 22 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidProvider              = errors.New("authentication: invalid provider")
	ErrInvalidState                 = errors.New("authentication: invalid state parameter")
	ErrCodeExchangeFailed           = errors.New("authentication: code exchange failed")
	ErrSessionNotFound              = errors.New("authentication: session not found")
	ErrSessionExpired               = errors.New("authentication: session expired")
	ErrSessionRevoked               = errors.New("authentication: session revoked")
	ErrTokenRefreshFailed           = errors.New("authentication: token refresh failed")
	ErrCredentialNotFound           = errors.New("authentication: credential not found")
	ErrEmailAlreadyTaken            = errors.New("authentication: email already registered with a password")
	ErrPasswordSupportNotConfigured = errors.New("authentication: password support not configured")
	ErrPasswordCredentialMissing    = errors.New("authentication: password credential not found for agent")
	// ErrJWTServiceNotConfigured is returned by RefreshIdentityToken when
	// no JWTService is wired. Distinct from IssueIdentityToken's
	// ("", nil) shape because refresh's sole purpose is to mint a token —
	// a silent empty result on misconfiguration would be a foot-gun.
	ErrJWTServiceNotConfigured = errors.New("authentication: JWTService not configured")
)

Sentinel errors for the authentication domain.

View Source
var (
	ErrInviteNotFound      = errors.New("invite: not found")
	ErrInviteExpired       = errors.New("invite: has expired")
	ErrInviteNotPending    = errors.New("invite: not in pending status")
	ErrNotAccountAdmin     = errors.New("invite: agent is not an admin or owner of the account")
	ErrInviteeAgentMissing = errors.New("invite: invitee agent not found")
	ErrInviteTokenInvalid  = errors.New("invite: token is invalid")
	ErrInviteEmailMismatch = errors.New("invite: authenticated email does not match invite")
)

Sentinel errors for the invite application service.

View Source
var (
	ErrTokenInvalid  = errors.New("authentication: token is invalid")
	ErrTokenExpired  = errors.New("authentication: token has expired")
	ErrSigningFailed = errors.New("authentication: failed to sign token")
	ErrNoSigningKey  = errors.New("authentication: no signing key configured")
	// ErrReservedClaim is returned when extras passed to IssueToken (or
	// carried on PericarpClaims at marshal time) contain keys that
	// collide with reserved JWT or pericarp core claims. All offending
	// keys are sorted and listed in the wrapped message so callers get a
	// deterministic, single-pass diagnosis rather than a flaky
	// one-key-at-a-time error driven by Go's map iteration order.
	//
	// This is the typed gate enforcing "extras cannot overwrite core
	// claims" — a ClaimsEnricher returning a reserved key surfaces here
	// (fail-closed on the IssueIdentityToken path), and TokenReissuer
	// implementations re-validate snapshotted extras against this same
	// gate so reserved-set growth in a future release cannot smuggle a
	// once-valid claim onto a re-signed token.
	ErrReservedClaim = errors.New("authentication: extras contain reserved claim names")
)

Sentinel errors for JWT operations.

View Source
var ErrInvalidPassword = errors.New("authentication: invalid password")

ErrInvalidPassword is returned when a plaintext password fails to match the stored hash. To avoid revealing which emails are registered, VerifyPassword also returns this sentinel when no matching credential is found at all.

Functions

func CloneExtras

func CloneExtras(extras map[string]any) map[string]any

CloneExtras returns a shallow copy of extras suitable for embedding into a PericarpClaims value that will be validated and signed. nil/empty input returns nil (matches the "no extras" wire format).

Scope of the snapshot — the clone is shallow:

  • The TOP-LEVEL map is decoupled from the caller's. After IssueToken / ReissueToken returns, the caller may add, remove, or replace top-level keys without affecting the issued token, and a subsequent call cannot observe partial state from a prior call's signing. This also closes the ValidateExtras→sign TOCTOU window inside a single call: the validated map and the signed map are the same map.
  • NESTED values (a map[string]any or []any inside an extras value) are still SHARED with the caller. A goroutine that mutates a nested map while json.Marshal is walking it will race regardless of this clone. Do not retain or mutate nested values after handing them to the auth service. A deep clone would close this gap but at a cost (reflection over arbitrary nested types) that is rarely justified — most enrichers return primitive-leaf maps.
  • Concurrent mutation of the source map's TOP-LEVEL keys *during* the clone itself is also a caller-side Go race: do not write a map while passing it to IssueToken.

func GenerateCodeChallenge

func GenerateCodeChallenge(verifier string) string

GenerateCodeChallenge generates a PKCE code challenge from a code verifier using the S256 method (SHA-256 hash, base64url-encoded).

func GenerateCodeVerifier

func GenerateCodeVerifier() (string, error)

GenerateCodeVerifier generates a cryptographically random PKCE code verifier. Returns a 43-character base64url-encoded string derived from 32 random bytes.

func GenerateNonce

func GenerateNonce() (string, error)

GenerateNonce generates a cryptographically random nonce for OpenID Connect ID token validation. Returns a 32-byte base64url-encoded string.

func GenerateState

func GenerateState() (string, error)

GenerateState generates a cryptographically random state parameter for OAuth CSRF protection. Returns a 32-byte base64url-encoded string.

func IsReservedClaim

func IsReservedClaim(name string) bool

IsReservedClaim reports whether name is a reserved top-level JWT claim owned by pericarp.

func ReservedClaimNames

func ReservedClaimNames() []string

ReservedClaimNames returns a fresh copy of the reserved JWT claim names that an extras map cannot overwrite. Useful for tests, validation helpers, or downstream JWTService implementations that want to mirror the protection.

func ValidateExtras

func ValidateExtras(extras map[string]any) error

ValidateExtras returns ErrReservedClaim listing every reserved key in extras (sorted), or nil when none collide. nil/empty extras are valid. JWTService implementations should call this before signing so that developers get a typed, single-shot diagnosis rather than fixing one reserved-key collision per deploy cycle.

Types

type AuthRequest

type AuthRequest struct {
	AuthURL      string
	State        string
	CodeVerifier string
	Nonce        string
	Provider     string
}

AuthRequest represents the result of initiating an OAuth authorization flow.

type AuthResult

type AuthResult struct {
	AccessToken  string
	RefreshToken string
	IDToken      string
	TokenType    string
	ExpiresIn    int
	UserInfo     UserInfo
}

AuthResult represents the result of a successful token exchange.

type AuthServiceOption

type AuthServiceOption func(*DefaultAuthenticationService)

AuthServiceOption configures the DefaultAuthenticationService.

func WithAuthorizationChecker

func WithAuthorizationChecker(checker AuthorizationChecker) AuthServiceOption

WithAuthorizationChecker sets a custom authorization checker for permission resolution. A nil checker disables permission resolution; ValidateSession will return empty permissions.

func WithBcryptCost

func WithBcryptCost(cost int) AuthServiceOption

WithBcryptCost overrides the bcrypt cost used when hashing newly registered or updated passwords. A non-positive value falls back to bcrypt.DefaultCost.

func WithClaimsEnricher

func WithClaimsEnricher(enricher ClaimsEnricher) AuthServiceOption

WithClaimsEnricher wires a ClaimsEnricher whose return value is passed as extras to JWTService.IssueToken on every IssueIdentityToken call. See ClaimsEnricher for the full contract; in summary:

  • Reserved JWT/pericarp claim names cannot be overwritten — collisions surface as ErrReservedClaim from the JWTService.
  • Enricher errors fail token issuance (fail-closed), unlike the SubscriptionService snapshot path (fail-open for third-party outages): a developer-supplied invariant that cannot be computed must not silently produce a token.
  • Extras are snapshotted on TokenReissuer.ReissueToken — the enricher is not re-invoked on account switch. A fresh snapshot is taken on the next IssueIdentityToken (re-auth) or RefreshIdentityToken (server-side state change without re-auth).

A nil enricher is silently ignored (matching every other With* option in this package); the enricher cannot be cleared after construction. Build a new service if you need to remove a previously-wired enricher.

func WithEventDispatcher

func WithEventDispatcher(dispatcher *esDomain.EventDispatcher) AuthServiceOption

WithEventDispatcher sets an in-process EventDispatcher that receives every committed domain event after the UnitOfWork persists it. Consumers can Subscribe[T] to react to events such as agent.created (e.g., to auto-assign a default role). Dispatch is best-effort: handler errors are non-fatal and do not roll back the auth operation, since the event is already durable. Dispatch only fires when an EventStore is also configured via WithEventStore.

func WithEventStore

func WithEventStore(store esDomain.EventStore) AuthServiceOption

WithEventStore sets the event store for atomic event persistence via UnitOfWork. When configured, FindOrCreateAgent will commit events atomically before saving projections.

func WithJWTService

func WithJWTService(js JWTService) AuthServiceOption

WithJWTService sets a JWTService for issuing identity tokens. When configured, IssueIdentityToken will produce a signed JWT; otherwise it returns an empty string (opaque-session-only mode).

func WithLogger

func WithLogger(logger Logger) AuthServiceOption

WithLogger sets a custom logger. The default is a no-op logger.

func WithPasswordCredentialRepository

func WithPasswordCredentialRepository(repo repositories.PasswordCredentialRepository) AuthServiceOption

WithPasswordCredentialRepository wires a PasswordCredentialRepository for password authentication support. The password methods on AuthenticationService return ErrPasswordSupportNotConfigured until this is set.

func WithSubscriptionService

func WithSubscriptionService(svc SubscriptionService) AuthServiceOption

WithSubscriptionService wires a SubscriptionService for snapshotting subscription state into issued JWTs. When unset, IssueIdentityToken issues tokens with no subscription claim.

func WithTokenStore

func WithTokenStore(store TokenStore) AuthServiceOption

WithTokenStore sets a custom token store for server-side OAuth token persistence.

type AuthenticationService

type AuthenticationService interface {
	// InitiateAuthFlow generates PKCE parameters and returns the authorization URL.
	InitiateAuthFlow(ctx context.Context, provider string, redirectURI string) (*AuthRequest, error)

	// ExchangeCode exchanges an authorization code for tokens (server-to-server).
	ExchangeCode(ctx context.Context, code string, codeVerifier string, provider string, redirectURI string) (*AuthResult, error)

	// ValidateState verifies the OAuth state parameter matches the stored state.
	ValidateState(ctx context.Context, receivedState string, storedState string) error

	// FindOrCreateAgent looks up an agent by provider credentials, creates if not found.
	// For new users, a personal Account is also created with the agent as owner.
	FindOrCreateAgent(ctx context.Context, userInfo UserInfo) (*entities.Agent, *entities.Credential, *entities.Account, error)

	// RegisterPassword creates a new Agent + personal Account + Credential
	// (provider="password") + PasswordCredential. Returns ErrEmailAlreadyTaken
	// when a password credential for the email already exists.
	RegisterPassword(ctx context.Context, email, displayName, plaintext string) (*entities.Agent, *entities.Credential, *entities.Account, error)

	// VerifyPassword authenticates an email + plaintext pair against a stored
	// PasswordCredential and returns the associated Agent, Credential and
	// (optional) personal Account on success. To prevent account enumeration,
	// both wrong-password and unknown-email cases return ErrInvalidPassword.
	VerifyPassword(ctx context.Context, email, plaintext string) (*entities.Agent, *entities.Credential, *entities.Account, error)

	// ImportPasswordCredential imports an already-hashed legacy bcrypt blob
	// against a caller-supplied agentID/accountID. Idempotent on
	// (provider="password", lower(email)). Used for bulk migration where
	// existing foreign keys must remain valid. Pass ImportWithSalt(salt)
	// for legacy systems that applied an extra application-layer salt
	// suffix on top of bcrypt.
	ImportPasswordCredential(ctx context.Context, email, displayName, bcryptHash, agentID, accountID string, opts ...ImportOption) error

	// UpdatePassword rotates the stored password for the given agent.
	// Verifies oldPlaintext before applying the change.
	UpdatePassword(ctx context.Context, agentID, oldPlaintext, newPlaintext string) error

	// IssueIdentityToken issues a signed JWT for the given agent.
	// Returns ("", nil) if no JWTService is configured.
	IssueIdentityToken(ctx context.Context, agent *entities.Agent, activeAccountID string) (string, error)

	// RefreshIdentityToken re-snapshots claims for an existing agent and
	// returns a freshly signed JWT. Re-runs the ClaimsEnricher and
	// re-fetches subscription state — unlike TokenReissuer.ReissueToken,
	// which copies existing extras + subscription verbatim. Takes an
	// agent ID instead of doing an OAuth round-trip or password verify,
	// so the caller owns the trust decision (typically: validated a
	// still-valid session/JWT before calling). Use after a server-side
	// change that affects authorization claims (subscription purchased,
	// role granted, feature flag flipped). Returns ErrJWTServiceNotConfigured
	// when no JWTService is wired (a misconfiguration if you reached
	// this method) — distinct from IssueIdentityToken's ("", nil) shape,
	// which exists to support opaque-session-only flows that refresh
	// does not.
	RefreshIdentityToken(ctx context.Context, agentID string, activeAccountID string) (string, error)

	// CreateSession creates an authenticated session for an agent.
	CreateSession(ctx context.Context, agentID string, credentialID string, ipAddress string, userAgent string, duration time.Duration) (*entities.AuthSession, error)

	// ValidateSession validates and returns session info.
	ValidateSession(ctx context.Context, sessionID string) (*SessionInfo, error)

	// RefreshTokens refreshes OAuth tokens for a credential.
	RefreshTokens(ctx context.Context, credentialID string) (*AuthResult, error)

	// RevokeSession revokes an active session.
	RevokeSession(ctx context.Context, sessionID string) error

	// RevokeAllSessions revokes all sessions for an agent.
	RevokeAllSessions(ctx context.Context, agentID string) error
}

AuthenticationService defines the interface for authentication operations.

type AuthorizationChecker

type AuthorizationChecker interface {
	// IsAuthorized checks whether the given agent is authorized to perform
	// the specified action on the target resource.
	// It evaluates all applicable policies, considering:
	// - Direct agent permissions
	// - Role-based permissions (via agent's assigned roles)
	// - Prohibitions (which override permissions per ODRL semantics)
	IsAuthorized(ctx context.Context, agentID, action, target string) (bool, error)

	// IsAuthorizedInAccount checks whether the given agent is authorized within
	// a specific account context. It considers:
	// - Direct agent permissions
	// - Global role-based permissions (via agent's assigned roles)
	// - Account-scoped role-based permissions (via agent's role in the account)
	// - Prohibitions (which override permissions per ODRL semantics)
	IsAuthorizedInAccount(ctx context.Context, agentID, accountID, action, target string) (bool, error)

	// GetPermissions returns all effective permissions for the given agent,
	// including permissions inherited through role assignments.
	GetPermissions(ctx context.Context, agentID string) ([]Permission, error)

	// GetProhibitions returns all effective prohibitions for the given agent,
	// including prohibitions inherited through role assignments.
	GetProhibitions(ctx context.Context, agentID string) ([]Permission, error)
}

AuthorizationChecker defines the interface for authorization decisions. Implementations resolve agent roles, policy assignments, and evaluate ODRL permissions and prohibitions to reach a decision.

type ClaimsEnricher

type ClaimsEnricher func(ctx context.Context, agent *entities.Agent, accounts []*entities.Account, activeAccountID string) (map[string]any, error)

ClaimsEnricher computes app-specific JWT claims for a freshly authenticated agent. Returned values are passed verbatim as the extras map to JWTService.IssueToken — keys colliding with reserved JWT or pericarp core claim names surface as ErrReservedClaim from the configured JWTService (per the JWTService contract). An enricher returning an error fails token issuance: a developer-supplied invariant must surface, never be silently dropped (contrast SubscriptionService, where a third-party outage is logged and the token issues without the claim).

The enricher is invoked on IssueIdentityToken (fresh authentication) and on RefreshIdentityToken (server-side state change without re-auth — see RefreshIdentityToken for the trust model). TokenReissuer.ReissueToken (e.g. account-switch) snapshots the existing extras verbatim onto the new token rather than re-running the enricher; the same stale-but-stable rule applies as for the subscription claim. A new enricher snapshot is taken on the next IssueIdentityToken or RefreshIdentityToken call.

Implementations MUST treat the accounts slice as read-only and MUST NOT retain it past the call — IssueIdentityToken passes the same slice to JWTService.IssueToken next, and a future caching refactor could expose mutations to other request-scoped code. Panics from the enricher are not recovered; they propagate to the caller of IssueIdentityToken.

type DefaultAuthenticationService

type DefaultAuthenticationService struct {
	// contains filtered or unexported fields
}

DefaultAuthenticationService implements AuthenticationService using OAuth providers and domain aggregates.

func NewDefaultAuthenticationService

NewDefaultAuthenticationService creates a new DefaultAuthenticationService. Required dependencies are the provider registry and repositories. Optional dependencies (TokenStore, AuthorizationChecker, Logger, EventStore) can be configured via functional options; safe no-op defaults are used when not provided.

func NewDefaultAuthenticationServiceLegacy deprecated

func NewDefaultAuthenticationServiceLegacy(
	providers OAuthProviderRegistry,
	agents repositories.AgentRepository,
	credentials repositories.CredentialRepository,
	sessions repositories.AuthSessionRepository,
	accounts repositories.AccountRepository,
	tokens TokenStore,
	authorization AuthorizationChecker,
) *DefaultAuthenticationService

Deprecated: NewDefaultAuthenticationServiceLegacy creates a DefaultAuthenticationService with a positional-parameter signature. Use NewDefaultAuthenticationService with functional options instead.

func (*DefaultAuthenticationService) CreateSession

func (s *DefaultAuthenticationService) CreateSession(ctx context.Context, agentID string, credentialID string, ipAddress string, userAgent string, duration time.Duration) (*entities.AuthSession, error)

CreateSession creates an authenticated session for an agent.

func (*DefaultAuthenticationService) ExchangeCode

func (s *DefaultAuthenticationService) ExchangeCode(ctx context.Context, code string, codeVerifier string, provider string, redirectURI string) (*AuthResult, error)

ExchangeCode exchanges an authorization code for tokens.

func (*DefaultAuthenticationService) FindOrCreateAgent

FindOrCreateAgent looks up an agent by provider credentials, creates if not found. For new users, a personal Account is also created with the agent as owner. For existing users, the personal Account is returned if one exists (may be nil).

func (*DefaultAuthenticationService) ImportPasswordCredential

func (s *DefaultAuthenticationService) ImportPasswordCredential(ctx context.Context, email, displayName, bcryptHash, agentID, accountID string, opts ...ImportOption) error

ImportPasswordCredential imports a pre-hashed bcrypt blob against existing agent/account IDs. Idempotent on (provider="password", lower(email)). Pass ImportWithSalt(salt) for legacy hashes that bcrypted plaintext+salt (an extra application-layer suffix on top of bcrypt's own per-hash salt).

func (*DefaultAuthenticationService) InitiateAuthFlow

func (s *DefaultAuthenticationService) InitiateAuthFlow(ctx context.Context, provider string, redirectURI string) (*AuthRequest, error)

InitiateAuthFlow generates PKCE parameters and returns the authorization URL.

func (*DefaultAuthenticationService) IssueIdentityToken

func (s *DefaultAuthenticationService) IssueIdentityToken(ctx context.Context, agent *entities.Agent, activeAccountID string) (string, error)

IssueIdentityToken issues a signed JWT for the given agent, or ("", nil) if no JWTService is configured. When a SubscriptionService is wired the current claim is snapshotted into the token; lookup failures are logged but do not block issuance — billing-provider outages must not break login. When a ClaimsEnricher is wired its result is passed as extras to JWTService.IssueToken; an enricher error fails token issuance (contrast SubscriptionService, which is fail-open).

func (*DefaultAuthenticationService) RefreshIdentityToken

func (s *DefaultAuthenticationService) RefreshIdentityToken(ctx context.Context, agentID, activeAccountID string) (string, error)

RefreshIdentityToken loads the agent by ID and delegates to IssueIdentityToken so every snapshot rule (fresh accounts, fresh subscription, fresh enricher result, fail-closed on enricher error, fail-open on subscription lookup) stays in one place.

func (*DefaultAuthenticationService) RefreshTokens

func (s *DefaultAuthenticationService) RefreshTokens(ctx context.Context, credentialID string) (*AuthResult, error)

RefreshTokens refreshes OAuth tokens for a credential.

func (*DefaultAuthenticationService) RegisterPassword

func (s *DefaultAuthenticationService) RegisterPassword(ctx context.Context, email, displayName, plaintext string) (*entities.Agent, *entities.Credential, *entities.Account, error)

RegisterPassword creates a new Agent + personal Account + Credential (provider="password") + PasswordCredential.

func (*DefaultAuthenticationService) RevokeAllSessions

func (s *DefaultAuthenticationService) RevokeAllSessions(ctx context.Context, agentID string) error

RevokeAllSessions revokes all sessions for an agent.

func (*DefaultAuthenticationService) RevokeSession

func (s *DefaultAuthenticationService) RevokeSession(ctx context.Context, sessionID string) error

RevokeSession revokes an active session.

func (*DefaultAuthenticationService) UpdatePassword

func (s *DefaultAuthenticationService) UpdatePassword(ctx context.Context, agentID, oldPlaintext, newPlaintext string) error

UpdatePassword rotates the stored password for the given agent.

func (*DefaultAuthenticationService) ValidateSession

func (s *DefaultAuthenticationService) ValidateSession(ctx context.Context, sessionID string) (*SessionInfo, error)

ValidateSession validates and returns session info.

func (*DefaultAuthenticationService) ValidateState

func (s *DefaultAuthenticationService) ValidateState(_ context.Context, receivedState string, storedState string) error

ValidateState verifies the OAuth state parameter matches the stored state. Uses constant-time comparison to prevent timing attacks.

func (*DefaultAuthenticationService) VerifyPassword

func (s *DefaultAuthenticationService) VerifyPassword(ctx context.Context, email, plaintext string) (*entities.Agent, *entities.Credential, *entities.Account, error)

VerifyPassword authenticates an email + plaintext pair. Returns ErrInvalidPassword for both wrong-password and unknown-email so callers cannot enumerate registered emails.

type ImportOption

type ImportOption func(*importConfig)

ImportOption configures a single ImportPasswordCredential call. Unlike AuthServiceOption (which wires service-wide dependencies once at construction), ImportOption carries per-credential state — the legacy salt suffix in particular is a per-row value that varies between migrated records.

func ImportWithSalt

func ImportWithSalt(salt string) ImportOption

ImportWithSalt attaches a plaintext salt suffix to the imported credential. The salt is appended to the user-supplied plaintext before bcrypt comparison on every subsequent VerifyPassword call, allowing import of legacy hashes whose plaintext was suffixed before hashing.

New credentials produced by RegisterPassword never carry a salt — pericarp relies on bcrypt's own per-hash salt — and rotating a password via UpdatePassword permanently clears any imported salt.

type InviteClaims

type InviteClaims struct {
	jwt.RegisteredClaims
	InviteID string `json:"invite_id"`
}

InviteClaims contains the JWT claims for an invite token.

type InviteService

type InviteService struct {
	// contains filtered or unexported fields
}

InviteService orchestrates the invite flow: creating, accepting, and revoking invites.

func NewInviteService

NewInviteService creates a new InviteService with the given dependencies.

func (*InviteService) AcceptInvite

func (s *InviteService) AcceptInvite(ctx context.Context, token string, userInfo UserInfo) (*entities.Agent, *entities.Credential, *entities.Account, error)

AcceptInvite accepts an invite using the provided token and user info. Returns the activated agent, credential, and account.

func (*InviteService) CreateInvite

func (s *InviteService) CreateInvite(ctx context.Context, accountID, email, roleID, inviterAgentID string) (*entities.Invite, string, error)

CreateInvite creates an invite for the given email to join an account with the specified role. Returns the invite and a signed invite token.

func (*InviteService) RevokeInvite

func (s *InviteService) RevokeInvite(ctx context.Context, inviteID, revokerAgentID string) error

RevokeInvite revokes a pending invite.

type InviteServiceOption

type InviteServiceOption func(*InviteService)

InviteServiceOption configures the InviteService.

func WithInviteEventStore

func WithInviteEventStore(store esDomain.EventStore) InviteServiceOption

WithInviteEventStore sets the event store for atomic event persistence via UnitOfWork.

func WithInviteLogger

func WithInviteLogger(logger Logger) InviteServiceOption

WithInviteLogger sets a custom logger for the InviteService.

type InviteTokenService

type InviteTokenService interface {
	// IssueInviteToken creates a signed JWT for the given invite.
	IssueInviteToken(ctx context.Context, inviteID string, expiry time.Duration) (string, error)

	// ValidateInviteToken parses and validates an invite token string, returning the claims.
	ValidateInviteToken(ctx context.Context, tokenString string) (*InviteClaims, error)
}

InviteTokenService defines the interface for issuing and validating invite tokens. This is separate from JWTService to avoid breaking existing implementors.

type JWTService

type JWTService interface {
	// IssueToken creates a signed JWT for the given agent and accounts.
	// A non-nil subscription is embedded as the "subscription" claim;
	// nil omits the claim. extras adds app-specific top-level claims;
	// nil/empty omits any extras. Reserved claim names in extras must
	// be rejected with a wrapped ErrReservedClaim.
	IssueToken(ctx context.Context, agent *entities.Agent, accounts []*entities.Account, activeAccountID string, subscription *auth.SubscriptionClaim, extras map[string]any) (string, error)

	// ValidateToken parses and validates a JWT string, returning the claims.
	// Non-reserved top-level claims are exposed via PericarpClaims.Extras.
	ValidateToken(ctx context.Context, tokenString string) (*PericarpClaims, error)
}

JWTService defines the interface for issuing and validating JWTs.

Implementations MUST reject extras containing reserved claim names (see ReservedClaimNames / ValidateExtras) by returning a wrapped ErrReservedClaim. This keeps the contract uniform across alternative signers so consumers can rely on the protection regardless of which JWTService is wired.

type Logger

type Logger interface {
	Info(ctx context.Context, msg string, keysAndValues ...interface{})
	Warn(ctx context.Context, msg string, keysAndValues ...interface{})
	Error(ctx context.Context, msg string, keysAndValues ...interface{})
}

Logger defines the interface for structured logging in the auth package.

type NoOpLogger

type NoOpLogger struct{}

NoOpLogger is the default Logger that silently discards all log messages. It is exported so infrastructure packages can share the same no-op default.

func (NoOpLogger) Error

func (NoOpLogger) Error(_ context.Context, _ string, _ ...interface{})

func (NoOpLogger) Info

func (NoOpLogger) Info(_ context.Context, _ string, _ ...interface{})

func (NoOpLogger) Warn

func (NoOpLogger) Warn(_ context.Context, _ string, _ ...interface{})

type OAuthProvider

type OAuthProvider interface {
	// Name returns the provider identifier (e.g., "google", "github").
	Name() string

	// AuthCodeURL generates the authorization URL with PKCE parameters.
	AuthCodeURL(state string, codeChallenge string, nonce string, redirectURI string) string

	// Exchange exchanges an authorization code for tokens.
	Exchange(ctx context.Context, code string, codeVerifier string, redirectURI string) (*AuthResult, error)

	// RefreshToken refreshes an access token using a refresh token.
	RefreshToken(ctx context.Context, refreshToken string) (*AuthResult, error)

	// RevokeToken revokes a token at the provider.
	RevokeToken(ctx context.Context, token string) error

	// ValidateIDToken validates the ID token and extracts user claims.
	ValidateIDToken(ctx context.Context, idToken string, nonce string) (*UserInfo, error)
}

OAuthProvider defines a provider-agnostic interface for OAuth 2.0 / OpenID Connect operations.

type OAuthProviderRegistry

type OAuthProviderRegistry map[string]OAuthProvider

OAuthProviderRegistry maps provider names to their OAuthProvider implementations.

type PericarpClaims

type PericarpClaims struct {
	jwt.RegisteredClaims
	AgentID         string                  `json:"agent_id"`
	AccountIDs      []string                `json:"account_ids"`
	ActiveAccountID string                  `json:"active_account_id,omitempty"`
	Subscription    *auth.SubscriptionClaim `json:"subscription,omitempty"`
	Extras          map[string]any          `json:"-"`
}

PericarpClaims contains the JWT claims issued by the auth system. AgentID mirrors RegisteredClaims.Subject for convenient access without parsing the standard "sub" field. Subscription is set by AuthenticationService.IssueIdentityToken when a SubscriptionService is configured; the omitempty tag keeps the claim absent (rather than null) in opaque-session-only deployments.

Extras carries app-specific claims attached by a ClaimsEnricher. They are flattened to top-level JWT claims on marshal and re-collected on unmarshal. Reserved claim names (see ReservedClaimNames) cannot reach the wire from Extras: MarshalJSON returns ErrReservedClaim if any are present, and UnmarshalJSON excludes them when parsing externally minted tokens. Numeric extras decode as float64 per encoding/json defaults — int64-precision values should be passed as strings.

func (PericarpClaims) MarshalJSON

func (c PericarpClaims) MarshalJSON() ([]byte, error)

MarshalJSON flattens Extras into the top-level JWT claims object alongside the core claims. If Extras contains any reserved claim name (defense in depth — IssueToken's ValidateExtras call is the developer-facing gate), MarshalJSON returns ErrReservedClaim instead of silently skipping the offending keys: a missing claim downstream is far harder to diagnose than a refused token.

func (*PericarpClaims) UnmarshalJSON

func (c *PericarpClaims) UnmarshalJSON(data []byte) error

UnmarshalJSON populates the core claims and collects every other top-level key into Extras. Reserved claim names are excluded from Extras even when an externally minted token places them as siblings of the core fields — silent exclusion (rather than error) keeps validation tolerant of forged tokens that try to smuggle reserved names into Extras to bypass authorization checks reading the map.

type Permission

type Permission struct {
	Assignee string // Agent or Role ID that holds this permission
	Action   string // ODRL action IRI (e.g., odrl:read)
	Target   string // Asset/resource identifier or wildcard "*"
}

Permission represents a resolved permission or prohibition for querying.

type PermissionStore

type PermissionStore interface {
	// GetPermissionsForAssignee returns all permissions for a specific assignee (agent or role).
	GetPermissionsForAssignee(ctx context.Context, assigneeID string) ([]Permission, error)

	// GetProhibitionsForAssignee returns all prohibitions for a specific assignee.
	GetProhibitionsForAssignee(ctx context.Context, assigneeID string) ([]Permission, error)

	// GetRolesForAgent returns all global role IDs currently assigned to the given agent.
	GetRolesForAgent(ctx context.Context, agentID string) ([]string, error)

	// GetRolesForAgentInAccount returns role IDs assigned to the agent within a specific account.
	GetRolesForAgentInAccount(ctx context.Context, agentID, accountID string) ([]string, error)
}

PermissionStore provides read access to permission data for authorization decisions. This interface abstracts the projection/read model that stores resolved permissions. Consuming applications implement this against their storage layer.

type PolicyDecisionPoint

type PolicyDecisionPoint struct {
	// contains filtered or unexported fields
}

PolicyDecisionPoint implements AuthorizationChecker using a PermissionStore for resolving authorization decisions following ODRL semantics.

Decision logic:

  1. Collect all assignee IDs (agent + their roles)
  2. Check prohibitions — if any match, deny (prohibitions override permissions)
  3. Check permissions — if any match, allow
  4. Default deny

func NewPolicyDecisionPoint

func NewPolicyDecisionPoint(store PermissionStore) *PolicyDecisionPoint

NewPolicyDecisionPoint creates a new PolicyDecisionPoint with the given store.

func (*PolicyDecisionPoint) GetPermissions

func (pdp *PolicyDecisionPoint) GetPermissions(ctx context.Context, agentID string) ([]Permission, error)

GetPermissions returns all effective permissions for the agent.

func (*PolicyDecisionPoint) GetProhibitions

func (pdp *PolicyDecisionPoint) GetProhibitions(ctx context.Context, agentID string) ([]Permission, error)

GetProhibitions returns all effective prohibitions for the agent.

func (*PolicyDecisionPoint) IsAuthorized

func (pdp *PolicyDecisionPoint) IsAuthorized(ctx context.Context, agentID, action, target string) (bool, error)

IsAuthorized checks whether the agent is authorized following ODRL semantics.

func (*PolicyDecisionPoint) IsAuthorizedInAccount

func (pdp *PolicyDecisionPoint) IsAuthorizedInAccount(ctx context.Context, agentID, accountID, action, target string) (bool, error)

IsAuthorizedInAccount checks whether the agent is authorized within an account context. It collects both global roles and account-scoped roles before evaluating.

type SessionInfo

type SessionInfo struct {
	SessionID   string
	AgentID     string
	AccountID   string
	Permissions []Permission
	ExpiresAt   time.Time
}

SessionInfo represents validated session information returned to consumers.

type SubscriptionService

type SubscriptionService interface {
	GetSubscription(ctx context.Context, agentID, accountID string) (*auth.SubscriptionClaim, error)
}

SubscriptionService resolves the current subscription state for an agent at token-issuance time. The resulting SubscriptionClaim is embedded in the JWT so consumer services can gate paid-tier access without per- request billing API calls. Returning (nil, nil) is the canonical "no record" answer — the claim is omitted from the token and consumers see inactive via SubscriptionClaim.IsActive on the nil pointer. Errors are logged by the caller and treated the same as no record so a billing- provider outage cannot block login.

type TokenReissuer

type TokenReissuer interface {
	ReissueToken(ctx context.Context, claims *PericarpClaims, activeAccountID string) (string, error)
}

TokenReissuer re-issues a JWT with a different active account without requiring entity lookups. Separate from JWTService to avoid breaking existing implementors.

type TokenStore

type TokenStore interface {
	// StoreTokens stores OAuth tokens for a credential.
	StoreTokens(ctx context.Context, credentialID string, accessToken, refreshToken, idToken string, expiresAt time.Time) error

	// GetTokens retrieves stored OAuth tokens for a credential.
	GetTokens(ctx context.Context, credentialID string) (accessToken, refreshToken string, expiresAt time.Time, err error)

	// DeleteTokens removes all stored tokens for a credential.
	DeleteTokens(ctx context.Context, credentialID string) error

	// NeedsRefresh checks if the stored access token needs refreshing.
	NeedsRefresh(ctx context.Context, credentialID string) (bool, error)
}

TokenStore defines the interface for server-side token storage.

type UserInfo

type UserInfo struct {
	ProviderUserID string
	Email          string
	DisplayName    string
	AvatarURL      string
	Provider       string
}

UserInfo represents normalized user information from any identity provider.

Jump to

Keyboard shortcuts

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