Documentation
¶
Index ¶
- Variables
- func CloneExtras(extras map[string]any) map[string]any
- func GenerateCodeChallenge(verifier string) string
- func GenerateCodeVerifier() (string, error)
- func GenerateNonce() (string, error)
- func GenerateState() (string, error)
- func IsReservedClaim(name string) bool
- func ReservedClaimNames() []string
- func ValidateExtras(extras map[string]any) error
- type AuthRequest
- type AuthResult
- type AuthServiceOption
- func WithAuthorizationChecker(checker AuthorizationChecker) AuthServiceOption
- func WithBcryptCost(cost int) AuthServiceOption
- func WithClaimsEnricher(enricher ClaimsEnricher) AuthServiceOption
- func WithEventDispatcher(dispatcher *esDomain.EventDispatcher) AuthServiceOption
- func WithEventStore(store esDomain.EventStore) AuthServiceOption
- func WithJWTService(js JWTService) AuthServiceOption
- func WithLogger(logger Logger) AuthServiceOption
- func WithPasswordCredentialRepository(repo repositories.PasswordCredentialRepository) AuthServiceOption
- func WithSubscriptionService(svc SubscriptionService) AuthServiceOption
- func WithTokenStore(store TokenStore) AuthServiceOption
- type AuthenticationService
- type AuthorizationChecker
- type ClaimsEnricher
- type DefaultAuthenticationService
- func (s *DefaultAuthenticationService) CreateSession(ctx context.Context, agentID string, credentialID string, ipAddress string, ...) (*entities.AuthSession, error)
- func (s *DefaultAuthenticationService) ExchangeCode(ctx context.Context, code string, codeVerifier string, provider string, ...) (*AuthResult, error)
- func (s *DefaultAuthenticationService) FindOrCreateAgent(ctx context.Context, userInfo UserInfo) (*entities.Agent, *entities.Credential, *entities.Account, error)
- func (s *DefaultAuthenticationService) ImportPasswordCredential(ctx context.Context, email, displayName, bcryptHash, agentID, accountID string, ...) error
- func (s *DefaultAuthenticationService) InitiateAuthFlow(ctx context.Context, provider string, redirectURI string) (*AuthRequest, error)
- func (s *DefaultAuthenticationService) IssueIdentityToken(ctx context.Context, agent *entities.Agent, activeAccountID string) (string, error)
- func (s *DefaultAuthenticationService) RefreshIdentityToken(ctx context.Context, agentID, activeAccountID string) (string, error)
- func (s *DefaultAuthenticationService) RefreshTokens(ctx context.Context, credentialID string) (*AuthResult, error)
- func (s *DefaultAuthenticationService) RegisterPassword(ctx context.Context, email, displayName, plaintext string) (*entities.Agent, *entities.Credential, *entities.Account, error)
- func (s *DefaultAuthenticationService) RevokeAllSessions(ctx context.Context, agentID string) error
- func (s *DefaultAuthenticationService) RevokeSession(ctx context.Context, sessionID string) error
- func (s *DefaultAuthenticationService) UpdatePassword(ctx context.Context, agentID, oldPlaintext, newPlaintext string) error
- func (s *DefaultAuthenticationService) ValidateSession(ctx context.Context, sessionID string) (*SessionInfo, error)
- func (s *DefaultAuthenticationService) ValidateState(_ context.Context, receivedState string, storedState string) error
- func (s *DefaultAuthenticationService) VerifyPassword(ctx context.Context, email, plaintext string) (*entities.Agent, *entities.Credential, *entities.Account, error)
- type ImportOption
- type InviteClaims
- type InviteService
- func (s *InviteService) AcceptInvite(ctx context.Context, token string, userInfo UserInfo) (*entities.Agent, *entities.Credential, *entities.Account, error)
- func (s *InviteService) CreateInvite(ctx context.Context, accountID, email, roleID, inviterAgentID string) (*entities.Invite, string, error)
- func (s *InviteService) RevokeInvite(ctx context.Context, inviteID, revokerAgentID string) error
- type InviteServiceOption
- type InviteTokenService
- type JWTService
- type Logger
- type NoOpLogger
- type OAuthProvider
- type OAuthProviderRegistry
- type PericarpClaims
- type Permission
- type PermissionStore
- type PolicyDecisionPoint
- func (pdp *PolicyDecisionPoint) GetPermissions(ctx context.Context, agentID string) ([]Permission, error)
- func (pdp *PolicyDecisionPoint) GetProhibitions(ctx context.Context, agentID string) ([]Permission, error)
- func (pdp *PolicyDecisionPoint) IsAuthorized(ctx context.Context, agentID, action, target string) (bool, error)
- func (pdp *PolicyDecisionPoint) IsAuthorizedInAccount(ctx context.Context, agentID, accountID, action, target string) (bool, error)
- type SessionInfo
- type SubscriptionService
- type TokenReissuer
- type TokenStore
- type UserInfo
Constants ¶
This section is empty.
Variables ¶
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.
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.
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.
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 ¶
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 ¶
GenerateCodeChallenge generates a PKCE code challenge from a code verifier using the S256 method (SHA-256 hash, base64url-encoded).
func GenerateCodeVerifier ¶
GenerateCodeVerifier generates a cryptographically random PKCE code verifier. Returns a 43-character base64url-encoded string derived from 32 random bytes.
func GenerateNonce ¶
GenerateNonce generates a cryptographically random nonce for OpenID Connect ID token validation. Returns a 32-byte base64url-encoded string.
func GenerateState ¶
GenerateState generates a cryptographically random state parameter for OAuth CSRF protection. Returns a 32-byte base64url-encoded string.
func IsReservedClaim ¶
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 ¶
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 ¶
func NewDefaultAuthenticationService( providers OAuthProviderRegistry, agents repositories.AgentRepository, credentials repositories.CredentialRepository, sessions repositories.AuthSessionRepository, accounts repositories.AccountRepository, opts ...AuthServiceOption, ) *DefaultAuthenticationService
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 ¶
func (s *DefaultAuthenticationService) FindOrCreateAgent(ctx context.Context, userInfo UserInfo) (*entities.Agent, *entities.Credential, *entities.Account, 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. 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 ¶
func NewInviteService( invites repositories.InviteRepository, agents repositories.AgentRepository, accounts repositories.AccountRepository, credentials repositories.CredentialRepository, tokenService InviteTokenService, opts ...InviteServiceOption, ) *InviteService
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.
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:
- Collect all assignee IDs (agent + their roles)
- Check prohibitions — if any match, deny (prohibitions override permissions)
- Check permissions — if any match, allow
- 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.