Documentation
¶
Overview ¶
Package service implements the business logic for the identity service.
The AuthService sits between the Connect-Go handler layer and the graph persistence layer. It contains all authentication, token management, and 2FA logic. It does NOT import Connect/protobuf types -- it uses plain Go structs and returns errors that the handler translates to gRPC codes.
Security invariants:
- Passwords, secrets, and tokens are NEVER logged.
- Failed login lockout: 5 attempts, 15-min lock (configurable).
- TOTP challenges: 5-min expiry, single-use.
- QR login sessions: 5-min expiry, consumed after token issuance.
- Refresh tokens: rotated on every use (old token deleted, new one issued).
- All security events are audit-logged via the audit.Logger.
Package service — stub implementations for Repository and DB.
These stubs return ErrServiceUnavailable for every operation. They exist so the identity service binary can start and serve health checks / JWKS even when the persistence adapter has not yet been wired up. Any RPC that touches persistence will receive a clean "service unavailable" error instead of a nil-pointer panic.
Index ¶
- Constants
- Variables
- func BuildAgeGate(cfg *config.Config, logger *zap.Logger) agegate.Determiner
- func WithProjectScope(ctx context.Context, scope *ProjectScope) context.Context
- type AdminProject
- type AdminProjectAuthDomain
- type AdminProjectCredential
- type AdminService
- func (s *AdminService) DeactivateUser(ctx context.Context, actorID, targetUserID, reason string) error
- func (s *AdminService) DeleteUser(ctx context.Context, actorID, targetUserID string) error
- func (s *AdminService) GetUser(ctx context.Context, actorID, userID string) (*User, error)
- func (s *AdminService) InviteUser(ctx context.Context, actorID, email, name, role, recoveryEmail string, ...) (*InviteResult, error)
- func (s *AdminService) ListUsers(ctx context.Context, actorID, statusFilter, search, cursor string, limit int) ([]*User, string, int, error)
- func (s *AdminService) ReactivateUser(ctx context.Context, actorID, targetUserID string) error
- func (s *AdminService) ResetUserPassword(ctx context.Context, actorID, targetUserID string, generateTemp bool) (*ResetPasswordResult, error)
- func (s *AdminService) SetUserQuota(ctx context.Context, actorID, targetUserID string, quotaBytes int64) error
- func (s *AdminService) UpdateUser(ctx context.Context, actorID, userID, name, role, avatarURL string) (*User, error)
- type AuditEvent
- type AuthService
- func (s *AuthService) AcceptInvitation(ctx context.Context, invitationToken, password, name, ipAddr, userAgent string) (*LoginResult, error)
- func (s *AuthService) ApproveQrLogin(ctx context.Context, sessionID string, approve bool, userID, userAgent string) (string, error)
- func (s *AuthService) BeginHostedOAuth(ctx context.Context, provider, redirectURI, returnTo, csrfToken string) (*HostedOAuthBeginResult, error)
- func (s *AuthService) BeginOAuthLogin(ctx context.Context, provider, redirectURI string) (*OAuthBeginResult, error)
- func (s *AuthService) BeginPasskeyLogin(ctx context.Context, email string) (string, string, error)
- func (s *AuthService) BeginPasskeyRegistration(ctx context.Context, userID, deviceName string) (string, string, error)
- func (s *AuthService) BeginTotpSetup(ctx context.Context, userID string) (string, string, []string, error)
- func (s *AuthService) CompleteHostedOAuth(ctx context.Context, ...) (*HostedOAuthCallbackResult, error)
- func (s *AuthService) CompletePasskeyLogin(ctx context.Context, challengeID, credentialJSON, ipAddr, userAgent string) (*LoginResult, error)
- func (s *AuthService) CompletePasskeyRegistration(ctx context.Context, userID, challengeID, credentialJSON, deviceName string) (*PasskeyInfo, error)
- func (s *AuthService) ConfirmEmailChange(ctx context.Context, token string) (*User, error)
- func (s *AuthService) ConfirmPasswordReset(ctx context.Context, token, newPassword string) error
- func (s *AuthService) DisableTotp(ctx context.Context, userID, password string) error
- func (s *AuthService) GetCurrentUser(ctx context.Context, userID string) (*User, error)
- func (s *AuthService) GetQrLoginSession(ctx context.Context, sessionID string) (*QrSessionInfo, error)
- func (s *AuthService) InitiateQrLogin(ctx context.Context, deviceInfo, userAgent, ipAddr string) (*InitiateQrLoginResult, error)
- func (s *AuthService) LinkIdentity(ctx context.Context, ...) (*OAuthIdentity, error)
- func (s *AuthService) Logout(ctx context.Context, rawRefreshToken string) error
- func (s *AuthService) OAuthLogin(ctx context.Context, params OAuthLoginParams) (*LoginResult, error)
- func (s *AuthService) PasswordLogin(ctx context.Context, email, password, ipAddr, userAgent string) (*LoginResult, error)
- func (s *AuthService) PasswordSignup(ctx context.Context, email, password, name, recoveryEmail string, ...) (*LoginResult, error)
- func (s *AuthService) PollQrLogin(ctx context.Context, sessionID, pollSecret, ipAddr, userAgent string) (*PollQrResult, error)
- func (s *AuthService) RedeemMagicLink(ctx context.Context, token, ipAddr, userAgent string) (*MagicLinkResult, error)
- func (s *AuthService) RedeemOAuthCode(ctx context.Context, code, ipAddr, userAgent string) (*LoginResult, error)
- func (s *AuthService) RefreshToken(ctx context.Context, rawRefreshToken, ipAddr, userAgent string) (*User, string, string, error)
- func (s *AuthService) RegenerateRecoveryCodes(ctx context.Context, userID, password string) ([]string, error)
- func (s *AuthService) RequestEmailChange(ctx context.Context, userID, newEmail, currentPassword string) error
- func (s *AuthService) RequestEmailLoginCode(ctx context.Context, emailAddr string) error
- func (s *AuthService) RequestMagicLink(ctx context.Context, emailAddr, returnTo string) error
- func (s *AuthService) RequestPasswordReset(ctx context.Context, emailAddr string) error
- func (s *AuthService) RequestPhoneVerification(ctx context.Context, userID, phoneNumber string) error
- func (s *AuthService) SendEmailVerification(ctx context.Context, userID string) error
- func (s *AuthService) VerifyEmail(ctx context.Context, token string) (*User, error)
- func (s *AuthService) VerifyEmailLoginCode(ctx context.Context, emailAddr, code string, ipAddr, userAgent string) (*LoginResult, error)
- func (s *AuthService) VerifyPhoneCode(ctx context.Context, userID, phoneNumber, code string) (*User, error)
- func (s *AuthService) VerifyTotp(ctx context.Context, challengeID, code, ipAddr, userAgent string) (*LoginResult, error)
- func (s *AuthService) VerifyTotpSetup(ctx context.Context, userID, code string) (bool, error)
- func (s *AuthService) WithLoginGovernance(g *LoginGovernance) *AuthService
- func (s *AuthService) WithTenantAutoFormer(af TenantAutoFormStore) *AuthService
- type BeginIdentityVerificationResult
- type BootstrappedAdmin
- type CachingProjectResolver
- type ControlPlaneAdminService
- func (s *ControlPlaneAdminService) AddProjectAuthDomain(ctx context.Context, secret, projectID, hostname string, isPrimary bool) (*RegisteredAuthDomain, error)
- func (s *ControlPlaneAdminService) AdminAddProjectAuthDomain(ctx context.Context, secret, projectID, hostname string, isPrimary bool) error
- func (s *ControlPlaneAdminService) AdminAddTenantAdmin(ctx context.Context, secret, projectID, tenantID, userID, role string) (*TenantMembership, error)
- func (s *ControlPlaneAdminService) AdminCreateProject(ctx context.Context, secret, name, storageScopeID string) (string, error)
- func (s *ControlPlaneAdminService) AdminCreateProjectCredential(ctx context.Context, secret, projectID, kind string) (*MintedCredential, error)
- func (s *ControlPlaneAdminService) AdminCreateTenant(ctx context.Context, secret, projectID, name, primaryDomain string) (string, error)
- func (s *ControlPlaneAdminService) CreateFirstPlatformAdmin(ctx context.Context, email, password string) (*BootstrappedAdmin, error)
- func (s *ControlPlaneAdminService) DeleteLoginPolicy(ctx context.Context, secret, projectID, tenantID string) error
- func (s *ControlPlaneAdminService) Enabled() bool
- func (s *ControlPlaneAdminService) GetLoginPolicy(ctx context.Context, secret, projectID, tenantID string) (*LoginPolicy, error)
- func (s *ControlPlaneAdminService) GetProjectConfig(ctx context.Context, secret, projectID string) (string, error)
- func (s *ControlPlaneAdminService) ListProjectAuthDomains(ctx context.Context, secret, projectID string) ([]*AdminProjectAuthDomain, error)
- func (s *ControlPlaneAdminService) SetPrimaryAuthDomain(ctx context.Context, secret, projectID, hostname string) (*AdminProjectAuthDomain, error)
- func (s *ControlPlaneAdminService) UpsertLoginPolicy(ctx context.Context, secret string, p *LoginPolicy) (*LoginPolicy, error)
- func (s *ControlPlaneAdminService) UpsertProjectConfig(ctx context.Context, secret, projectID, configJSON string) (string, error)
- func (s *ControlPlaneAdminService) VerifyProjectAuthDomain(ctx context.Context, secret, projectID, hostname string) (*AdminProjectAuthDomain, error)
- type ControlPlaneProjectStore
- type CreatedDomain
- type CreatedInvitation
- type DB
- type DNSResolver
- type Domain
- type DomainService
- func (s *DomainService) CreateDomain(ctx context.Context, callerID, tenantID, domain, method string) (*CreatedDomain, error)
- func (s *DomainService) ListTenantDomains(ctx context.Context, callerID, tenantID string) ([]*Domain, error)
- func (s *DomainService) VerifyDomain(ctx context.Context, callerID, domainID string) (*Domain, error)
- type DomainStore
- type EmailChangeToken
- type EmailLoginCodeRecord
- type EmailVerificationToken
- type Group
- type GroupService
- func (s *GroupService) AddGroupMember(ctx context.Context, actorID, groupID, userID string) error
- func (s *GroupService) CreateGroup(ctx context.Context, actorID, name, description string) (*Group, error)
- func (s *GroupService) DeleteGroup(ctx context.Context, actorID, groupID string) error
- func (s *GroupService) ListGroupMembers(ctx context.Context, actorID, groupID string) ([]*User, error)
- func (s *GroupService) ListGroups(ctx context.Context, actorID, cursor string, limit int) ([]*Group, string, error)
- func (s *GroupService) RemoveGroupMember(ctx context.Context, actorID, groupID, userID string) error
- func (s *GroupService) UpdateGroup(ctx context.Context, actorID, groupID, name, description string) (*Group, error)
- type HelpRequest
- type HelpService
- func (s *HelpService) ListHelpRequests(ctx context.Context, actorID, statusFilter, cursor string, limit int) ([]*HelpRequest, string, int, error)
- func (s *HelpService) RequestAdminHelp(ctx context.Context, email, reason, sourceIP, userAgent string) error
- func (s *HelpService) ResolveHelpRequest(ctx context.Context, actorID, requestID string, reject bool, notes string) (*HelpRequest, error)
- type HostedOAuthBeginResult
- type HostedOAuthCallbackResult
- type IdentityVerificationRecord
- type IdentityVerificationService
- func (s *IdentityVerificationService) BeginIdentityVerification(ctx context.Context, userID string) (*BeginIdentityVerificationResult, error)
- func (s *IdentityVerificationService) GetIdentityVerificationStatus(ctx context.Context, callerUserID, verificationID string) (*IdentityVerificationRecord, error)
- func (s *IdentityVerificationService) WithMinorDataMinimizer(m MinorDataMinimizer) *IdentityVerificationService
- type InitiateQrLoginResult
- type InvitationRecord
- type InvitationStore
- type InviteResult
- type LoginChallengeRecord
- type LoginGovernance
- type LoginPolicy
- type LoginPolicyStore
- type LoginResult
- type MagicLinkResult
- type MagicLinkTokenRecord
- type MembershipService
- func (s *MembershipService) AcceptTenantInvitation(ctx context.Context, callerID, rawToken string) (*TenantMembership, error)
- func (s *MembershipService) CreateTenantInvitation(ctx context.Context, callerID, tenantID, emailAddr, role string) (*CreatedInvitation, error)
- func (s *MembershipService) ListTenantInvitations(ctx context.Context, callerID, tenantID string) ([]*TenantInvitation, error)
- func (s *MembershipService) ListTenantMembers(ctx context.Context, callerID, tenantID string) ([]*TenantMembership, error)
- func (s *MembershipService) RemoveTenantMember(ctx context.Context, callerID, tenantID, targetUserID string) error
- type MembershipStore
- type MinorDataMinimizer
- type MintedCredential
- type OAuthBeginResult
- type OAuthIdentity
- type OAuthLoginParams
- type OAuthOneTimeCodeRecord
- type PasskeyChallengeRecord
- type PasskeyCredRecord
- type PasskeyInfo
- type PasswordResetToken
- type PhoneVerificationCodeRecord
- type PlatformAdmin
- type PlatformAdminStore
- type PollQrResult
- type ProfileService
- func (s *ProfileService) ChangePassword(ctx context.Context, userID, currentPassword, newPassword string) error
- func (s *ProfileService) DeletePasskey(ctx context.Context, userID, credentialID string) error
- func (s *ProfileService) ListAuditEvents(ctx context.Context, actorID, targetID, eventType string, ...) ([]*AuditEvent, string, error)
- func (s *ProfileService) ListLinkedIdentities(ctx context.Context, userID string) ([]*OAuthIdentity, error)
- func (s *ProfileService) ListMyPasskeys(ctx context.Context, userID string) ([]*PasskeyInfo, error)
- func (s *ProfileService) ListMySessions(ctx context.Context, userID string) ([]*Session, error)
- func (s *ProfileService) RevokeAllSessions(ctx context.Context, userID, password string) (int, error)
- func (s *ProfileService) RevokeSession(ctx context.Context, userID, sessionID string) error
- func (s *ProfileService) UnlinkIdentity(ctx context.Context, userID, provider, providerUserID string) error
- func (s *ProfileService) UpdateProfile(ctx context.Context, userID, name, avatarURL string) (*User, error)
- func (s *ProfileService) WithMinorDataMinimizer(m MinorDataMinimizer) *ProfileService
- type ProjectBrandingConfig
- type ProjectCORSConfig
- type ProjectConfig
- type ProjectLoginConfig
- type ProjectPasskeyConfig
- type ProjectResolver
- type ProjectScope
- type QrLoginSessionRecord
- type QrSessionInfo
- type RecoveryCodeRecord
- type RefreshTokenRecord
- type RegisteredAuthDomain
- type Repository
- type ResetPasswordResult
- type ResolvedProject
- type ReturnAllowlist
- type Session
- type SessionRecord
- type StubDB
- func (StubDB) ExecuteAtomic(context.Context, string, string, []graph.Operation) (*graph.CommitResult, error)
- func (StubDB) GetEdgesFrom(context.Context, string, string, string, int) ([]*graph.Edge, error)
- func (StubDB) GetEdgesTo(context.Context, string, string, string, int) ([]*graph.Edge, error)
- func (StubDB) GetNode(context.Context, string, string, int, string) (*graph.Node, error)
- func (StubDB) QueryNodes(context.Context, string, string, int, map[string]any) ([]*graph.Node, error)
- func (StubDB) RegisterUserInTenant(context.Context, string, string, string, string, string) error
- func (StubDB) SearchNodes(context.Context, string, string, int, string) ([]*graph.Node, error)
- type StubRepository
- func (StubRepository) ConsumeEmailLoginCode(context.Context, string, int64) (*EmailLoginCodeRecord, error)
- func (StubRepository) ConsumeMagicLinkToken(context.Context, string, int64) (*MagicLinkTokenRecord, error)
- func (StubRepository) ConsumeOAuthOneTimeCode(context.Context, string, int64) (*OAuthOneTimeCodeRecord, error)
- func (StubRepository) ConsumePhoneVerificationCode(context.Context, string, int64) (*PhoneVerificationCodeRecord, error)
- func (StubRepository) ConsumeQrLoginSession(context.Context, string, int64) error
- func (StubRepository) ConsumeRefreshTokenByHash(context.Context, string, int64) error
- func (StubRepository) CreateEmailChangeToken(context.Context, *EmailChangeToken) error
- func (StubRepository) CreateEmailVerificationToken(context.Context, *EmailVerificationToken) error
- func (StubRepository) CreateIdentityVerification(context.Context, *IdentityVerificationRecord) error
- func (StubRepository) CreateLoginChallenge(context.Context, *LoginChallengeRecord) (string, error)
- func (StubRepository) CreateMagicLinkToken(context.Context, *MagicLinkTokenRecord) (string, error)
- func (StubRepository) CreateOAuthIdentity(context.Context, *OAuthIdentity) error
- func (StubRepository) CreateOAuthOneTimeCode(context.Context, *OAuthOneTimeCodeRecord) (string, error)
- func (StubRepository) CreatePasskeyChallenge(context.Context, *PasskeyChallengeRecord) (string, error)
- func (StubRepository) CreatePasskeyCredential(context.Context, *PasskeyCredRecord) (string, error)
- func (StubRepository) CreatePasswordResetToken(context.Context, *PasswordResetToken) error
- func (StubRepository) CreateQrLoginSession(context.Context, *QrLoginSessionRecord) (string, error)
- func (StubRepository) CreateRecoveryCode(context.Context, *RecoveryCodeRecord) (string, error)
- func (StubRepository) CreateRefreshToken(context.Context, *RefreshTokenRecord) (string, error)
- func (StubRepository) CreateSession(context.Context, *SessionRecord) (string, error)
- func (StubRepository) CreateTotpCredential(context.Context, *TotpCredRecord) (string, error)
- func (StubRepository) CreateUser(context.Context, *User) (string, error)
- func (StubRepository) DeleteExpiredEmailChangeTokens(context.Context, int64, int) error
- func (StubRepository) DeleteExpiredEmailLoginCodes(context.Context, int64, int) error
- func (StubRepository) DeleteExpiredEmailVerificationTokens(context.Context, int64, int) error
- func (StubRepository) DeleteExpiredInvitations(context.Context, int64, int) error
- func (StubRepository) DeleteExpiredLoginChallenges(context.Context, int64, int) error
- func (StubRepository) DeleteExpiredMagicLinkTokens(context.Context, int64, int) error
- func (StubRepository) DeleteExpiredOAuthOneTimeCodes(context.Context, int64, int) error
- func (StubRepository) DeleteExpiredPasswordResetTokens(context.Context, int64, int) error
- func (StubRepository) DeleteExpiredPhoneVerificationCodes(context.Context, int64, int) error
- func (StubRepository) DeleteExpiredQrLoginSessions(context.Context, int64, int) error
- func (StubRepository) DeleteExpiredWebAuthnChallenges(context.Context, int64, int) error
- func (StubRepository) DeleteLoginChallenge(context.Context, string) error
- func (StubRepository) DeleteOAuthIdentity(context.Context, string, string, string) error
- func (StubRepository) DeletePasskeyChallenge(context.Context, string) error
- func (StubRepository) DeleteRecoveryCodesForUser(context.Context, string) error
- func (StubRepository) DeleteRefreshToken(context.Context, string) error
- func (StubRepository) DeleteRefreshTokensForUser(context.Context, string) error
- func (StubRepository) DeleteTotpCredential(context.Context, string) error
- func (StubRepository) DeleteTotpCredentialsForUser(context.Context, string) error
- func (StubRepository) DeleteUser(context.Context, string) error
- func (StubRepository) FindEmailChangeTokenByHash(context.Context, string) (*EmailChangeToken, error)
- func (StubRepository) FindEmailLoginCodeByEmail(context.Context, string) (*EmailLoginCodeRecord, error)
- func (StubRepository) FindEmailVerificationTokenByHash(context.Context, string) (*EmailVerificationToken, error)
- func (StubRepository) FindInvitationByHash(context.Context, string) (*InvitationRecord, error)
- func (StubRepository) FindPasswordResetTokenByHash(context.Context, string) (*PasswordResetToken, error)
- func (StubRepository) FindPhoneVerificationCodeByUser(context.Context, string) (*PhoneVerificationCodeRecord, error)
- func (StubRepository) FindQrLoginSession(context.Context, string) (*QrLoginSessionRecord, error)
- func (StubRepository) FindRecoveryCodeByHash(context.Context, string, string) (*RecoveryCodeRecord, error)
- func (StubRepository) FindRefreshTokenByHash(context.Context, string) (*RefreshTokenRecord, error)
- func (StubRepository) FindRefreshTokenByHashIncludingConsumed(context.Context, string) (*RefreshTokenRecord, error)
- func (StubRepository) FindUserByEmail(context.Context, string) (*User, error)
- func (StubRepository) FindUserByProviderID(context.Context, string, string) (*User, error)
- func (StubRepository) GetIdentityVerification(context.Context, string) (*IdentityVerificationRecord, error)
- func (StubRepository) GetLatestIdentityVerificationForUser(context.Context, string) (*IdentityVerificationRecord, error)
- func (StubRepository) GetLoginChallengeByChallengeID(context.Context, string) (*LoginChallengeRecord, error)
- func (StubRepository) GetPasskeyChallenge(context.Context, string) (*PasskeyChallengeRecord, error)
- func (StubRepository) GetPasskeyCredentialByCredID(context.Context, string) (*PasskeyCredRecord, error)
- func (StubRepository) GetSessionBySid(context.Context, string) (*SessionRecord, error)
- func (StubRepository) GetTotpCredential(context.Context, string) (*TotpCredRecord, error)
- func (StubRepository) GetUser(context.Context, string) (*User, error)
- func (StubRepository) IncrementEmailLoginCodeAttempts(context.Context, string) error
- func (StubRepository) IncrementFailedLoginCount(context.Context, string) (int32, error)
- func (StubRepository) IncrementPhoneVerificationCodeAttempts(context.Context, string) error
- func (StubRepository) ListOAuthIdentitiesForUser(context.Context, string) ([]*OAuthIdentity, error)
- func (StubRepository) ListPasskeyCredentials(context.Context, string) ([]*PasskeyCredRecord, error)
- func (StubRepository) MarkEmailChangeTokenConsumed(context.Context, string, int64) error
- func (StubRepository) MarkEmailVerificationTokenConsumed(context.Context, string, int64) error
- func (StubRepository) MarkPasswordResetTokenConsumed(context.Context, string, int64) error
- func (StubRepository) ResetFailedLoginCount(context.Context, string) error
- func (StubRepository) RevokeSession(context.Context, string, int64) error
- func (StubRepository) RevokeSessionsForUser(context.Context, string, int64) error
- func (StubRepository) SetUserEmailVerified(context.Context, string, int64) error
- func (StubRepository) SetUserIDVVerified(context.Context, string, int64) error
- func (StubRepository) SetUserLockedUntil(context.Context, string, int64) error
- func (StubRepository) SetUserPhoneVerified(context.Context, string, string, int64) error
- func (StubRepository) UpdateIdentityVerificationStatus(context.Context, string, string, string, int64, int64) error
- func (StubRepository) UpdateInvitation(context.Context, string, map[string]any) error
- func (StubRepository) UpdatePasskeyCredential(context.Context, string, map[string]any) error
- func (StubRepository) UpdateQrLoginSession(context.Context, string, map[string]any) error
- func (StubRepository) UpdateRecoveryCode(context.Context, string, map[string]any) error
- func (StubRepository) UpdateTotpCredential(context.Context, string, map[string]any) error
- func (StubRepository) UpdateUser(context.Context, string, map[string]any) error
- func (StubRepository) UpdateUserEmail(context.Context, string, string, int64) error
- func (StubRepository) UpsertEmailLoginCode(context.Context, *EmailLoginCodeRecord) (string, error)
- func (StubRepository) UpsertPhoneVerificationCode(context.Context, *PhoneVerificationCodeRecord) (string, error)
- type Tenant
- type TenantAutoFormStore
- type TenantInvitation
- type TenantMembership
- type TenantStore
- type TotpCredRecord
- type User
- type UserDirectory
Constants ¶
const ( CredentialKindPublishable = "publishable" CredentialKindSecret = "secret" )
Credential kinds an operator may mint. "publishable" is a public lookup key with no secret half; "secret" carries a secret shown exactly once.
const ( IDVStatusPending = "pending" IDVStatusInReview = "in_review" IDVStatusApproved = "approved" IDVStatusRejected = "rejected" IDVStatusExpired = "expired" )
Identity-verification status string constants. Mirrored as proto enum values in IdentityVerificationStatus.
const ( LoginMethodEmailOTP = "email_otp" LoginMethodPassword = "password" LoginMethodOAuth = "oauth" LoginMethodPasskey = "passkey" LoginMethodSSO = "sso" )
Login method tokens used in LoginPolicy.AllowedMethods (a comma-separated list). Empty AllowedMethods means "no restriction" — the caller falls back to its safe default rather than locking the tenant out.
const ( MembershipSourceDomain = "domain" // derived from a verified email domain MembershipSourceInvited = "invited" // accepted an invitation MembershipSourceAdded = "added" // added by a tenant admin )
Membership source — how the row came to exist.
const ( RoleMember = "member" RoleAdmin = "admin" RoleOwner = "owner" )
Membership / invitation role.
const ( MembershipStatusActive = "active" MembershipStatusPending = "pending" MembershipStatusInactive = "inactive" )
Membership status.
const ( InvitationStatusPending = "pending" InvitationStatusAccepted = "accepted" InvitationStatusRevoked = "revoked" InvitationStatusExpired = "expired" )
Invitation status.
const ( PlatformAdminStatusActive = "active" PlatformAdminStatusSuspended = "suspended" )
PlatformAdmin status — an active operator may sign in; a suspended one is retained for audit but cannot.
const ( // TenantStatusLatent is a tenant auto-formed from a user's email domain // that has not yet had that domain verified. It governs nothing until // claimed. TenantStatusLatent = "latent" // TenantStatusClaimed is a tenant whose domain has been verified; its // login policy and membership are now authoritative. TenantStatusClaimed = "claimed" // TenantStatusSuspended is an administratively disabled tenant. TenantStatusSuspended = "suspended" )
Tenant status values.
const ( DomainStatusPending = "pending" DomainStatusVerified = "verified" DomainStatusFailed = "failed" )
Domain status values.
const ( DomainVerificationDNSTXT = "dns_txt" DomainVerificationEmail = "email" )
Domain verification methods.
const StatusPendingParentalConsent = "pending_parental_consent"
StatusPendingParentalConsent is the user status for a child-band account created under age-gating that has not yet obtained verifiable parental consent. Such an account exists but cannot be issued access tokens.
Variables ¶
var ( ErrUnauthenticated = errors.New("unauthenticated") ErrPermissionDenied = errors.New("permission denied") ErrInvalidArgument = errors.New("invalid argument") ErrNotFound = errors.New("not found") ErrAlreadyExists = errors.New("already exists") ErrAccountLocked = errors.New("account locked") ErrNoPasswordSet = errors.New("no password set for this account") ErrAccountNotActive = errors.New("account is not active") ErrInvitationPending = errors.New("account has not completed invitation") ErrIDVRequired = errors.New("identity verification required") // ErrEmailVerificationRequired is returned when GATEWAY_AUTH_REQUIRE_VERIFIED_EMAIL // is enabled and the account's email is not yet verified. Like ErrIDVRequired // it is a "do something else first" precondition (verify your email, then // retry), mapped to CodeFailedPrecondition by the Connect layer. ErrEmailVerificationRequired = errors.New("email verification required") // ErrMinorDataMinimized is returned when GATEWAY_MINOR_DATA_MINIMIZATION is // enabled and a CHILD-band account attempts an RPC that would collect // non-essential PII the server refuses to gather from a minor — phone // verification or identity verification. Like ErrIDVRequired it is a // "this is not permitted for this account" precondition, mapped to // CodeFailedPrecondition by the Connect layer. ErrMinorDataMinimized = errors.New("data collection not permitted for a minor account") // ErrParentalConsentRequired is returned when an admin status mutator // (e.g. ReactivateUser) attempts to move an account out of // pending_parental_consent. The only valid transition out of that state // is the dedicated parental-consent flow; ordinary status patches must // not silently bypass the COPPA consent gate. ErrParentalConsentRequired = errors.New("account is pending parental consent and cannot be activated by this operation") ErrWeakPassword = errors.New("password does not meet strength requirements") ErrTotpRequired = errors.New("totp required") // ErrSSORequired is returned when a claimed tenant's LoginPolicy mandates // single sign-on and the caller attempted a non-SSO method. Like // ErrTotpRequired it is a "do something else first" signal rather than a // hard failure, so the Connect handler maps it to CodeFailedPrecondition, // steering the client to the tenant's SSO connection. ErrSSORequired = errors.New("sso required for this domain") ErrTokenExpired = errors.New("token expired") ErrInvalidTotpCode = errors.New("invalid totp code") ErrQrLoginExpired = errors.New("qr login session expired") ErrQrLoginNotPending = errors.New("qr login session is not pending") // ErrOAuthCodeInvalid is returned when a hosted-flow one-time code is // missing, expired, or already consumed. The Connect handler maps it // to CodeUnauthenticated so replays and expiries look identical to a // brute-force attacker. ErrOAuthCodeInvalid = errors.New("oauth one-time code is invalid or already used") // ErrEmailLoginCodeInvalid is returned when a passwordless OTP is // missing, expired, already consumed, the wrong code, or has exhausted // its attempt budget. The Connect handler maps it to // CodeUnauthenticated so all failure modes look identical to a // brute-force attacker. ErrEmailLoginCodeInvalid = errors.New("email login code is invalid or expired") // ErrMagicLinkInvalid is returned when a passwordless magic-link token // is missing, expired, or already consumed. Maps to CodeUnauthenticated. ErrMagicLinkInvalid = errors.New("magic link is invalid or already used") // ErrPhoneCodeInvalid is returned when an SMS-OTP phone-verification // code is missing, expired, already consumed, the wrong code, or has // exhausted its attempt budget. The Connect handler maps it to // CodeUnauthenticated so all failure modes look identical to a // brute-force attacker. ErrPhoneCodeInvalid = errors.New("phone verification code is invalid or expired") // ErrSMSDisabled is returned by the phone-verification RPCs when // GATEWAY_SMS_ENABLED is false. Maps to CodeUnavailable. ErrSMSDisabled = errors.New("sms phone verification is not configured") // ErrPhoneAlreadyVerified is returned by RequestPhoneVerification when // the caller has already verified the same number. Maps to // CodeAlreadyExists. ErrPhoneAlreadyVerified = errors.New("phone number is already verified") ErrInvitationUsed = errors.New("invitation has already been accepted") ErrInvitationExpired = errors.New("invitation has expired") ErrLocalAuthDisabled = errors.New("local auth disabled") ErrOAuthDisabled = errors.New("oauth login is not configured") ErrSignupDisabled = errors.New("signup is disabled for this deployment") // ErrCaptchaRequired is returned when CAPTCHA is enforced on an // endpoint but the request carried no captcha token. ErrCaptchaFailed // is returned when the supplied token was rejected by the provider. // Both map to CodePermissionDenied so a forged token and a missing one // look the same to a client. ErrCaptchaRequired = errors.New("captcha token required") ErrCaptchaFailed = errors.New("captcha verification failed") // ErrUnimplemented signals that the requested RPC is intentionally // disabled for the active repository driver (e.g. the redesign // Domain/Tenant RPCs are postgres-only; memory returns this). // The Connect handler layer maps it to CodeUnimplemented. ErrUnimplemented = errors.New("operation unimplemented for this repository driver") // ErrLastOwner is returned by RemoveTenantMember when removing the target // would strand the tenant with no active owner. The caller is permitted // to remove members (so PermissionDenied is wrong); this is a state // precondition, mapped to CodeFailedPrecondition. ErrLastOwner = errors.New("cannot remove the last owner of a tenant") // ErrPlatformAdminExists is returned by CreateFirstPlatformAdmin once any // platform admin already exists: the zero-config bootstrap is a one-time // path that permanently closes after the first admin is created, so a // later call cannot escalate to operator. It is a state precondition (not // an authorization failure), mapped to CodeFailedPrecondition. ErrPlatformAdminExists = errors.New("a platform admin already exists; bootstrap is closed") // ErrAuthDomainNotVerified is returned by SetPrimaryAuthDomain when the // target custom auth-domain has not proven ownership (verified_at_ms == 0). // Only a DNS-verified domain may be promoted to a project's primary serving // host, so this is a state precondition mapped to CodeFailedPrecondition. ErrAuthDomainNotVerified = errors.New("auth domain is not verified") // ErrLastCredential is returned by UnlinkIdentity when removing the // requested provider link would leave the user with no remaining way to // sign in (no password, no passkey, and no other linked provider). The // caller is allowed to unlink their own identities, so this is a state // precondition (not an authorization failure), mapped to // CodeFailedPrecondition. ErrLastCredential = errors.New("cannot remove the last sign-in credential") )
ErrServiceUnavailable is returned by stub implementations.
var ErrSweepNotImplemented = errors.New("identity: sweep not implemented for this backend")
ErrSweepNotImplemented is the soft-skip sentinel a Repository may return from a DeleteExpired* method when the backend cannot yet run the expired-row sweep. The sweeper goroutine in internal/app/sweeper.go logs this once per node type per process and continues. No backend in tree returns this today; it remains so a new backend can land its CRUD methods first and its sweep in a follow-up PR without erroring the sweeper goroutine.
Functions ¶
func BuildAgeGate ¶ added in v1.4.0
BuildAgeGate selects the age-determination provider from config. When age-gating is off the no-op determiner is returned (everyone is an adult). When on, the threshold determiner is built from the configured boundaries; config.Validate already guarantees they are well-formed, but if a caller bypassed validation we fail safe to the no-op rather than panic.
func WithProjectScope ¶ added in v0.16.0
func WithProjectScope(ctx context.Context, scope *ProjectScope) context.Context
WithProjectScope returns a child context carrying scope. The project-resolution middleware calls this once it has resolved the request's project. A nil scope is a no-op so callers need not branch.
Types ¶
type AdminProject ¶ added in v0.19.0
AdminProject is the control-plane project row an operator creates. It is a driver-agnostic value type so the admin service and its tests depend on a contract, not the concrete postgres store type.
type AdminProjectAuthDomain ¶ added in v1.1.0
AdminProjectAuthDomain is a project's serving hostname as the admin service reads it back. VerifiedAtMs is 0 until ownership is proven; a domain with 0 does NOT resolve requests.
type AdminProjectCredential ¶ added in v0.19.0
type AdminProjectCredential struct {
ID string
ProjectID string
Kind string
PublicID string
SecretHash string
}
AdminProjectCredential is the credential row an operator mints. Only the hash of the secret is persisted; the raw secret never round-trips through the store.
type AdminService ¶
type AdminService struct {
// contains filtered or unexported fields
}
AdminService implements admin user-management operations. All methods verify that the acting user has role=admin.
func NewAdminService ¶
func NewAdminService(repo Repository, db DB, projectID string, auditLog *audit.Logger, cfg *config.Config, mailer email.Transport, logger *zap.Logger) *AdminService
NewAdminService creates an AdminService.
The repo handle backs the cascade-aware paths (DeleteUser, and the session/refresh-token revocation done by DeactivateUser); the DB handle backs the admin-authorization read and the existing invite/deactivate/reactivate/reset graph writes. This mirrors the dual-handle ProfileService(repo, db, …) pattern.
mailer may be nil; if nil, a log-only transport is substituted so invitation emails are at least visible in the logs during local dev. Email-side-effect failures never block the surrounding RPC.
func (*AdminService) DeactivateUser ¶
func (s *AdminService) DeactivateUser(ctx context.Context, actorID, targetUserID, reason string) error
DeactivateUser marks a user as deactivated, then revokes their active sessions and deletes their refresh tokens so the suspension takes effect immediately rather than at the next token's natural expiry. The user row is retained (reversible via ReactivateUser).
func (*AdminService) DeleteUser ¶ added in v0.13.0
func (s *AdminService) DeleteUser(ctx context.Context, actorID, targetUserID string) error
DeleteUser physically removes a user and cascades all user-owned records (sessions, refresh/login challenges, passkeys, totp, recovery codes, oauth identities, qr sessions, one-time codes, idv records, invitations, and the password/email-verification/ email-change tokens), plus the user's group MEMBER_OF edges. After it, GetUser returns NotFound and the email is reusable. Audit events are retained for accountability.
Active sessions and refresh tokens are revoked BEFORE the cascade so any in-flight access token is dead immediately rather than at its natural expiry.
func (*AdminService) InviteUser ¶
func (s *AdminService) InviteUser( ctx context.Context, actorID, email, name, role, recoveryEmail string, quotaBytes int64, createImmediately bool, ) (*InviteResult, error)
InviteUser creates a new user (invited or immediately active) and returns the user, an invitation token, setup URL, and optional temporary password.
func (*AdminService) ListUsers ¶
func (s *AdminService) ListUsers( ctx context.Context, actorID, statusFilter, search, cursor string, limit int, ) ([]*User, string, int, error)
ListUsers returns a paginated list of users, optionally filtered by status and/or search substring.
func (*AdminService) ReactivateUser ¶
func (s *AdminService) ReactivateUser(ctx context.Context, actorID, targetUserID string) error
ReactivateUser sets a deactivated user back to active.
func (*AdminService) ResetUserPassword ¶
func (s *AdminService) ResetUserPassword( ctx context.Context, actorID, targetUserID string, generateTemp bool, ) (*ResetPasswordResult, error)
ResetUserPassword generates a temp password or reset token for a user.
func (*AdminService) SetUserQuota ¶
func (s *AdminService) SetUserQuota(ctx context.Context, actorID, targetUserID string, quotaBytes int64) error
SetUserQuota updates the storage quota for a user.
func (*AdminService) UpdateUser ¶
func (s *AdminService) UpdateUser(ctx context.Context, actorID, userID, name, role, avatarURL string) (*User, error)
UpdateUser patches name, role, and/or avatar_url for a user.
type AuditEvent ¶
type AuditEvent struct {
ID string
EventType string
ActorUserID string
TargetUserID string
IPAddress string
UserAgent string
Success bool
Details map[string]any
CreatedAt int64
}
AuditEvent is the domain representation of an audit event (type_id 26).
type AuthService ¶
type AuthService struct {
// contains filtered or unexported fields
}
AuthService implements authentication and token management business logic.
func NewAuthService ¶
func NewAuthService( repo Repository, cfg *config.Config, signer jwt.Signer, passkeysSvc *passkeys.WebAuthnService, auditLogger *audit.Logger, totpKey []byte, totpRecoveryPepper []byte, mailer email.Transport, smsSender sms.Sender, logger *zap.Logger, ) *AuthService
NewAuthService creates an AuthService with all required dependencies.
mailer may be nil; if nil, a non-delivering log-only transport is substituted so service code can always call s.mailer.Send without a nil check. Email-side-effect failures are logged and do NOT fail the surrounding RPC.
func NewAuthServiceWithOAuth ¶
func NewAuthServiceWithOAuth( repo Repository, cfg *config.Config, signer jwt.Signer, passkeysSvc *passkeys.WebAuthnService, auditLogger *audit.Logger, totpKey []byte, totpRecoveryPepper []byte, mailer email.Transport, smsSender sms.Sender, logger *zap.Logger, oauthRegistry *oauth.Registry, ) *AuthService
NewAuthServiceWithOAuth is the extended constructor that injects an oauth.Registry. Pass nil to disable OAuth login.
func (*AuthService) AcceptInvitation ¶
func (s *AuthService) AcceptInvitation(ctx context.Context, invitationToken, password, name, ipAddr, userAgent string) (*LoginResult, error)
AcceptInvitation completes an admin-issued invitation.
func (*AuthService) ApproveQrLogin ¶
func (s *AuthService) ApproveQrLogin(ctx context.Context, sessionID string, approve bool, userID, userAgent string) (string, error)
ApproveQrLogin approves or rejects a QR login session. Returns the new status.
func (*AuthService) BeginHostedOAuth ¶ added in v0.9.0
func (s *AuthService) BeginHostedOAuth( ctx context.Context, provider, redirectURI, returnTo, csrfToken string, ) (*HostedOAuthBeginResult, error)
BeginHostedOAuth mints state + PKCE for the hosted flow and returns the provider authorization URL. redirectURI is the identity-owned callback (e.g. https://identity.example.com/oauth/callback/google); returnTo is the already-allowlist-validated app URL the callback will redirect back to. It reuses the same provider Authorizer the headless BeginOAuthLogin uses — there is no forked authorization path.
func (*AuthService) BeginOAuthLogin ¶
func (s *AuthService) BeginOAuthLogin( ctx context.Context, provider, redirectURI string, ) (*OAuthBeginResult, error)
BeginOAuthLogin returns a provider authorization URL plus the server-minted state artifacts needed to complete the callback safely.
func (*AuthService) BeginPasskeyLogin ¶
BeginPasskeyLogin generates WebAuthn authentication options. If email is provided, scopes to that user's credentials. Otherwise allows discoverable credentials (usernameless flow).
func (*AuthService) BeginPasskeyRegistration ¶
func (s *AuthService) BeginPasskeyRegistration(ctx context.Context, userID, deviceName string) (string, string, error)
BeginPasskeyRegistration generates WebAuthn registration options for the authenticated user. Returns (optionsJSON, challengeID, error).
func (*AuthService) BeginTotpSetup ¶
func (s *AuthService) BeginTotpSetup(ctx context.Context, userID string) (string, string, []string, error)
BeginTotpSetup starts TOTP enrollment for the authenticated user. Returns (secret, qrURI, recoveryCodes, error).
func (*AuthService) CompleteHostedOAuth ¶ added in v0.9.0
func (s *AuthService) CompleteHostedOAuth( ctx context.Context, providerFromPath, code, stateToken, appleUserPayload, ipAddr, userAgent string, csrfTokens []string, ) (*HostedOAuthCallbackResult, error)
CompleteHostedOAuth runs the hosted callback: it verifies the signed hosted state token (recovering provider + PKCE verifier + return_to), runs the same OAuthLogin exchange the headless flow uses, then mints a single-use one-time code bound to the authenticated user. The caller (the HTTP handler) 302-redirects to result.ReturnTo?code=result.Code.
stateToken is the OAuth `state` value the provider echoed back; providerFromPath is the provider segment from the callback path, used only to cross-check the token's provider claim.
func (*AuthService) CompletePasskeyLogin ¶
func (s *AuthService) CompletePasskeyLogin(ctx context.Context, challengeID, credentialJSON, ipAddr, userAgent string) (*LoginResult, error)
CompletePasskeyLogin verifies the assertion response and issues tokens.
func (*AuthService) CompletePasskeyRegistration ¶
func (s *AuthService) CompletePasskeyRegistration(ctx context.Context, userID, challengeID, credentialJSON, deviceName string) (*PasskeyInfo, error)
CompletePasskeyRegistration verifies the attestation response and stores the new credential. Returns (credentialInfo, error).
func (*AuthService) ConfirmEmailChange ¶
ConfirmEmailChange consumes a pending EmailChangeToken and swaps the user's primary email to the verified new address. Token must be unconsumed and unexpired. On success:
- user.email is updated to new_email
- user.email_verified is set to true (verified by clicking the link)
- user.email_verified_at is updated
- the token is marked consumed
- all of the user's refresh tokens are revoked (OAuth 2.1 §4.13: credential changes force re-auth on every device)
If the new address has been claimed by another user since the request, returns ErrAlreadyExists — the token is NOT consumed in that case so the user can call ConfirmEmailChange again if the conflict resolves before the token expires.
func (*AuthService) ConfirmPasswordReset ¶
func (s *AuthService) ConfirmPasswordReset(ctx context.Context, token, newPassword string) error
ConfirmPasswordReset consumes a password-reset token and sets the user's new password.
Token must be unconsumed and unexpired. On success, every refresh token belonging to the user is revoked — OAuth 2.1 §4.13 best practice for any credential change forces re-login on all devices.
func (*AuthService) DisableTotp ¶
func (s *AuthService) DisableTotp(ctx context.Context, userID, password string) error
DisableTotp disables 2FA for the authenticated user. Requires password confirmation.
func (*AuthService) GetCurrentUser ¶
GetCurrentUser returns the user record for the given user ID.
func (*AuthService) GetQrLoginSession ¶
func (s *AuthService) GetQrLoginSession(ctx context.Context, sessionID string) (*QrSessionInfo, error)
GetQrLoginSession returns display-safe details of a QR login session.
func (*AuthService) InitiateQrLogin ¶
func (s *AuthService) InitiateQrLogin(ctx context.Context, deviceInfo, userAgent, ipAddr string) (*InitiateQrLoginResult, error)
InitiateQrLogin creates a new QR login session for an unauthenticated device. The returned PollSecret is shown only to the initiating device and must be presented on every PollQrLogin call; it is NEVER embedded in the QR URL.
func (*AuthService) LinkIdentity ¶ added in v1.3.0
func (s *AuthService) LinkIdentity( ctx context.Context, userID, code, provider, redirectURI, codeVerifier, state, stateToken string, ) (*OAuthIdentity, error)
LinkIdentity attaches a freshly-verified OAuth identity to an already authenticated user. The server performs the provider code exchange itself (the client is never trusted to assert the identity), exactly as OAuthLogin does, then persists the (provider, provider_user_id) link against userID.
It differs from login-time auto-linking in two ways: it targets the CURRENTLY AUTHENTICATED user rather than resolving a user from the provider's email, and it surfaces a hard error (rather than best-effort logging) so the caller learns whether the link was created. If the provider identity is already linked — to this user or another — it returns ErrAlreadyExists; the caller must not be able to steal another account's provider identity, and a no-op re-link should not look like success.
func (*AuthService) Logout ¶
func (s *AuthService) Logout(ctx context.Context, rawRefreshToken string) error
Logout deletes the refresh token identified by the raw token value.
func (*AuthService) OAuthLogin ¶
func (s *AuthService) OAuthLogin( ctx context.Context, params OAuthLoginParams, ) (*LoginResult, error)
OAuthLogin performs the full OAuth code-exchange flow: it looks up the registered Exchanger for the provider, swaps the code for a verified Identity, then upserts the local user and issues tokens.
The frontend / gateway is NOT trusted to validate the user's identity; identity does the exchange itself. Provider access / refresh tokens are discarded — they are not persisted.
func (*AuthService) PasswordLogin ¶
func (s *AuthService) PasswordLogin(ctx context.Context, email, password, ipAddr, userAgent string) (*LoginResult, error)
PasswordLogin authenticates a user with email + password. If TOTP is enabled, returns TotpRequired=true with a LoginChallengeID.
func (*AuthService) PasswordSignup ¶
func (s *AuthService) PasswordSignup(ctx context.Context, email, password, name, recoveryEmail string, dateOfBirthMs int64) (*LoginResult, error)
PasswordSignup creates a new user with email + password and issues tokens.
func (*AuthService) PollQrLogin ¶
func (s *AuthService) PollQrLogin(ctx context.Context, sessionID, pollSecret, ipAddr, userAgent string) (*PollQrResult, error)
PollQrLogin polls a QR login session. When approved, atomically consumes the session and issues tokens. Returns (status, user, accessToken, refreshToken, error). pollSecret must match the value returned by InitiateQrLogin; otherwise the session appears "expired" to the caller — a stolen QR URL alone is useless.
Multi-replica correctness: the approved→consumed transition runs through the repository's ConsumeQrLoginSession compare-and-set primitive BEFORE tokens are minted, so only one of N concurrent pollers against the same approved session can complete the flow. The loser sees status="consumed" on the next poll cycle.
func (*AuthService) RedeemMagicLink ¶ added in v0.11.0
func (s *AuthService) RedeemMagicLink(ctx context.Context, token, ipAddr, userAgent string) (*MagicLinkResult, error)
RedeemMagicLink consumes the single-use token, resolves-or-creates the user keyed by the bound email, and issues a token pair. A replay, an expired token, or an unknown token all return ErrMagicLinkInvalid.
func (*AuthService) RedeemOAuthCode ¶ added in v0.9.0
func (s *AuthService) RedeemOAuthCode(ctx context.Context, code, ipAddr, userAgent string) (*LoginResult, error)
RedeemOAuthCode exchanges the single-use hosted-flow code for a fresh token pair. The repository's ConsumeOAuthOneTimeCode is the serialization point: it atomically consumes the code (single winner across replicas) and returns the bound user, after which tokens are minted via the same issueTokens path every other login uses. A replay, an expired code, or an unknown code all return ErrOAuthCodeInvalid.
func (*AuthService) RefreshToken ¶
func (s *AuthService) RefreshToken(ctx context.Context, rawRefreshToken, ipAddr, userAgent string) (*User, string, string, error)
RefreshToken validates a refresh token, rotates it, and returns new tokens.
Replay detection is durable: rotated tokens are kept in the repository with consumed_at != 0 instead of being deleted, so any instance — and any process restart — can detect a stolen-token replay (OAuth 2.1 §4.13). When replay is detected, ALL of the user's refresh tokens are hard-deleted to bound the blast radius.
Concurrency: the repository's ConsumeRefreshTokenByHash is the serialization point. Two goroutines presenting the same refresh token race to consume it; the loser sees ErrUnauthenticated and the row stays consumed exactly once.
Background sweep: rows whose ConsumedAtMs is older than the desired retention window (e.g. 90 days) may be hard-deleted by a periodic job. That sweep is intentionally NOT implemented here.
func (*AuthService) RegenerateRecoveryCodes ¶
func (s *AuthService) RegenerateRecoveryCodes(ctx context.Context, userID, password string) ([]string, error)
RegenerateRecoveryCodes issues a fresh batch of recovery codes. Requires password confirmation and TOTP to be enabled.
func (*AuthService) RequestEmailChange ¶
func (s *AuthService) RequestEmailChange(ctx context.Context, userID, newEmail, currentPassword string) error
RequestEmailChange initiates a primary-email rotation for the user.
The caller must re-authenticate with their current password (OAuth 2.1 best practice for any high-value credential change). The new address is validated and checked for uniqueness; an EmailChangeToken is created and two emails are dispatched:
- to the NEW address: a verification link the user must click to complete the change.
- to the OLD address: a security notice informing the user that a change has been requested.
The email swap does NOT take effect until ConfirmEmailChange consumes the token. If another user claims the new_email between this call and the confirm call, the confirm call will fail with ErrAlreadyExists (last-write-wins is rejected — the contended new_email is still owned by the other user, so we cannot reassign it).
func (*AuthService) RequestEmailLoginCode ¶ added in v0.11.0
func (s *AuthService) RequestEmailLoginCode(ctx context.Context, emailAddr string) error
RequestEmailLoginCode mints a 6-digit OTP for the email and dispatches it. Anti-enumeration: it always returns nil with no observable difference between a known and an unknown address. The account is NOT created here — only VerifyEmailLoginCode resolves or creates the user.
func (*AuthService) RequestMagicLink ¶ added in v0.11.0
func (s *AuthService) RequestMagicLink(ctx context.Context, emailAddr, returnTo string) error
RequestMagicLink mints a single-use magic-link token bound to the email and the allowlist-validated return_to, then emails the link. Anti- enumeration: identical response regardless of account existence.
return_to is validated against GATEWAY_OAUTH_ALLOWED_RETURN_URLS (shared with hosted OAuth). A disallowed return_to is the one hard error the caller sees — it is a client misconfiguration, not an account probe, and failing closed here prevents an open redirect. An empty allowlist (the feature is unconfigured) rejects every return_to the same way.
func (*AuthService) RequestPasswordReset ¶
func (s *AuthService) RequestPasswordReset(ctx context.Context, emailAddr string) error
RequestPasswordReset creates a password-reset token for the user matching the supplied email and dispatches a reset email.
Per OWASP guidance and the proto contract, this method always returns nil even when the email is unknown — the response time is also kept roughly equivalent so the endpoint cannot be used as an email-enumeration oracle. Errors during token persistence or email dispatch are logged internally; the caller is told nothing.
func (*AuthService) RequestPhoneVerification ¶ added in v0.14.0
func (s *AuthService) RequestPhoneVerification(ctx context.Context, userID, phoneNumber string) error
RequestPhoneVerification mints a 6-digit OTP for the user's phone and texts it. The caller must be authenticated (userID is the verified `sub`). Returns ErrSMSDisabled when phone verification is not configured, ErrPhoneAlreadyVerified when the user has already verified the same number, and ErrInvalidArgument for a malformed number. A per-user send cooldown bounds inbox/cost abuse.
func (*AuthService) SendEmailVerification ¶
func (s *AuthService) SendEmailVerification(ctx context.Context, userID string) error
SendEmailVerification creates a verification token for the user and dispatches a verification email. Idempotent — calling it repeatedly just creates additional valid tokens (older tokens remain valid until their own expiry, on the principle that we should never invalidate a token a user might have already clicked).
func (*AuthService) VerifyEmail ¶
VerifyEmail consumes a verification token and marks the user's email as verified. Idempotent — re-verifying an already-verified user still consumes the supplied token but does not change state. Returns the updated user.
func (*AuthService) VerifyEmailLoginCode ¶ added in v0.11.0
func (s *AuthService) VerifyEmailLoginCode(ctx context.Context, emailAddr, code string, ipAddr, userAgent string) (*LoginResult, error)
VerifyEmailLoginCode validates the OTP, resolves-or-creates the user keyed by email, and issues a token pair.
Failure modes (missing/expired/consumed code, wrong code, exhausted attempts, and — when auto-create is disabled — an unknown email) all collapse to ErrEmailLoginCodeInvalid so the endpoint reveals nothing. A wrong guess bumps the per-code attempt counter; once it reaches the cap captured at mint time the code is consumed (invalidated) to stop a brute-force walk of the 6-digit space.
func (*AuthService) VerifyPhoneCode ¶ added in v0.14.0
func (s *AuthService) VerifyPhoneCode(ctx context.Context, userID, phoneNumber, code string) (*User, error)
VerifyPhoneCode validates the OTP and, on success, records the verified number on the user. The caller must be authenticated. All failure modes (missing/expired/consumed code, wrong code, exhausted attempts, or a number that does not match the one the code was minted for) collapse to ErrPhoneCodeInvalid. A wrong guess bumps the per-code attempt counter; at the cap the code is consumed so the brute-force window over the 6-digit space is bounded.
func (*AuthService) VerifyTotp ¶
func (s *AuthService) VerifyTotp(ctx context.Context, challengeID, code, ipAddr, userAgent string) (*LoginResult, error)
VerifyTotp completes a pending login by verifying a TOTP or recovery code. Returns (user, accessToken, refreshToken, error).
func (*AuthService) VerifyTotpSetup ¶
VerifyTotpSetup completes TOTP enrollment by verifying a code. Returns (verified, error).
func (*AuthService) WithLoginGovernance ¶ added in v0.17.0
func (s *AuthService) WithLoginGovernance(g *LoginGovernance) *AuthService
WithLoginGovernance wires the optional login-governance bundle (the Domain/Tenant/LoginPolicy read stores) the login path consults to enforce a claimed tenant's LoginPolicy, and returns the service for chaining. app.New calls it once at construction (with the postgres stores, or nil for drivers without a governance plane). A nil bundle disables enforcement.
func (*AuthService) WithTenantAutoFormer ¶ added in v0.17.0
func (s *AuthService) WithTenantAutoFormer(af TenantAutoFormStore) *AuthService
WithTenantAutoFormer wires the optional tenant auto-formation store and returns the service for chaining. app.New calls it once at construction (with the postgres store, or nil for drivers without a control plane).
type BeginIdentityVerificationResult ¶ added in v0.4.0
type BeginIdentityVerificationResult struct {
VerificationID string
Provider string
SessionToken string
ExpiresAt time.Time
}
BeginIdentityVerificationResult carries everything the connect handler needs to build a BeginIdentityVerificationResponse. The service layer keeps proto types out of its surface so the wiring is unit-testable without spinning up a Connect server.
type BootstrappedAdmin ¶ added in v1.1.0
BootstrappedAdmin is the result of CreateFirstPlatformAdmin: the created admin's id and canonical email, plus GeneratedPassword — a server-minted password shown EXACTLY once, and only when the caller supplied no password. When the caller supplied their own password, GeneratedPassword is empty.
type CachingProjectResolver ¶ added in v1.2.0
type CachingProjectResolver struct {
// contains filtered or unexported fields
}
CachingProjectResolver decorates a ProjectResolver with a short-TTL, LRU-bounded in-process cache. Project resolution runs on every request ahead of the rate limiter (and on every CORS preflight), so without a cache each request issues 2-3 uncached control-plane queries — a DoS amplification and scaling liability. The cache removes that cost while keeping correctness identical within the TTL window: a suspended project or revoked credential is re-read from the store once the (short) TTL elapses. Resolution semantics are otherwise unchanged — this is purely a performance/availability decorator.
func (*CachingProjectResolver) ResolveByCredential ¶ added in v1.2.0
func (c *CachingProjectResolver) ResolveByCredential(ctx context.Context, publicID string) (*ResolvedProject, error)
func (*CachingProjectResolver) ResolveByHostname ¶ added in v1.2.0
func (c *CachingProjectResolver) ResolveByHostname(ctx context.Context, hostname string) (*ResolvedProject, error)
type ControlPlaneAdminService ¶ added in v0.19.0
type ControlPlaneAdminService struct {
// contains filtered or unexported fields
}
ControlPlaneAdminService provisions control-plane resources on behalf of a platform operator authenticated by a shared secret.
func NewControlPlaneAdminService ¶ added in v0.19.0
func NewControlPlaneAdminService( secret string, projects ControlPlaneProjectStore, tenants TenantStore, memberships MembershipStore, policies LoginPolicyStore, admins PlatformAdminStore, resolver DNSResolver, auditLog *audit.Logger, logger *zap.Logger, ) *ControlPlaneAdminService
NewControlPlaneAdminService wires the admin service. An empty secret leaves the service constructed but DISABLED: every method returns ErrUnimplemented (so the handler maps it to CodeUnimplemented). A nil logger defaults to a no-op. A nil resolver defaults to net.DefaultResolver, matching DomainService. A nil auditLog defaults to a no-op audit.Logger so blocked-bootstrap recording stays best-effort. A nil policies store leaves the LoginPolicy-authoring RPCs disabled (ErrUnimplemented), matching the memory shape with no governance plane. nowFunc is injected so the auth-domain verified-at stamp is deterministic in tests; it defaults to wall-clock epoch-millis.
func (*ControlPlaneAdminService) AddProjectAuthDomain ¶ added in v1.1.0
func (s *ControlPlaneAdminService) AddProjectAuthDomain(ctx context.Context, secret, projectID, hostname string, isPrimary bool) (*RegisteredAuthDomain, error)
AddProjectAuthDomain registers a CUSTOMER-owned serving hostname on a project, UNVERIFIED, and returns the DNS TXT challenge to publish. Unlike AdminAddProjectAuthDomain (operator-vouched, seeded verified), the domain does NOT resolve requests until VerifyProjectAuthDomain proves ownership. Re-adding an already-registered hostname returns its existing record and the same deterministic challenge, so a caller can re-fetch the TXT value without a conflict. A hostname owned by a DIFFERENT project surfaces the store's ErrAlreadyExists.
isPrimary=true is rejected with ErrInvalidArgument: a custom domain is always added NON-primary here, because an unverified host must not resolve, let alone drive branded links. To make a custom domain primary, add it non-primary, verify it, then call SetPrimaryAuthDomain — which promotes only a verified domain and atomically demotes the current primary.
func (*ControlPlaneAdminService) AdminAddProjectAuthDomain ¶ added in v0.19.0
func (s *ControlPlaneAdminService) AdminAddProjectAuthDomain(ctx context.Context, secret, projectID, hostname string, isPrimary bool) error
AdminAddProjectAuthDomain registers a serving hostname on a project, idempotently and seeded VERIFIED (the operator vouches for it). It lets an operator add branded/serving hostnames at runtime, complementing the GATEWAY_DEFAULT_PROJECT_AUTH_DOMAINS config-seed. A hostname already bound to a DIFFERENT project surfaces the store's conflict error.
func (*ControlPlaneAdminService) AdminAddTenantAdmin ¶ added in v0.19.0
func (s *ControlPlaneAdminService) AdminAddTenantAdmin(ctx context.Context, secret, projectID, tenantID, userID, role string) (*TenantMembership, error)
AdminAddTenantAdmin makes a user an owner/admin of a tenant (source=added), bootstrapping the first tenant administrator so a human can then self-serve (invite others, verify domains, manage members). The role defaults to owner — the highest privilege an operator would grant when standing up a tenant — and may be admin; plain member is rejected (this RPC bootstraps administration, not ordinary membership).
func (*ControlPlaneAdminService) AdminCreateProject ¶ added in v0.19.0
func (s *ControlPlaneAdminService) AdminCreateProject(ctx context.Context, secret, name, storageScopeID string) (string, error)
AdminCreateProject provisions a new control-plane project mapped onto the given physical storage scope, and returns its id. storage_scope_id is required and globally unique (a duplicate surfaces ErrAlreadyExists).
func (*ControlPlaneAdminService) AdminCreateProjectCredential ¶ added in v0.19.0
func (s *ControlPlaneAdminService) AdminCreateProjectCredential(ctx context.Context, secret, projectID, kind string) (*MintedCredential, error)
AdminCreateProjectCredential mints a lookup credential for a project. For a publishable kind it generates a public id only (no secret). For a secret kind it generates a public id AND a secret half: the secret's hash is stored, and the full "publicID.secret" raw key is returned ONCE — the only time it is ever shown, exactly like an API key. The public id is always the lookup key the project resolver matches on.
func (*ControlPlaneAdminService) AdminCreateTenant ¶ added in v0.19.0
func (s *ControlPlaneAdminService) AdminCreateTenant(ctx context.Context, secret, projectID, name, primaryDomain string) (string, error)
AdminCreateTenant provisions a tenant under a project and returns its id. An operator-created tenant is seeded CLAIMED, not latent: the operator vouches for it out-of-band, so it is immediately authoritative (its login policy and membership apply) without a domain-verification round.
func (*ControlPlaneAdminService) CreateFirstPlatformAdmin ¶ added in v1.1.0
func (s *ControlPlaneAdminService) CreateFirstPlatformAdmin(ctx context.Context, email, password string) (*BootstrappedAdmin, error)
CreateFirstPlatformAdmin is the zero-config bootstrap that establishes the FIRST platform admin on a fresh deployment. It is the one Admin RPC that is NOT secret-gated: a brand-new deployer has configured nothing yet, so gating it on GATEWAY_ADMIN_API_SECRET would make standing up the first operator impossible. Instead it is self-securing — it succeeds ONLY while the platform_admins table is empty and PERMANENTLY closes (ErrPlatformAdminExists → FailedPrecondition) once any admin exists, so it can never be replayed to escalate privilege on a provisioned deployment.
The emptiness check and the insert are one atomic, serialized store operation (admins.CreateFirstPlatformAdmin), so two concurrent bootstraps create exactly one admin and the loser is rejected — there is no check-then-write window.
When password is blank the server generates a strong one and returns it once in GeneratedPassword; when supplied it must satisfy the password strength policy (else ErrWeakPassword → InvalidArgument). Only the bcrypt hash is ever stored. When no control plane is wired (memory have no platform_admins table) it returns ErrUnimplemented.
func (*ControlPlaneAdminService) DeleteLoginPolicy ¶ added in v1.3.0
func (s *ControlPlaneAdminService) DeleteLoginPolicy(ctx context.Context, secret, projectID, tenantID string) error
DeleteLoginPolicy clears a claimed tenant's LoginPolicy, reverting the login path to its safe default for that tenant. It is idempotent (deleting an absent policy is a no-op) and emits an audit event.
func (*ControlPlaneAdminService) Enabled ¶ added in v0.19.0
func (s *ControlPlaneAdminService) Enabled() bool
Enabled reports whether the admin surface is active (a non-empty secret is configured). The handler consults it to return Unimplemented up front without leaking, via the secret check, whether a secret happens to match.
func (*ControlPlaneAdminService) GetLoginPolicy ¶ added in v1.3.0
func (s *ControlPlaneAdminService) GetLoginPolicy(ctx context.Context, secret, projectID, tenantID string) (*LoginPolicy, error)
GetLoginPolicy returns the LoginPolicy for (projectID, tenantID), or (nil, nil) when none is set. project_id and tenant_id are required.
func (*ControlPlaneAdminService) GetProjectConfig ¶ added in v1.3.0
func (s *ControlPlaneAdminService) GetProjectConfig(ctx context.Context, secret, projectID string) (string, error)
GetProjectConfig returns a project's stored config_json ("{}" when unset). project_id is required; an unknown project surfaces ErrNotFound.
func (*ControlPlaneAdminService) ListProjectAuthDomains ¶ added in v1.1.0
func (s *ControlPlaneAdminService) ListProjectAuthDomains(ctx context.Context, secret, projectID string) ([]*AdminProjectAuthDomain, error)
ListProjectAuthDomains returns every auth-domain of a project, primary-first (the store's ordering), so a caller sees both verified and pending domains.
func (*ControlPlaneAdminService) SetPrimaryAuthDomain ¶ added in v1.2.0
func (s *ControlPlaneAdminService) SetPrimaryAuthDomain(ctx context.Context, secret, projectID, hostname string) (*AdminProjectAuthDomain, error)
SetPrimaryAuthDomain promotes a VERIFIED custom auth-domain to the project's primary serving host. The store demotes the current primary and promotes the target in a SINGLE transaction, so the per-project primary uniqueness is never violated, even under concurrent promotions. Only a verified domain may be promoted (an unverified target is ErrAuthDomainNotVerified); a hostname the project does not own is ErrNotFound. The returned record reflects the newly-promoted (now primary) host, which the resolver's primaryAuthHostname / PrimaryAuthDomain then surfaces.
func (*ControlPlaneAdminService) UpsertLoginPolicy ¶ added in v1.3.0
func (s *ControlPlaneAdminService) UpsertLoginPolicy(ctx context.Context, secret string, p *LoginPolicy) (*LoginPolicy, error)
UpsertLoginPolicy authors (inserts or replaces) the LoginPolicy the login path enforces for a claimed tenant within a project. project_id and tenant_id are required; AllowedMethods is validated against the known method tokens. The mutation emits an audit event. When this build has no governance plane (nil policies store) it returns ErrUnimplemented.
func (*ControlPlaneAdminService) UpsertProjectConfig ¶ added in v1.3.0
func (s *ControlPlaneAdminService) UpsertProjectConfig(ctx context.Context, secret, projectID, configJSON string) (string, error)
UpsertProjectConfig REPLACES a project's config_json blob and returns the stored (normalised) value. The blob must be a valid JSON object — it is validated by decoding it through ParseProjectConfig so a malformed config is rejected (ErrInvalidArgument) before it is persisted, never silently stored. The mutation emits an audit event. project_id is required; an unknown project surfaces ErrNotFound from the store.
func (*ControlPlaneAdminService) VerifyProjectAuthDomain ¶ added in v1.1.0
func (s *ControlPlaneAdminService) VerifyProjectAuthDomain(ctx context.Context, secret, projectID, hostname string) (*AdminProjectAuthDomain, error)
VerifyProjectAuthDomain checks the DNS TXT ownership challenge for a project's custom auth-domain and, on success, stamps verified_at_ms so the hostname resolves. A missing/mismatched TXT record leaves the domain unverified and surfaces ErrPermissionDenied (a verification failure the caller can retry); a hostname the project does not own is ErrNotFound. An already-verified domain is idempotent — re-verifying re-checks DNS and returns the current record.
type ControlPlaneProjectStore ¶ added in v0.19.0
type ControlPlaneProjectStore interface {
CreateProject(ctx context.Context, p *AdminProject) (string, error)
CreateProjectCredential(ctx context.Context, c *AdminProjectCredential) (string, error)
EnsureAuthDomain(ctx context.Context, projectID, hostname string, isPrimary bool, verifiedAtMs int64) error
// CreateAuthDomain registers an UNVERIFIED serving hostname (verifiedAtMs
// is 0). A hostname already bound to any project surfaces ErrAlreadyExists.
CreateAuthDomain(ctx context.Context, projectID, hostname string, isPrimary bool) error
// GetAuthDomain returns a project's own auth-domain, or (nil, nil) when the
// project has no such hostname.
GetAuthDomain(ctx context.Context, projectID, hostname string) (*AdminProjectAuthDomain, error)
// ListAuthDomains returns every auth-domain of a project, primary-first.
ListAuthDomains(ctx context.Context, projectID string) ([]*AdminProjectAuthDomain, error)
// SetAuthDomainVerified stamps verifiedAtMs (> 0) on a project's own
// auth-domain, making it resolve. A hostname the project does not own
// surfaces ErrNotFound.
SetAuthDomainVerified(ctx context.Context, projectID, hostname string, verifiedAtMs int64) error
// SetPrimaryAuthDomain promotes a project's VERIFIED auth-domain to its
// primary serving host, atomically demoting the current primary in one
// transaction so the per-project primary uniqueness is never violated. An
// unverified target surfaces ErrAuthDomainNotVerified; a hostname the
// project does not own surfaces ErrNotFound.
SetPrimaryAuthDomain(ctx context.Context, projectID, hostname string) (*AdminProjectAuthDomain, error)
// UpdateProjectConfig REPLACES a project's config_json blob and returns the
// stored value (normalised — an empty blob becomes "{}"). A project that
// does not exist surfaces ErrNotFound.
UpdateProjectConfig(ctx context.Context, projectID, configJSON string) (string, error)
// GetProjectConfig returns a project's stored config_json ("{}" when
// unset). A project that does not exist surfaces ErrNotFound.
GetProjectConfig(ctx context.Context, projectID string) (string, error)
}
ControlPlaneProjectStore is the narrow write side of the control-plane project registry the admin service needs. *pgrepo.ProjectStore satisfies it; injecting only this method set keeps the service decoupled from the full store surface and trivially fakeable.
EnsureAuthDomain is idempotent and seeds the domain VERIFIED at verifiedAtMs (operator-asserted — the operator vouches for the hostname, so it needs no DNS challenge). The customer-facing custom-domain methods (CreateAuthDomain / GetAuthDomain / ListAuthDomains / SetAuthDomainVerified) instead register a domain UNVERIFIED and flip it only after a DNS-TXT ownership proof.
type CreatedDomain ¶ added in v0.17.0
CreatedDomain is the result of CreateDomain: the pending domain plus the DNS TXT challenge the caller must publish before VerifyDomain succeeds. TXTName/TXTValue are empty for the email method (verified out-of-band).
type CreatedInvitation ¶ added in v0.18.0
type CreatedInvitation struct {
Invitation *TenantInvitation
RawToken string
}
CreatedInvitation is the result of CreateTenantInvitation: the stored invitation plus, ONLY when no mailer is configured, the raw token (so a headless deployment can hand it to the recipient out-of-band). When a mailer delivered the invitation, RawToken is empty.
type DB ¶
type DB interface {
GetNode(ctx context.Context, tenantID, actor string, typeID int, nodeID string) (*graph.Node, error)
QueryNodes(ctx context.Context, tenantID, actor string, typeID int, filter map[string]any) ([]*graph.Node, error)
ExecuteAtomic(ctx context.Context, tenantID, actor string, ops []graph.Operation) (*graph.CommitResult, error)
GetEdgesFrom(ctx context.Context, tenantID, actor, fromNodeID string, edgeTypeID int) ([]*graph.Edge, error)
GetEdgesTo(ctx context.Context, tenantID, actor, toNodeID string, edgeTypeID int) ([]*graph.Edge, error)
SearchNodes(ctx context.Context, tenantID, actor string, typeID int, query string) ([]*graph.Node, error)
// RegisterUserInTenant registers userID in the global user
// registry and adds them as a member of tenantID with the given
// role. Idempotent: tolerates ALREADY_EXISTS on both calls. The
// service-layer admin path (admin.InviteUser) uses this to bring
// new users onto v1.12+'s "actor must be a tenant member"
// contract; the typed entRepository.CreateUser path uses the
// equivalent SDK calls directly via its entClient seam.
RegisterUserInTenant(ctx context.Context, tenantID, userID, email, name, role string) error
}
DB is the subset of the graph DB used by identity services.
func ScopedDB ¶ added in v1.1.0
ScopedDB resolves a request's project from ctx and returns bootDB bound to that project together with the resolved project id. It is the exported pair of scopedDB + requestProjectID, wired by internal/app into the audit logger so an audit write lands under the SAME project the request resolved to (ADR-0002): the project id partitions the graph transport (which keys on the per-call tenant argument every method already takes), and the returned DB is the project-bound postgres writer (which ignores that argument and filters on its bound project). Reads via ProfileService.ListAuditEvents resolve the project identically, so writes and reads round-trip under one project.
type DNSResolver ¶ added in v0.18.0
DNSResolver looks up DNS TXT records. It is the single I/O boundary of VerifyDomain, injected so callers (and tests) can supply the lookup implementation instead of always hitting real DNS. *net.Resolver satisfies it; the embedding API (identityserver.Options.DNSResolver) threads a custom one through for full-stack tests against a real DB.
type Domain ¶ added in v0.17.0
type Domain struct {
ID string
ProjectID string
TenantID string
Domain string
VerificationMethod string
Status string
VerifiedAtMs int64
CreatedAtMs int64
UpdatedAtMs int64
}
Domain is an email domain bound to a Tenant within a Project. Verifying it (DNS TXT or email) flips its status to `verified` and claims the owning tenant. (project_id, lower(domain)) is unique — one tenant per email domain within a project.
type DomainService ¶ added in v0.17.0
type DomainService struct {
// contains filtered or unexported fields
}
DomainService verifies tenant email domains within a project.
func NewDomainService ¶ added in v0.17.0
func NewDomainService( domains DomainStore, tenants TenantStore, memberships MembershipStore, resolver DNSResolver, cfg *config.Config, logger *zap.Logger, ) *DomainService
NewDomainService wires a DomainService. resolver may be nil, in which case net.DefaultResolver is used — callers inject a custom resolver to drive the TXT-present / TXT-absent paths without touching real DNS.
func (*DomainService) CreateDomain ¶ added in v0.17.0
func (s *DomainService) CreateDomain(ctx context.Context, callerID, tenantID, domain, method string) (*CreatedDomain, error)
CreateDomain registers a pending email domain on a tenant. callerID must already be an owner/admin member of the tenant. For the DNS-TXT method it returns the deterministic challenge (TXT name + value) the caller publishes; VerifyDomain later checks for it.
func (*DomainService) ListTenantDomains ¶ added in v0.17.0
func (s *DomainService) ListTenantDomains(ctx context.Context, callerID, tenantID string) ([]*Domain, error)
ListTenantDomains returns every domain bound to a tenant. callerID must be an owner/admin member of the tenant.
func (*DomainService) VerifyDomain ¶ added in v0.17.0
func (s *DomainService) VerifyDomain(ctx context.Context, callerID, domainID string) (*Domain, error)
VerifyDomain proves control of a pending domain and, on success, marks the domain verified, claims its tenant, and makes the caller an owner.
AuthZ: callerID must be an owner/admin member of the tenant — EXCEPT when the tenant is still latent with no members yet, in which case verification is open and the first verifier becomes its owner.
Only the DNS-TXT method is implemented; the email method returns ErrUnimplemented rather than faking success.
type DomainStore ¶ added in v0.17.0
type DomainStore interface {
// CreateDomain inserts a domain. ProjectID, TenantID and Domain are
// required; a blank id is generated and written back. A duplicate
// (project_id, lower(domain)) surfaces ErrAlreadyExists.
CreateDomain(ctx context.Context, d *Domain) (string, error)
// GetDomain returns the domain by id within a project, or (nil, nil).
GetDomain(ctx context.Context, projectID, domainID string) (*Domain, error)
// GetDomainByName returns the domain row for a name (case-insensitive)
// within a project, or (nil, nil).
GetDomainByName(ctx context.Context, projectID, domain string) (*Domain, error)
// SetDomainStatus transitions a domain's status and, when verifying,
// stamps verified_at_ms (pass 0 to default to now on a verify). Unknown
// ids are a no-op.
SetDomainStatus(ctx context.Context, projectID, domainID, status string, verifiedAtMs int64) error
// ListDomainsByTenant returns every domain bound to a tenant, newest
// first.
ListDomainsByTenant(ctx context.Context, projectID, tenantID string) ([]*Domain, error)
}
DomainStore persists Domains within a Project. Reads that miss return (nil, nil), never an error.
type EmailChangeToken ¶
type EmailChangeToken struct {
NodeID string
TokenHash string
UserID string
OldEmail string
NewEmail string
ExpiresAt int64 // epoch ms
CreatedAt int64 // epoch ms
ConsumedAt int64 // epoch ms; 0 = unconsumed
}
EmailChangeToken represents a pending primary-email rotation. The token is created at request time (after re-auth) and consumed when the user clicks the verification link sent to the *new* address.
type EmailLoginCodeRecord ¶ added in v0.11.0
type EmailLoginCodeRecord struct {
NodeID string
Email string
CodeHash string
ExpiresAt int64 // epoch ms
CreatedAt int64 // epoch ms
ConsumedAt int64 // epoch ms; 0 = unconsumed
AttemptCount int64
MaxAttempts int64
}
EmailLoginCodeRecord is the OTP arm of passwordless email login. The record is keyed by Email (at most one live code per address); a new request overwrites the previous one. CodeHash is sha256 of the 6-digit code. AttemptCount tracks failed verifies; once it reaches MaxAttempts the code is invalidated (brute-force cap). The record carries no user id — the account may not exist until VerifyEmailLoginCode resolves or creates it.
type EmailVerificationToken ¶
type EmailVerificationToken struct {
NodeID string
TokenHash string
UserID string
Email string
ExpiresAt int64 // epoch ms
CreatedAt int64 // epoch ms
ConsumedAt int64 // epoch ms; 0 = unconsumed
}
EmailVerificationToken represents a stored email-verification token.
type GroupService ¶
type GroupService struct {
// contains filtered or unexported fields
}
GroupService implements working-group CRUD and membership operations. The underlying graph node type is WorkingGroup (type_id 2).
func NewGroupService ¶
func NewGroupService(db DB, projectID string, auditLog *audit.Logger, logger *zap.Logger) *GroupService
NewGroupService creates a GroupService.
func (*GroupService) AddGroupMember ¶
func (s *GroupService) AddGroupMember(ctx context.Context, actorID, groupID, userID string) error
AddGroupMember creates a MEMBER_OF edge from user to group.
func (*GroupService) CreateGroup ¶
func (s *GroupService) CreateGroup(ctx context.Context, actorID, name, description string) (*Group, error)
CreateGroup creates a new working group.
func (*GroupService) DeleteGroup ¶
func (s *GroupService) DeleteGroup(ctx context.Context, actorID, groupID string) error
DeleteGroup deletes a working group.
func (*GroupService) ListGroupMembers ¶
func (s *GroupService) ListGroupMembers(ctx context.Context, actorID, groupID string) ([]*User, error)
ListGroupMembers returns all users that belong to a group via MEMBER_OF edges.
func (*GroupService) ListGroups ¶
func (s *GroupService) ListGroups(ctx context.Context, actorID, cursor string, limit int) ([]*Group, string, error)
ListGroups returns a paginated list of groups.
func (*GroupService) RemoveGroupMember ¶
func (s *GroupService) RemoveGroupMember(ctx context.Context, actorID, groupID, userID string) error
RemoveGroupMember deletes the MEMBER_OF edge from user to group.
func (*GroupService) UpdateGroup ¶
func (s *GroupService) UpdateGroup(ctx context.Context, actorID, groupID, name, description string) (*Group, error)
UpdateGroup patches name and/or description of a group.
type HelpRequest ¶
type HelpRequest struct {
ID string
Email string
Reason string
SourceIP string
UserAgent string
Status string
ResolvedBy string
ResolutionNotes string
ResolvedAt int64
CreatedAt int64
}
HelpRequest is the domain representation of an admin help request (type_id 28).
type HelpService ¶
type HelpService struct {
// contains filtered or unexported fields
}
HelpService implements the admin-help-request flow. End users who cannot log in raise a help request; admins resolve or reject them from the dashboard.
func NewHelpService ¶
func NewHelpService(db DB, projectID string, auditLog *audit.Logger, logger *zap.Logger) *HelpService
NewHelpService creates a HelpService.
func (*HelpService) ListHelpRequests ¶
func (s *HelpService) ListHelpRequests( ctx context.Context, actorID, statusFilter, cursor string, limit int, ) ([]*HelpRequest, string, int, error)
ListHelpRequests returns a paginated list of help requests. Admin-only. Returns pendingCount (all pending, regardless of filter).
func (*HelpService) RequestAdminHelp ¶
func (s *HelpService) RequestAdminHelp( ctx context.Context, email, reason, sourceIP, userAgent string, ) error
RequestAdminHelp creates a help request. Always returns nil error to prevent email enumeration — internal failures are logged and swallowed. Rate limited to 3 requests per email per 24 hours.
func (*HelpService) ResolveHelpRequest ¶
func (s *HelpService) ResolveHelpRequest( ctx context.Context, actorID, requestID string, reject bool, notes string, ) (*HelpRequest, error)
ResolveHelpRequest marks a help request as resolved or rejected. Admin-only.
type HostedOAuthBeginResult ¶ added in v0.9.0
type HostedOAuthBeginResult struct {
AuthorizationURL string
}
HostedOAuthBeginResult is the output of BeginHostedOAuth: the provider authorization URL the browser should be 302-redirected to. The state + PKCE verifier + return_to are all sealed inside the signed hosted state token carried in the URL's `state` parameter, so the callback needs nothing else from the browser.
type HostedOAuthCallbackResult ¶ added in v0.9.0
HostedOAuthCallbackResult is the output of CompleteHostedOAuth: the validated return_to plus the freshly-minted one-time code the callback appends as ?code=<otc>.
type IdentityVerificationRecord ¶ added in v0.4.0
type IdentityVerificationRecord struct {
NodeID string
VerificationID string // public identifier returned to clients
UserID string
ProjectID string // storage shard (ADR-0002): the per-request project
Provider string
ProviderSessionID string
Status string // one of IDVStatus* constants
CreatedAt int64 // epoch ms
UpdatedAt int64 // epoch ms
CompletedAt int64 // epoch ms; 0 if not yet completed
RejectionReason string // empty unless Status == IDVStatusRejected
}
IdentityVerificationRecord represents a single verification session (document + selfie) tracked by the service. The provider field names the backing implementation (e.g. "azure", "stub"); ProviderSessionID is the provider's own identifier for the check.
type IdentityVerificationService ¶ added in v0.4.0
type IdentityVerificationService struct {
// contains filtered or unexported fields
}
IdentityVerificationService orchestrates document + selfie verification through a pluggable idv.Provider, persisting one IdentityVerificationRecord per session against the user.
The caller-facing identifier (VerificationID) is server-issued; the provider's own session id is stored alongside but never returned to the client.
func NewIdentityVerificationService ¶ added in v0.4.0
func NewIdentityVerificationService( repo Repository, provider idv.Provider, projectID string, logger *zap.Logger, ) *IdentityVerificationService
NewIdentityVerificationService constructs the service. clock and newID default to time.Now / a hex random id when nil.
func (*IdentityVerificationService) BeginIdentityVerification ¶ added in v0.4.0
func (s *IdentityVerificationService) BeginIdentityVerification( ctx context.Context, userID string, ) (*BeginIdentityVerificationResult, error)
BeginIdentityVerification creates a new verification session for the given user. It allocates a server-side VerificationID, asks the provider to mint a client session token, and persists a PENDING record so subsequent status polls have something to read.
Returns ErrNotFound when userID does not resolve to a user.
func (*IdentityVerificationService) GetIdentityVerificationStatus ¶ added in v0.4.0
func (s *IdentityVerificationService) GetIdentityVerificationStatus( ctx context.Context, callerUserID, verificationID string, ) (*IdentityVerificationRecord, error)
GetIdentityVerificationStatus returns the current state of a verification. When verificationID is empty the caller's latest session is returned; this matches the "what's my status?" query the client SDK makes most often.
For sessions whose persisted state is non-terminal, the provider is consulted and the local record is updated if a verdict has arrived.
Returns ErrNotFound when no matching session exists, and ErrPermissionDenied when the caller is not the owner.
func (*IdentityVerificationService) WithMinorDataMinimizer ¶ added in v1.4.0
func (s *IdentityVerificationService) WithMinorDataMinimizer(m MinorDataMinimizer) *IdentityVerificationService
WithMinorDataMinimizer wires COPPA data-minimization: when the minimizer is active, a CHILD-band account cannot begin identity verification. Returns the service for chaining. Off by default (zero-value minimizer is a no-op).
type InitiateQrLoginResult ¶ added in v0.6.0
type InitiateQrLoginResult struct {
SessionID string
QRURL string
PollSecret string
ExpiresIn int32
}
InitiateQrLoginResult is the value returned by InitiateQrLogin.
type InvitationRecord ¶
type InvitationRecord struct {
NodeID string
TokenHash string
Email string
UserID string
InvitedBy string
Role string
ExpiresAt int64
AcceptedAt int64
CreatedAt int64
}
InvitationRecord represents a user invitation.
type InvitationStore ¶ added in v0.17.0
type InvitationStore interface {
// CreateInvitation atomically enforces one-open-invite: in a single
// transaction it revokes any existing pending invitation for the same
// (project, tenant, lower(email)) and inserts the new one. A blank id
// is generated and written back. Returns the new invitation id.
CreateInvitation(ctx context.Context, inv *TenantInvitation) (string, error)
// GetInvitationByTokenHash resolves an invitation by its hashed token
// within a project, or (nil, nil).
GetInvitationByTokenHash(ctx context.Context, projectID, tokenHash string) (*TenantInvitation, error)
// SetInvitationStatus transitions an invitation's status and, when
// accepting, stamps accepted_at_ms (0 defaults to now on accept).
// Unknown ids are a no-op.
SetInvitationStatus(ctx context.Context, projectID, invitationID, status string, acceptedAtMs int64) error
// ListInvitationsForTenant returns every invitation in a tenant,
// newest first.
ListInvitationsForTenant(ctx context.Context, projectID, tenantID string) ([]*TenantInvitation, error)
}
InvitationStore persists TenantInvitations within a Project. Reads that miss return (nil, nil).
type InviteResult ¶
type InviteResult struct {
User *User
InvitationToken string
SetupURL string
TemporaryPassword string
}
InviteResult is returned by AdminService.InviteUser.
type LoginChallengeRecord ¶
type LoginChallengeRecord struct {
NodeID string
ChallengeID string
UserID string
ExpiresAt int64
CreatedAt int64
}
LoginChallengeRecord represents a pending 2FA login challenge.
type LoginGovernance ¶ added in v0.17.0
type LoginGovernance struct {
Domains DomainStore
Tenants TenantStore
Policies LoginPolicyStore
}
LoginGovernance is the read-side bundle the login path consults to enforce a claimed tenant's LoginPolicy. It is postgres-only governance state, set once via AuthService.WithLoginGovernance; drivers without a governance plane (memory) leave it nil and impose no restriction.
It is deliberately read-only — enforcement never mutates governance state — and groups the three stores the lookup walks (domain → tenant → policy) so the dependency stays a single optional field on the already-wide service.
type LoginPolicy ¶ added in v0.17.0
type LoginPolicy struct {
ID string
ProjectID string
TenantID string
// AllowedMethods is a comma-separated allow-list of login method
// tokens (see LoginMethod*). Empty means no restriction.
AllowedMethods string
// SSORequired forces SSO; SSOConnectionJSON carries the connection
// config (IdP metadata) as a JSON object.
SSORequired bool
SSOConnectionJSON string
// Require2FA forces a second factor after the primary method.
Require2FA bool
CreatedAtMs int64
UpdatedAtMs int64
}
LoginPolicy is a claimed tenant's authentication policy.
type LoginPolicyStore ¶ added in v0.17.0
type LoginPolicyStore interface {
// UpsertLoginPolicy inserts or replaces the policy for
// (ProjectID, TenantID). Both are required. Returns the row id.
UpsertLoginPolicy(ctx context.Context, p *LoginPolicy) (string, error)
// GetLoginPolicy returns the policy for (projectID, tenantID), or
// (nil, nil) when none is set.
GetLoginPolicy(ctx context.Context, projectID, tenantID string) (*LoginPolicy, error)
// DeleteLoginPolicy removes the policy for (projectID, tenantID). It is
// idempotent: deleting an absent policy is a no-op that returns nil, so a
// caller can clear a tenant's policy without first checking for one. Both
// ids are required.
DeleteLoginPolicy(ctx context.Context, projectID, tenantID string) error
}
LoginPolicyStore persists at most one LoginPolicy per (project, tenant).
type LoginResult ¶
type LoginResult struct {
User *User
AccessToken string
RefreshToken string
ExpiresIn int32
TotpRequired bool
LoginChallengeID string
}
LoginResult is returned by password/OAuth login when successful.
type MagicLinkResult ¶ added in v0.11.0
type MagicLinkResult struct {
*LoginResult
ReturnTo string
}
MagicLinkResult is a LoginResult plus the allowlist-validated app URL the SPA should navigate to after a successful redeem.
type MagicLinkTokenRecord ¶ added in v0.11.0
type MagicLinkTokenRecord struct {
NodeID string
TokenHash string
Email string
ReturnTo string
ExpiresAt int64 // epoch ms
CreatedAt int64 // epoch ms
ConsumedAt int64 // epoch ms; 0 = unconsumed
}
MagicLinkTokenRecord is the magic-link arm of passwordless email login. TokenHash is sha256 of a high-entropy opaque token; the row is bound to the requested Email and the allowlist-validated ReturnTo. Single-use is enforced by the ConsumedAt compare-and-set.
type MembershipService ¶ added in v0.18.0
type MembershipService struct {
// contains filtered or unexported fields
}
MembershipService manages tenant invitations and memberships within a project.
func NewMembershipService ¶ added in v0.18.0
func NewMembershipService( invitations InvitationStore, memberships MembershipStore, tenants TenantStore, users UserDirectory, mailer email.Transport, mailerConfigured bool, cfg *config.Config, logger *zap.Logger, ) *MembershipService
NewMembershipService wires a MembershipService. mailer must be non-nil (the app builds a log-only transport when no provider is configured); mailerConfigured tells the service whether that transport actually delivers, which governs whether the raw token is returned in the RPC response. A nil logger defaults to a no-op.
func (*MembershipService) AcceptTenantInvitation ¶ added in v0.18.0
func (s *MembershipService) AcceptTenantInvitation(ctx context.Context, callerID, rawToken string) (*TenantMembership, error)
AcceptTenantInvitation redeems a raw invitation token for the authenticated caller. It validates the token (must be pending and unexpired), enforces that the caller's account email equals the invitation email, then upserts an active invited-source membership at the invitation's role and marks the invitation accepted.
Email-match policy: a leaked token must not let the wrong account join. The caller is looked up and their email compared (case-insensitively) to the invitation email; a mismatch is PermissionDenied. An expired invitation is marked expired and rejected; an unknown/revoked/already-accepted token is rejected without mutation.
func (*MembershipService) CreateTenantInvitation ¶ added in v0.18.0
func (s *MembershipService) CreateTenantInvitation(ctx context.Context, callerID, tenantID, emailAddr, role string) (*CreatedInvitation, error)
CreateTenantInvitation invites an email address to join a tenant. callerID must be an active owner/admin member of the tenant. It generates a random token, stores its hash via InvitationStore.CreateInvitation (which atomically revokes any prior open invite for the same recipient), and best-effort dispatches a branded invitation email carrying the raw token.
func (*MembershipService) ListTenantInvitations ¶ added in v0.18.0
func (s *MembershipService) ListTenantInvitations(ctx context.Context, callerID, tenantID string) ([]*TenantInvitation, error)
ListTenantInvitations returns every invitation in a tenant, newest first. callerID must be an active owner/admin member of the tenant.
func (*MembershipService) ListTenantMembers ¶ added in v0.18.0
func (s *MembershipService) ListTenantMembers(ctx context.Context, callerID, tenantID string) ([]*TenantMembership, error)
ListTenantMembers returns every membership in a tenant. callerID must be an active owner/admin member of the tenant.
func (*MembershipService) RemoveTenantMember ¶ added in v0.18.0
func (s *MembershipService) RemoveTenantMember(ctx context.Context, callerID, tenantID, targetUserID string) error
RemoveTenantMember removes a user's membership from a tenant. callerID must be an active owner/admin member of the tenant.
Last-owner guard: a tenant must never be left ownerless. Removing the only remaining active owner is rejected with FailedPrecondition-style ErrInvalidArgument-adjacent semantics — here ErrPermissionDenied is wrong (the caller IS permitted to remove members), so we surface a dedicated guard error. The rule: if the target is an active owner and is the last active owner of the tenant, the removal is refused. Removing a non-owner, or an owner while other active owners remain, is allowed (including an owner removing themselves, as long as another owner survives).
type MembershipStore ¶ added in v0.17.0
type MembershipStore interface {
// UpsertMembership inserts or, on a (project, tenant, user) conflict,
// updates source/role/status in place (keeping id + created_at_ms).
// Returns the surviving row id.
UpsertMembership(ctx context.Context, m *TenantMembership) (string, error)
// GetMembership returns the membership for (project, tenant, user), or
// (nil, nil).
GetMembership(ctx context.Context, projectID, tenantID, userID string) (*TenantMembership, error)
// ListMembershipsForUser returns every membership a user holds across
// the tenants of a project — the set the auth middleware checks.
ListMembershipsForUser(ctx context.Context, projectID, userID string) ([]*TenantMembership, error)
// ListMembershipsForTenant returns every membership in a tenant.
ListMembershipsForTenant(ctx context.Context, projectID, tenantID string) ([]*TenantMembership, error)
// RemoveMembership deletes the membership for (project, tenant, user).
// Unknown rows are a no-op.
RemoveMembership(ctx context.Context, projectID, tenantID, userID string) error
}
MembershipStore persists TenantMemberships within a Project. Reads that miss return (nil, nil).
type MinorDataMinimizer ¶ added in v1.4.0
type MinorDataMinimizer struct {
// contains filtered or unexported fields
}
MinorDataMinimizer encapsulates the COPPA data-minimization decision: given a user's stored date of birth, should the server refuse to collect or persist non-essential PII because the account is a COPPA-protected child?
It is a small value type (not an interface) shared by every service that touches optional PII — auth signup, profile updates, phone verification, and identity verification — so the "is this a minimized child?" rule lives in exactly one place. Only the CHILD band triggers minimization; teens and adults are unaffected, matching the COPPA scope (issue #257).
When minimization is disabled (the default) BlocksChild always returns false, so behavior is identical to a deployment that never heard of the feature.
func NewMinorDataMinimizer ¶ added in v1.4.0
func NewMinorDataMinimizer(enabled bool, determiner agegate.Determiner, now func() time.Time) MinorDataMinimizer
NewMinorDataMinimizer builds the minimizer from the resolved age-gate determiner and the GATEWAY_MINOR_DATA_MINIMIZATION flag. now supplies the reference instant for age math; when nil it defaults to time.Now.
Minimization is only ever active when the age gate itself is enabled — a CHILD band can only be derived from a DOB under an enabled gate — so a caller that flips GATEWAY_MINOR_DATA_MINIMIZATION on without age-gating gets a safe no-op rather than a half-wired control.
func (MinorDataMinimizer) BlocksChild ¶ added in v1.4.0
func (m MinorDataMinimizer) BlocksChild(dobMs int64) bool
BlocksChild reports whether the account identified by dobMs is a COPPA-protected child whose optional PII must not be collected/persisted. Always false when minimization is disabled.
func (MinorDataMinimizer) Enabled ¶ added in v1.4.0
func (m MinorDataMinimizer) Enabled() bool
Enabled reports whether data-minimization is active for this deployment.
type MintedCredential ¶ added in v0.19.0
MintedCredential is the result of AdminCreateProjectCredential: the stored row's id and public lookup id, plus the RAW key shown exactly once. RawKey is empty for a publishable kind (which has no secret half).
type OAuthBeginResult ¶
type OAuthBeginResult struct {
AuthorizationURL string
State string
StateToken string
CodeVerifier string
ExpiresIn int32
}
OAuthBeginResult carries the provider authorization URL and the server-minted state artifacts needed to complete the OAuth flow.
type OAuthIdentity ¶
type OAuthIdentity struct {
NodeID string
UserID string
Provider string
ProviderUserID string
EmailAtLinkTime string
CreatedAt int64 // epoch ms
}
OAuthIdentity is the persisted link between a local User and a provider-side stable identity (provider, provider_user_id).
Lookups by (provider, provider_user_id) are what make OAuth login resilient to a provider-side email change: the user keeps the same local account even if their gmail address changes.
Composite uniqueness on (provider, provider_user_id) is enforced at the application layer — the graph DB does not expose composite unique constraints. CreateOAuthIdentity callers must lookup first.
type OAuthLoginParams ¶ added in v1.5.0
type OAuthOneTimeCodeRecord ¶ added in v0.9.0
type OAuthOneTimeCodeRecord struct {
NodeID string
CodeHash string
UserID string
ExpiresAt int64 // epoch ms
CreatedAt int64 // epoch ms
ConsumedAt int64 // epoch ms; 0 = unconsumed
}
OAuthOneTimeCodeRecord is the single-use handover artifact for the hosted OAuth flow. The hosted callback stores the SHA-256 hash of an opaque code keyed to the authenticated user; RedeemOAuthCode consumes it (consumed_at CAS from 0) and mints a fresh token pair. Only the user id is persisted — no token material is stored at rest.
type PasskeyChallengeRecord ¶
type PasskeyChallengeRecord struct {
NodeID string
Challenge string // base64url
UserID string
ChallengeType string // "registration" or "authentication"
ExpiresAt int64
CreatedAt int64
}
PasskeyChallengeRecord represents a stored passkey challenge.
type PasskeyCredRecord ¶
type PasskeyCredRecord struct {
NodeID string
CredentialID string
UserID string
PublicKey string
SignCount int64
DeviceName string
AAGUID string
Transports string
// BackupEligible / BackupState are the WebAuthn backup flags captured at
// registration. They must be persisted and replayed at login: go-webauthn
// rejects an assertion whose backup flags are inconsistent with the stored
// credential, and every synced platform passkey (iCloud Keychain, Google
// Password Manager) sets BackupEligible, so dropping them breaks login.
BackupEligible bool
BackupState bool
CreatedAt int64
LastUsedAt int64
}
PasskeyCredRecord represents a stored passkey credential.
type PasskeyInfo ¶
type PasskeyInfo struct {
CredentialID string
DeviceName string
CreatedAt time.Time
LastUsedAt time.Time
}
PasskeyInfo holds display-safe passkey credential metadata.
type PasswordResetToken ¶
type PasswordResetToken struct {
NodeID string
TokenHash string
UserID string
ExpiresAt int64 // epoch ms
CreatedAt int64 // epoch ms
ConsumedAt int64 // epoch ms; 0 = unconsumed
}
PasswordResetToken represents a stored password-reset token.
type PhoneVerificationCodeRecord ¶ added in v0.14.0
type PhoneVerificationCodeRecord struct {
NodeID string
UserID string
PhoneNumber string
CodeHash string
ExpiresAt int64 // epoch ms
CreatedAt int64 // epoch ms
ConsumedAt int64 // epoch ms; 0 = unconsumed
AttemptCount int64
MaxAttempts int64
}
PhoneVerificationCodeRecord is the SMS-OTP arm of phone-ownership verification. The record is keyed by UserID (at most one live code per user); a new request overwrites the previous one. CodeHash is sha256 of the 6-digit code. AttemptCount tracks failed verifies; once it reaches MaxAttempts the code is invalidated (brute-force cap). Unlike EmailLoginCodeRecord this always carries a UserID — the caller is an already-authenticated user proving ownership of PhoneNumber.
type PlatformAdmin ¶ added in v1.1.0
type PlatformAdmin struct {
ID string
Email string
PasswordHash string
TOTPRequired bool
Status string
CreatedAtMs int64
LastLoginAtMs int64
}
PlatformAdmin is a control-plane operator account. Email is unique (case-insensitive); only PasswordHash — never a raw password — is stored.
type PlatformAdminStore ¶ added in v1.1.0
type PlatformAdminStore interface {
// CreateFirstPlatformAdmin inserts a the first platform admin ATOMICALLY
// and ONLY while the table is empty. It is the storage primitive behind
// the zero-config bootstrap: the emptiness check and the insert happen in
// one serialized transaction so two concurrent bootstraps create exactly
// one admin.
//
// It returns (created=true, nil) when this call inserted the admin, and
// (created=false, nil) when an admin already existed (the table was not
// empty) — the bootstrap is then permanently closed. A blank id is
// generated and written back to a.ID on success. Any other failure (e.g.
// a duplicate-email race) is returned as an error.
CreateFirstPlatformAdmin(ctx context.Context, a *PlatformAdmin) (created bool, err error)
// CountPlatformAdmins returns the number of platform admins. It backs the
// store conformance assertions; the bootstrap itself relies on the atomic
// CreateFirstPlatformAdmin, not a read-then-write.
CountPlatformAdmins(ctx context.Context) (int, error)
}
PlatformAdminStore persists PlatformAdmins. It exists only on the postgres control-plane driver; the memory driver has no platform_admins table, so the admin service is constructed without it and the bootstrap RPC returns Unimplemented.
type PollQrResult ¶
type PollQrResult struct {
Status string
User *User
AccessToken string
RefreshToken string
ExpiresIn int32
}
PollQrResult holds the result of polling a QR login session.
type ProfileService ¶
type ProfileService struct {
// contains filtered or unexported fields
}
ProfileService implements self-service profile, session, passkey, password, and audit log operations.
func NewProfileService ¶
func NewProfileService(repo Repository, db DB, projectID string, auditLog *audit.Logger, logger *zap.Logger) *ProfileService
NewProfileService creates a ProfileService.
The repo argument may be nil — in that case only the DB-backed paths work (Sessions, AuditEvents). Profile/password operations need repo to be set; nil triggers ErrServiceUnavailable at call time, the same shape the no-persistence stub takes.
func (*ProfileService) ChangePassword ¶
func (s *ProfileService) ChangePassword(ctx context.Context, userID, currentPassword, newPassword string) error
ChangePassword changes the user's password after verifying the current password. On success every one of the user's sessions is revoked (their refresh tokens are deleted), forcing re-authentication on all devices — the documented credential-change behavior.
The caller's own session is included: this RPC does not carry the current session/token id (the handler only resolves the authenticated user id from the JWT), so we cannot single out and preserve the caller's session. The user re-signs in with their new password. The session revoke is best-effort — the password is already committed when it runs, so a revoke failure is logged rather than failing the RPC.
func (*ProfileService) DeletePasskey ¶
func (s *ProfileService) DeletePasskey(ctx context.Context, userID, credentialID string) error
DeletePasskey deletes a passkey belonging to the user.
func (*ProfileService) ListAuditEvents ¶
func (s *ProfileService) ListAuditEvents( ctx context.Context, actorID, targetID, eventType string, startTime, endTime int64, cursor string, limit int, ) ([]*AuditEvent, string, error)
ListAuditEvents queries audit events. Admin-only.
func (*ProfileService) ListLinkedIdentities ¶ added in v1.3.0
func (s *ProfileService) ListLinkedIdentities(ctx context.Context, userID string) ([]*OAuthIdentity, error)
ListLinkedIdentities returns the authenticated user's connected provider identities (the (provider, provider_user_id) links), oldest first. It is a read-only self-service view backing a "connected accounts" surface.
func (*ProfileService) ListMyPasskeys ¶
func (s *ProfileService) ListMyPasskeys(ctx context.Context, userID string) ([]*PasskeyInfo, error)
ListMyPasskeys returns all passkey credentials for the user.
func (*ProfileService) ListMySessions ¶
ListMySessions returns all active sessions for the user.
func (*ProfileService) RevokeAllSessions ¶
func (s *ProfileService) RevokeAllSessions(ctx context.Context, userID, password string) (int, error)
RevokeAllSessions revokes every session for the user. Requires password confirmation.
func (*ProfileService) RevokeSession ¶
func (s *ProfileService) RevokeSession(ctx context.Context, userID, sessionID string) error
RevokeSession revokes a specific session. The session must belong to the calling user.
func (*ProfileService) UnlinkIdentity ¶ added in v1.3.0
func (s *ProfileService) UnlinkIdentity(ctx context.Context, userID, provider, providerUserID string) error
UnlinkIdentity disconnects a provider identity from the authenticated user.
It refuses to remove the user's LAST remaining sign-in credential: if the user has no password and no passkey, the final linked provider cannot be removed (ErrLastCredential), so a self-service unlink can never lock a user out of their own account. The check counts what would remain AFTER the removal, using the link the caller asked to drop, so removing a non-final link is always allowed.
func (*ProfileService) UpdateProfile ¶
func (s *ProfileService) UpdateProfile(ctx context.Context, userID, name, avatarURL string) (*User, error)
UpdateProfile updates the authenticated user's name and/or avatar. Routes through the Repository interface (which every backend implements fully) rather than the low-level DB.GetNode/ExecuteAtomic pair (which the memory backend stubs out as ErrServiceUnavailable).
func (*ProfileService) WithMinorDataMinimizer ¶ added in v1.4.0
func (s *ProfileService) WithMinorDataMinimizer(m MinorDataMinimizer) *ProfileService
WithMinorDataMinimizer wires COPPA data-minimization: when active, a CHILD-band account's profile updates drop non-essential PII (avatar URL). Returns the service for chaining. Off by default (zero-value is a no-op).
type ProjectBrandingConfig ¶ added in v1.3.0
type ProjectBrandingConfig struct {
// ProductName is the human-facing product name shown in email bodies
// (e.g. "Glassa Kids"). Empty falls back to the global default.
ProductName string `json:"product_name"`
// EmailFrom is the bare From address for this project's mail
// (e.g. "no-reply@kids.example.com"). Empty falls back to the global
// default (GATEWAY_EMAIL_BRAND_FROM, else GATEWAY_SMTP_FROM).
EmailFrom string `json:"email_from"`
// EmailFromName is the display name shown in the From header
// (e.g. "Glassa Kids"). Empty falls back to the global default.
EmailFromName string `json:"email_from_name"`
// LogoURL is an absolute https URL to the product logo, shown in HTML
// email. Empty omits the logo (today's behaviour).
LogoURL string `json:"logo_url"`
// PrimaryColor is a CSS colour (e.g. "#1a73e8") used to tint branded
// HTML email. Empty falls back to the global default.
PrimaryColor string `json:"primary_color"`
// SupportEmail is the address users can reply to / contact. When set it
// drives the Reply-To header and is shown in email footers. Empty omits
// Reply-To.
SupportEmail string `json:"support_email"`
}
ProjectBrandingConfig is the per-project transactional-email branding. All fields are optional. ProductName/LogoURL/PrimaryColor/SupportEmail are threaded into email template data; EmailFrom/EmailFromName build the SMTP From header; SupportEmail also drives the Reply-To header.
type ProjectCORSConfig ¶ added in v1.1.0
type ProjectCORSConfig struct {
AllowedOrigins []string `json:"allowed_origins"`
}
ProjectCORSConfig is the per-project CORS policy. AllowedOrigins is layered on top of the global GATEWAY_ALLOWED_ORIGINS floor: a browser origin is accepted when it is in either set. Each entry must be a bare scheme+host(+port) origin (no path/query/fragment, lower-case http:// or https:// scheme); the project resolver validates them with middleware.ParseAllowedOrigins before they reach a request.
type ProjectConfig ¶ added in v1.1.0
type ProjectConfig struct {
// CORS holds the project's browser cross-origin policy.
CORS ProjectCORSConfig `json:"cors"`
// Branding holds the project's transactional-email branding. Every field
// is optional; an unset field falls back to the global
// GATEWAY_EMAIL_BRAND_* default (and, when that too is unset, to today's
// byte-compatible output). This lets one server brand two products
// (e.g. a kids app and a B2B app) distinctly.
Branding ProjectBrandingConfig `json:"branding"`
// Passkey holds the project's WebAuthn relying-party identity. When set,
// it overrides the global GATEWAY_PASSKEY_* values for this project so a
// passkey registered under one product's domain validates under that
// product's RP-ID. Empty fields fall back to the global value.
Passkey ProjectPasskeyConfig `json:"passkey"`
// Login holds the project-wide login-method defaults applied to users
// who have NO claimed tenant (the common case for a consumer pool). A
// tenant's LoginPolicy, when one applies, fully overrides these.
Login ProjectLoginConfig `json:"login"`
}
ProjectConfig is the typed view of a project's config_json blob. It is the single decode target for everything an operator can configure per project in the control plane, so callers never reach into a raw map. New per-project knobs are added as fields here, not as scattered map lookups.
Unknown keys are tolerated (forward compatibility): a config written by a newer server still decodes on an older one, ignoring fields it does not understand.
func ParseProjectConfig ¶ added in v1.1.0
func ParseProjectConfig(configJSON string) (ProjectConfig, error)
ParseProjectConfig decodes a project's config_json into the typed ProjectConfig. An empty or all-whitespace blob is the zero config (no per-project overrides) — the same meaning the store gives "" by normalising it to "{}". A malformed blob is a configuration error the caller must surface, never silently swallow.
func (ProjectConfig) Validate ¶ added in v1.3.0
func (c ProjectConfig) Validate() error
Validate checks the optional branding/passkey blocks are well-formed when set. It is invariant-only: every field stays optional (an unset field is valid and means "fall back to the global default"); a *set* field that is malformed is a configuration error the write path must reject rather than persist a value that would later produce a broken email or a passkey that silently never validates.
type ProjectLoginConfig ¶ added in v1.3.0
type ProjectLoginConfig struct {
// AllowedMethods is a comma-separated allow-list of login method tokens
// (see service LoginMethod*). Empty means no project-wide restriction.
AllowedMethods string `json:"allowed_methods"`
// Require2FA forces a second factor after the primary method for every
// user in the project who is not governed by a tenant policy.
Require2FA bool `json:"require_2fa"`
}
ProjectLoginConfig is the project-wide login-method default, layered UNDER any tenant LoginPolicy (tenant overrides project overrides global). It lets an operator constrain authentication for an ENTIRE project — e.g. a kids pool that disables passkeys/OAuth/SSO — for users whose email domain maps to no claimed tenant, who would otherwise get no restriction at all.
The zero value is "no project-wide restriction", so an existing project with empty config_json behaves exactly as before.
type ProjectPasskeyConfig ¶ added in v1.3.0
type ProjectPasskeyConfig struct {
// RPID is the WebAuthn relying-party id (an effective domain, no scheme
// or port, e.g. "kids.example.com"). Empty falls back to the global
// GATEWAY_PASSKEY_RP_ID.
RPID string `json:"rp_id"`
// RPName is the human-facing relying-party name. Empty falls back to the
// global GATEWAY_PASSKEY_RP_NAME.
RPName string `json:"rp_name"`
// Origin is the expected WebAuthn origin (scheme+host(+port), e.g.
// "https://kids.example.com"). Empty falls back to the global
// GATEWAY_PASSKEY_ORIGIN.
Origin string `json:"origin"`
}
ProjectPasskeyConfig is the per-project WebAuthn relying-party identity.
type ProjectResolver ¶ added in v0.16.0
type ProjectResolver interface {
// ResolveByCredential resolves the active project a credential public
// id belongs to.
ResolveByCredential(ctx context.Context, publicID string) (*ResolvedProject, error)
// ResolveByHostname resolves the active project a serving hostname maps
// onto (case-insensitive).
ResolveByHostname(ctx context.Context, hostname string) (*ResolvedProject, error)
}
ProjectResolver resolves a request's project from the credentials it carries. It is implemented by the control-plane store (postgres only); deployments whose driver has no control plane pass a nil resolver and the middleware pins every request to the default project.
Both methods return (nil, nil) for a clean miss — an unknown key or an unmapped hostname — and a non-nil error only for an infrastructure failure. A resolver must NOT resolve a suspended project or a revoked credential; those are misses.
func NewCachingProjectResolver ¶ added in v1.2.0
func NewCachingProjectResolver(inner ProjectResolver, ttl time.Duration, maxEntries int) ProjectResolver
NewCachingProjectResolver wraps inner with a cache of the given TTL and max-entries bound. When inner is nil it returns nil (no control plane, so nothing to cache). When ttl <= 0 the cache is disabled and inner is returned unwrapped so the decorator adds no overhead. maxEntries <= 0 falls back to defaultProjectResolutionCacheMaxEntries so a misconfigured bound can never make the cache unbounded.
type ProjectScope ¶ added in v0.16.0
type ProjectScope struct {
// ProjectID is the resolved control-plane project id.
ProjectID string
// StorageScopeID is the physical storage scope the project maps onto.
// It is distinct from ProjectID (a project is a logical entity that
// points at a storage scope) and must not be conflated with it.
StorageScopeID string
// PrimaryAuthDomain is the project's primary serving hostname, when one
// is configured. The service builds branded links (email verification,
// password reset, magic-link, …) from it so a user sees a URL on the
// product's own domain. Empty when the project has no auth-domain, in
// which case the service falls back to its configured base URL.
PrimaryAuthDomain string
// CORSAllowedOrigins is the project's own browser CORS allow-list,
// parsed and validated from its config_json. It is layered on top of
// the global GATEWAY_ALLOWED_ORIGINS floor by the CORS middleware: a
// request whose Origin is in either set is allowed. Empty when the
// project configures none, in which case only the global floor applies.
CORSAllowedOrigins []string
// Branding is the project's transactional-email branding, parsed from
// its config_json. Empty fields fall back to the global
// GATEWAY_EMAIL_BRAND_* defaults so a zero-config project's mail is
// byte-compatible with today's.
Branding ProjectBrandingConfig
// Passkey is the project's WebAuthn relying-party identity, parsed from
// its config_json. Empty fields fall back to the global GATEWAY_PASSKEY_*
// values.
Passkey ProjectPasskeyConfig
// LoginDefaults is the project-wide login-method policy applied to users
// with NO claimed tenant, parsed from config_json. It is layered UNDER
// any tenant LoginPolicy (tenant overrides project overrides global) by
// the login-path enforcement. The zero value imposes no restriction, so
// a project that configures none behaves exactly as before.
LoginDefaults ProjectLoginConfig
}
ProjectScope is the per-request project binding carried in the request context by the project-resolution middleware. A Project is the redesign's top-level isolation entity (Firebase-project equivalent): it is resolved from a request's credential key or its Host header (via an auth-domain), ahead of any tenant resolution.
In a zero-config single-project deployment every request resolves to the default project, so the scope is always present once the middleware is installed; nothing downstream is forced to special-case its absence.
func ProjectScopeFromContext ¶ added in v0.16.0
func ProjectScopeFromContext(ctx context.Context) *ProjectScope
ProjectScopeFromContext returns the per-request project scope, or nil when none was injected (any code path that runs before resolution, or a deployment whose driver has no control plane).
type QrLoginSessionRecord ¶
type QrLoginSessionRecord struct {
NodeID string
SessionID string
Status string
UserID string
NewDeviceInfo string
NewDeviceIP string
NewDeviceUserAgent string
ApprovedDeviceInfo string
// PollSecretHash is sha256(poll_secret) where poll_secret is returned
// only to the initiating device by InitiateQrLogin. PollQrLogin must
// present the matching plaintext or the session looks "expired".
PollSecretHash string
ExpiresAt int64
CreatedAt int64
UpdatedAt int64
}
QrLoginSessionRecord represents a stored QR login session.
type QrSessionInfo ¶
type QrSessionInfo struct {
Status string
NewDeviceInfo string
NewDeviceIP string
ExpiresAt time.Time
}
QrSessionInfo holds the public details of a QR login session.
type RecoveryCodeRecord ¶
type RecoveryCodeRecord struct {
NodeID string
UserID string
CodeHash string
Used bool
CreatedAt int64
UsedAt int64
}
RecoveryCodeRecord represents a stored recovery code.
type RefreshTokenRecord ¶
type RefreshTokenRecord struct {
NodeID string
TokenHash string
UserID string
DeviceInfo string
DeviceName string
IPAddress string
UserAgent string
ExpiresAt int64 // epoch ms
CreatedAt int64
LastUsedAt int64
ConsumedAtMs int64 // epoch ms; 0 = unconsumed (still valid for refresh)
}
RefreshTokenRecord represents a stored refresh token.
type RegisteredAuthDomain ¶ added in v1.1.0
type RegisteredAuthDomain struct {
Domain *AdminProjectAuthDomain
TXTName string
TXTValue string
}
RegisteredAuthDomain is the result of AddProjectAuthDomain: the registered (still-UNVERIFIED) domain plus the deterministic DNS TXT challenge the caller must publish before VerifyProjectAuthDomain succeeds.
type Repository ¶
type Repository interface {
// Users
FindUserByEmail(ctx context.Context, email string) (*User, error)
GetUser(ctx context.Context, userID string) (*User, error)
CreateUser(ctx context.Context, u *User) (string, error) // returns node ID
UpdateUser(ctx context.Context, userID string, fields map[string]any) error
// DeleteUser physically removes the user and every user_id-keyed
// record, synchronously, on every driver. This covers the durable
// identity/auth material — sessions, refresh tokens, oauth identities,
// passkey credentials, totp credentials, recovery codes, identity
// verifications, and phone verification codes — and, as of #168, the
// short-lived tokens too: password-reset, email-verification/change,
// passkey and login challenges, qr sessions, oauth one-time codes, and
// invitations.
// After it, GetUser returns nil and the email is reusable for a new
// CreateUser.
//
// The only artifacts NOT removed synchronously are the email-keyed
// login codes / magic-link tokens, which carry no user_id and so
// cannot be enumerated per-user; they are reaped by the TTL sweepers —
// safe, since they expire quickly, reference a now-deleted user, and
// never block email reuse. audit_events are retained for
// accountability. Idempotent: deleting a non-existent user returns nil.
//
// (Invitations are drained but are the one user_id-keyed type the
// cross-driver conformance suite cannot seed/assert — Repository
// exposes no invitation create method; they are written via the graph
// graph.)
DeleteUser(ctx context.Context, userID string) error
// Lockout state. These are dedicated methods (rather than UpdateUser
// patches) so the persistence layer can implement them as single
// atomic writes — important for racing concurrent failed-login
// attempts on the same account.
IncrementFailedLoginCount(ctx context.Context, userID string) (newCount int32, err error)
ResetFailedLoginCount(ctx context.Context, userID string) error
SetUserLockedUntil(ctx context.Context, userID string, lockedUntilMs int64) error
// Sessions (mode=session revocation). The verification middleware
// looks the row up by SID on every authenticated request (via an
// in-process cache; see internal/middleware/session.go). A non-zero
// RevokedAtMs means the session has been killed and any access
// token carrying this SID must be rejected.
//
// CreateSession is invoked from the token-issuance path when
// `mode=session`. GetSessionBySid is the read on the hot path;
// RevokeSession atomically marks one session revoked; and
// RevokeSessionsForUser is invoked from DeleteRefreshTokensForUser
// so the existing replay-detection path also kills the access
// tokens.
//
// Implementations MUST guarantee SID uniqueness; CreateSession
// returns ErrAlreadyExists when the SID collides. RevokeSession
// is idempotent — re-revoking an already-revoked session is a
// no-op rather than an error so concurrent revoke calls don't
// race each other into failure.
CreateSession(ctx context.Context, s *SessionRecord) (string, error)
GetSessionBySid(ctx context.Context, sid string) (*SessionRecord, error)
RevokeSession(ctx context.Context, sid string, atMs int64) error
RevokeSessionsForUser(ctx context.Context, userID string, atMs int64) error
// Refresh tokens
FindRefreshTokenByHash(ctx context.Context, hash string) (*RefreshTokenRecord, error)
// FindRefreshTokenByHashIncludingConsumed returns the row even when
// consumed_at != 0, so the replay-detection branch can identify the
// user_id whose sessions must be invalidated.
FindRefreshTokenByHashIncludingConsumed(ctx context.Context, hash string) (*RefreshTokenRecord, error)
CreateRefreshToken(ctx context.Context, r *RefreshTokenRecord) (string, error)
DeleteRefreshToken(ctx context.Context, nodeID string) error
DeleteRefreshTokensForUser(ctx context.Context, userID string) error
// ConsumeRefreshTokenByHash marks the refresh-token row as rotated by
// setting consumed_at = atMs. The row is NOT deleted: replay attempts
// must still find it so they can be detected. The implementation must
// only mark a row consumed if it is currently unconsumed (consumed_at
// == 0); concurrent rotations of the same token must result in
// exactly one caller succeeding. Returns ErrUnauthenticated when the
// row is already consumed or does not exist.
//
// Background sweep: rows with consumed_at older than the desired
// retention window (e.g. 90 days, comfortably exceeding the longest
// refresh-token lifetime) may be hard-deleted by a periodic job. Such
// a sweep is not implemented in this package.
ConsumeRefreshTokenByHash(ctx context.Context, hash string, atMs int64) error
// Passkey credentials
ListPasskeyCredentials(ctx context.Context, userID string) ([]*PasskeyCredRecord, error)
GetPasskeyCredentialByCredID(ctx context.Context, credentialID string) (*PasskeyCredRecord, error)
CreatePasskeyCredential(ctx context.Context, r *PasskeyCredRecord) (string, error)
UpdatePasskeyCredential(ctx context.Context, nodeID string, fields map[string]any) error
// Passkey challenges
GetPasskeyChallenge(ctx context.Context, nodeID string) (*PasskeyChallengeRecord, error)
CreatePasskeyChallenge(ctx context.Context, r *PasskeyChallengeRecord) (string, error)
DeletePasskeyChallenge(ctx context.Context, nodeID string) error
// QR login sessions
FindQrLoginSession(ctx context.Context, sessionID string) (*QrLoginSessionRecord, error)
CreateQrLoginSession(ctx context.Context, r *QrLoginSessionRecord) (string, error)
UpdateQrLoginSession(ctx context.Context, nodeID string, fields map[string]any) error
// ConsumeQrLoginSession atomically transitions a QR login session
// from status="approved" to status="consumed", setting updated_at
// to atMs. Implementations must guarantee single-winner semantics
// across concurrent callers: exactly one of N replicas polling the
// same approved session may complete the transition; the rest see
// ErrQrLoginNotPending and must NOT mint tokens. This is the
// serialization point for the QR-login token-issuance flow in
// multi-replica deployments. Returns ErrQrLoginNotPending when the
// session does not exist or is no longer in the "approved" state.
ConsumeQrLoginSession(ctx context.Context, nodeID string, atMs int64) error
// OAuth one-time codes (hosted-flow SPA handover).
//
// CreateOAuthOneTimeCode stores the code-hash → user binding written
// by the hosted /oauth/callback handler. ConsumeOAuthOneTimeCode is
// the single-winner compare-and-set: it marks the row consumed
// (consumed_at = atMs) only if it is currently unconsumed
// (consumed_at == 0) AND not yet expired (expires_at > atMs),
// returning the bound record on success. A replay, an expired code,
// or a code that does not exist all return ErrOAuthCodeInvalid so
// the redeem endpoint cannot be probed. Like ConsumeQrLoginSession
// this is the serialization point across replicas: exactly one of N
// concurrent redeems of the same code wins.
CreateOAuthOneTimeCode(ctx context.Context, r *OAuthOneTimeCodeRecord) (string, error)
ConsumeOAuthOneTimeCode(ctx context.Context, codeHash string, atMs int64) (*OAuthOneTimeCodeRecord, error)
// Email login codes (OTP arm of passwordless email login).
//
// UpsertEmailLoginCode stores the latest code for an email, replacing
// any existing live code for that address so at most one is valid at a
// time (a re-request invalidates the previous code). It is keyed on
// email; the unique index makes the upsert a delete-then-create or an
// in-place overwrite depending on the backend.
//
// FindEmailLoginCodeByEmail returns the live row (consumed or not) so
// the verify path can compare the code hash, count attempts, and
// distinguish expired/consumed from a hash mismatch. Returns nil when
// no row exists for the email.
//
// IncrementEmailLoginCodeAttempts bumps attempt_count by one. Used on
// a wrong-code guess; the service invalidates the code once the count
// reaches the cap captured on the record.
//
// ConsumeEmailLoginCode is the single-winner compare-and-set: it marks
// the row consumed (consumed_at = atMs) only when currently unconsumed
// AND unexpired, returning the bound record on success. A replay, an
// expired code, or a missing code all return ErrEmailLoginCodeInvalid.
UpsertEmailLoginCode(ctx context.Context, r *EmailLoginCodeRecord) (string, error)
FindEmailLoginCodeByEmail(ctx context.Context, email string) (*EmailLoginCodeRecord, error)
IncrementEmailLoginCodeAttempts(ctx context.Context, nodeID string) error
ConsumeEmailLoginCode(ctx context.Context, email string, atMs int64) (*EmailLoginCodeRecord, error)
// Magic-link tokens (magic-link arm of passwordless email login).
//
// CreateMagicLinkToken stores the token-hash → (email, return_to)
// binding. ConsumeMagicLinkToken is the single-winner compare-and-set
// (same shape as ConsumeOAuthOneTimeCode): it marks the row consumed
// only when currently unconsumed AND unexpired, returning the bound
// record on success. A replay, expired, or missing token all return
// ErrMagicLinkInvalid.
CreateMagicLinkToken(ctx context.Context, r *MagicLinkTokenRecord) (string, error)
ConsumeMagicLinkToken(ctx context.Context, tokenHash string, atMs int64) (*MagicLinkTokenRecord, error)
// Phone verification codes (SMS-OTP phone-ownership verification).
//
// UpsertPhoneVerificationCode stores the latest code for a user,
// replacing any existing live code for that user so at most one is
// valid at a time (a re-request invalidates the previous code). It is
// keyed on user_id.
//
// FindPhoneVerificationCodeByUser returns the live row (consumed or
// not) so the verify path can compare the code hash, count attempts,
// and distinguish expired/consumed from a hash mismatch. Returns nil
// when no row exists for the user.
//
// IncrementPhoneVerificationCodeAttempts bumps attempt_count by one,
// used on a wrong-code guess; the service invalidates the code once
// the count reaches the cap captured on the record.
//
// ConsumePhoneVerificationCode is the single-winner compare-and-set:
// it marks the row consumed (consumed_at = atMs) only when currently
// unconsumed AND unexpired, returning the bound record on success. A
// replay, an expired code, or a missing code all return
// ErrPhoneCodeInvalid.
//
// SetUserPhoneVerified records the verified phone on the user and
// flips phone_verified, mirroring SetUserEmailVerified.
UpsertPhoneVerificationCode(ctx context.Context, r *PhoneVerificationCodeRecord) (string, error)
FindPhoneVerificationCodeByUser(ctx context.Context, userID string) (*PhoneVerificationCodeRecord, error)
IncrementPhoneVerificationCodeAttempts(ctx context.Context, nodeID string) error
ConsumePhoneVerificationCode(ctx context.Context, userID string, atMs int64) (*PhoneVerificationCodeRecord, error)
SetUserPhoneVerified(ctx context.Context, userID, phoneNumber string, atMs int64) error
// TOTP credentials
GetTotpCredential(ctx context.Context, userID string) (*TotpCredRecord, error)
CreateTotpCredential(ctx context.Context, r *TotpCredRecord) (string, error)
UpdateTotpCredential(ctx context.Context, nodeID string, fields map[string]any) error
DeleteTotpCredential(ctx context.Context, nodeID string) error
DeleteTotpCredentialsForUser(ctx context.Context, userID string) error
// Recovery codes
CreateRecoveryCode(ctx context.Context, r *RecoveryCodeRecord) (string, error)
FindRecoveryCodeByHash(ctx context.Context, userID, hash string) (*RecoveryCodeRecord, error)
UpdateRecoveryCode(ctx context.Context, nodeID string, fields map[string]any) error
DeleteRecoveryCodesForUser(ctx context.Context, userID string) error
// Login challenges (TOTP 2FA step)
CreateLoginChallenge(ctx context.Context, r *LoginChallengeRecord) (string, error)
GetLoginChallengeByChallengeID(ctx context.Context, challengeID string) (*LoginChallengeRecord, error)
DeleteLoginChallenge(ctx context.Context, nodeID string) error
// User invitations
FindInvitationByHash(ctx context.Context, tokenHash string) (*InvitationRecord, error)
UpdateInvitation(ctx context.Context, nodeID string, fields map[string]any) error
// Password-reset tokens
CreatePasswordResetToken(ctx context.Context, t *PasswordResetToken) error
FindPasswordResetTokenByHash(ctx context.Context, tokenHash string) (*PasswordResetToken, error)
MarkPasswordResetTokenConsumed(ctx context.Context, tokenID string, atMs int64) error
// Email-verification tokens
CreateEmailVerificationToken(ctx context.Context, t *EmailVerificationToken) error
FindEmailVerificationTokenByHash(ctx context.Context, tokenHash string) (*EmailVerificationToken, error)
MarkEmailVerificationTokenConsumed(ctx context.Context, tokenID string, atMs int64) error
// User email-verified update
SetUserEmailVerified(ctx context.Context, userID string, atMs int64) error
// User idv-verified update; called by IdentityVerificationService
// when a verification reaches APPROVED.
SetUserIDVVerified(ctx context.Context, userID string, atMs int64) error
// Identity-verification records (document + selfie verification).
// Latest ordering is by CreatedAt descending.
CreateIdentityVerification(ctx context.Context, r *IdentityVerificationRecord) error
GetIdentityVerification(ctx context.Context, verificationID string) (*IdentityVerificationRecord, error)
GetLatestIdentityVerificationForUser(ctx context.Context, userID string) (*IdentityVerificationRecord, error)
UpdateIdentityVerificationStatus(ctx context.Context, verificationID, status, rejectionReason string, completedAtMs, updatedAtMs int64) error
// Email-change tokens (primary email rotation, double-opt-in)
CreateEmailChangeToken(ctx context.Context, t *EmailChangeToken) error
FindEmailChangeTokenByHash(ctx context.Context, tokenHash string) (*EmailChangeToken, error)
MarkEmailChangeTokenConsumed(ctx context.Context, tokenID string, atMs int64) error
// UpdateUserEmail sets the user's primary email and marks it verified
// (since the new address has just proven control via the consumed
// token). Implementations must also set updated_at = atMs.
UpdateUserEmail(ctx context.Context, userID, newEmail string, atMs int64) error
// OAuth identities — links a (provider, provider_user_id) pair to a
// local User so OAuth login can survive provider-side email changes.
FindUserByProviderID(ctx context.Context, provider, providerUserID string) (*User, error)
CreateOAuthIdentity(ctx context.Context, oi *OAuthIdentity) error
ListOAuthIdentitiesForUser(ctx context.Context, userID string) ([]*OAuthIdentity, error)
// DeleteOAuthIdentity removes the (provider, provider_user_id) link
// owned by userID. It is scoped to the owning user so one user can
// never unlink another user's identity. Implementations return
// ErrNotFound when no matching link exists for that user.
DeleteOAuthIdentity(ctx context.Context, userID, provider, providerUserID string) error
// Garbage-collection sweepers for ephemeral state. The
// background sweeper started by app.New calls these every
// GATEWAY_SWEEPER_INTERVAL_SECONDS; each call deletes up to
// `limit` rows whose ExpiresAt is strictly less than `beforeMs`.
// Every shipping backend (memory, postgres) implements the
// real sweep; the ErrSweepNotImplemented sentinel remains so a new
// backend can land its CRUD methods first and its sweep in a
// follow-up PR without erroring the sweeper goroutine.
// Implementations MUST reject limit <= 0 — an unbounded delete
// batch could lock a hot table for an unbounded window.
//
// Return value is only error: tenant-shard-db v1.14.0's
// OpDeleteWhere primitive intentionally does not return a deleted-
// row count (see #540 "applied, no count for v1"), so identity
// drops the count from the contract to avoid forcing one of the
// three backends into a per-row tally that the others can't
// match. The app-layer sweeper emits per-tick "sweep completed"
// events instead of a row count.
DeleteExpiredWebAuthnChallenges(ctx context.Context, beforeMs int64, limit int) error
DeleteExpiredEmailVerificationTokens(ctx context.Context, beforeMs int64, limit int) error
DeleteExpiredPasswordResetTokens(ctx context.Context, beforeMs int64, limit int) error
DeleteExpiredEmailChangeTokens(ctx context.Context, beforeMs int64, limit int) error
DeleteExpiredLoginChallenges(ctx context.Context, beforeMs int64, limit int) error
DeleteExpiredOAuthOneTimeCodes(ctx context.Context, beforeMs int64, limit int) error
DeleteExpiredEmailLoginCodes(ctx context.Context, beforeMs int64, limit int) error
DeleteExpiredMagicLinkTokens(ctx context.Context, beforeMs int64, limit int) error
DeleteExpiredPhoneVerificationCodes(ctx context.Context, beforeMs int64, limit int) error
DeleteExpiredQrLoginSessions(ctx context.Context, beforeMs int64, limit int) error
DeleteExpiredInvitations(ctx context.Context, beforeMs int64, limit int) error
}
Repository abstracts all persistence operations for the auth service.
type ResetPasswordResult ¶
ResetPasswordResult is returned by AdminService.ResetUserPassword.
type ResolvedProject ¶ added in v0.16.0
type ResolvedProject struct {
ID string
StorageScopeID string
PrimaryAuthDomain string
CORSAllowedOrigins []string
Branding ProjectBrandingConfig
Passkey ProjectPasskeyConfig
LoginDefaults ProjectLoginConfig
}
ResolvedProject is what a ProjectResolver returns: the minimal project identity the middleware needs to build a ProjectScope. It is a driver-agnostic value so the resolver contract does not leak a concrete store type into the middleware or app wiring.
type ReturnAllowlist ¶ added in v0.11.0
type ReturnAllowlist struct {
// contains filtered or unexported fields
}
ReturnAllowlist is the fail-closed validator for an app return_to URL. It is built from GATEWAY_OAUTH_ALLOWED_RETURN_URLS — a comma-separated list of exact origins or path prefixes — and shared by the hosted OAuth flow (where the HTTP handler checks return_to at /oauth/start) and the passwordless magic-link flow (where RequestMagicLink checks the requested return_to). A return_to must have the configured origin and, for path entries, match the configured path or one of its descendants.
An empty allowlist disables both flows that depend on it: Enabled() reports false and Allows() rejects everything.
func ParseReturnAllowlist ¶ added in v0.11.0
func ParseReturnAllowlist(csv string) ReturnAllowlist
ParseReturnAllowlist splits the comma-separated config value into trimmed, non-empty entries. Whitespace-only entries are dropped.
func (ReturnAllowlist) Allows ¶ added in v0.11.0
func (a ReturnAllowlist) Allows(returnTo string) bool
Allows reports whether returnTo is permitted by an exact origin or a path-bound prefix. Allowlist entries may not contain a query or fragment.
func (ReturnAllowlist) Enabled ¶ added in v0.11.0
func (a ReturnAllowlist) Enabled() bool
Enabled reports whether any allowlist entry is configured.
func (ReturnAllowlist) Entries ¶ added in v0.11.0
func (a ReturnAllowlist) Entries() []string
Entries returns the configured allowlist entries (for startup logging).
type Session ¶
type Session struct {
ID string
DeviceName string
IPAddress string
UserAgent string
CreatedAt int64
LastUsedAt int64
ExpiresAt int64
Current bool
}
Session represents an active refresh-token-backed session.
type SessionRecord ¶ added in v0.8.0
type SessionRecord struct {
NodeID string
SID string
UserID string
CreatedAtMs int64 // epoch ms
RevokedAtMs int64 // epoch ms; 0 = active
}
SessionRecord represents a stored access-token-bound session, used by `GATEWAY_REVOCATION_MODE=session` deployments. The SID is the stable per-session identifier carried as the `sid` claim on access tokens; the verification middleware looks the row up on every authenticated request (via an in-process cache).
In `mode=ttl` deployments (the default) this record type is not written or read — the row type costs zero on the hot path for deployers who never opt in.
type StubDB ¶
type StubDB struct{}
StubDB implements DB (and audit.NodeWriter) but returns ErrServiceUnavailable for every method.
func (StubDB) ExecuteAtomic ¶
func (StubDB) GetEdgesFrom ¶
func (StubDB) GetEdgesTo ¶
func (StubDB) QueryNodes ¶
func (StubDB) RegisterUserInTenant ¶ added in v0.7.1
type StubRepository ¶
type StubRepository struct{}
StubRepository implements Repository but returns ErrServiceUnavailable for every method. Use it as a placeholder until the real repository is implemented.
func (StubRepository) ConsumeEmailLoginCode ¶ added in v0.11.0
func (StubRepository) ConsumeEmailLoginCode(context.Context, string, int64) (*EmailLoginCodeRecord, error)
func (StubRepository) ConsumeMagicLinkToken ¶ added in v0.11.0
func (StubRepository) ConsumeMagicLinkToken(context.Context, string, int64) (*MagicLinkTokenRecord, error)
func (StubRepository) ConsumeOAuthOneTimeCode ¶ added in v0.9.0
func (StubRepository) ConsumeOAuthOneTimeCode(context.Context, string, int64) (*OAuthOneTimeCodeRecord, error)
func (StubRepository) ConsumePhoneVerificationCode ¶ added in v0.14.0
func (StubRepository) ConsumePhoneVerificationCode(context.Context, string, int64) (*PhoneVerificationCodeRecord, error)
func (StubRepository) ConsumeQrLoginSession ¶ added in v0.7.2
func (StubRepository) ConsumeRefreshTokenByHash ¶
func (StubRepository) CreateEmailChangeToken ¶
func (StubRepository) CreateEmailChangeToken(context.Context, *EmailChangeToken) error
func (StubRepository) CreateEmailVerificationToken ¶
func (StubRepository) CreateEmailVerificationToken(context.Context, *EmailVerificationToken) error
func (StubRepository) CreateIdentityVerification ¶ added in v0.4.0
func (StubRepository) CreateIdentityVerification(context.Context, *IdentityVerificationRecord) error
func (StubRepository) CreateLoginChallenge ¶
func (StubRepository) CreateLoginChallenge(context.Context, *LoginChallengeRecord) (string, error)
func (StubRepository) CreateMagicLinkToken ¶ added in v0.11.0
func (StubRepository) CreateMagicLinkToken(context.Context, *MagicLinkTokenRecord) (string, error)
func (StubRepository) CreateOAuthIdentity ¶
func (StubRepository) CreateOAuthIdentity(context.Context, *OAuthIdentity) error
func (StubRepository) CreateOAuthOneTimeCode ¶ added in v0.9.0
func (StubRepository) CreateOAuthOneTimeCode(context.Context, *OAuthOneTimeCodeRecord) (string, error)
func (StubRepository) CreatePasskeyChallenge ¶
func (StubRepository) CreatePasskeyChallenge(context.Context, *PasskeyChallengeRecord) (string, error)
func (StubRepository) CreatePasskeyCredential ¶
func (StubRepository) CreatePasskeyCredential(context.Context, *PasskeyCredRecord) (string, error)
func (StubRepository) CreatePasswordResetToken ¶
func (StubRepository) CreatePasswordResetToken(context.Context, *PasswordResetToken) error
func (StubRepository) CreateQrLoginSession ¶
func (StubRepository) CreateQrLoginSession(context.Context, *QrLoginSessionRecord) (string, error)
func (StubRepository) CreateRecoveryCode ¶
func (StubRepository) CreateRecoveryCode(context.Context, *RecoveryCodeRecord) (string, error)
func (StubRepository) CreateRefreshToken ¶
func (StubRepository) CreateRefreshToken(context.Context, *RefreshTokenRecord) (string, error)
func (StubRepository) CreateSession ¶ added in v0.8.0
func (StubRepository) CreateSession(context.Context, *SessionRecord) (string, error)
func (StubRepository) CreateTotpCredential ¶
func (StubRepository) CreateTotpCredential(context.Context, *TotpCredRecord) (string, error)
func (StubRepository) CreateUser ¶
func (StubRepository) DeleteExpiredEmailChangeTokens ¶ added in v0.7.1
func (StubRepository) DeleteExpiredEmailLoginCodes ¶ added in v0.11.0
func (StubRepository) DeleteExpiredEmailVerificationTokens ¶ added in v0.7.1
func (StubRepository) DeleteExpiredInvitations ¶ added in v0.15.0
func (StubRepository) DeleteExpiredLoginChallenges ¶ added in v0.7.1
func (StubRepository) DeleteExpiredMagicLinkTokens ¶ added in v0.11.0
func (StubRepository) DeleteExpiredOAuthOneTimeCodes ¶ added in v0.9.0
func (StubRepository) DeleteExpiredPasswordResetTokens ¶ added in v0.7.1
func (StubRepository) DeleteExpiredPhoneVerificationCodes ¶ added in v0.14.0
func (StubRepository) DeleteExpiredQrLoginSessions ¶ added in v0.15.0
func (StubRepository) DeleteExpiredWebAuthnChallenges ¶ added in v0.7.1
func (StubRepository) DeleteLoginChallenge ¶
func (StubRepository) DeleteLoginChallenge(context.Context, string) error
func (StubRepository) DeleteOAuthIdentity ¶ added in v1.3.0
func (StubRepository) DeletePasskeyChallenge ¶
func (StubRepository) DeletePasskeyChallenge(context.Context, string) error
func (StubRepository) DeleteRecoveryCodesForUser ¶
func (StubRepository) DeleteRecoveryCodesForUser(context.Context, string) error
func (StubRepository) DeleteRefreshToken ¶
func (StubRepository) DeleteRefreshToken(context.Context, string) error
func (StubRepository) DeleteRefreshTokensForUser ¶
func (StubRepository) DeleteRefreshTokensForUser(context.Context, string) error
func (StubRepository) DeleteTotpCredential ¶
func (StubRepository) DeleteTotpCredential(context.Context, string) error
func (StubRepository) DeleteTotpCredentialsForUser ¶
func (StubRepository) DeleteTotpCredentialsForUser(context.Context, string) error
func (StubRepository) DeleteUser ¶ added in v0.13.0
func (StubRepository) DeleteUser(context.Context, string) error
func (StubRepository) FindEmailChangeTokenByHash ¶
func (StubRepository) FindEmailChangeTokenByHash(context.Context, string) (*EmailChangeToken, error)
func (StubRepository) FindEmailLoginCodeByEmail ¶ added in v0.11.0
func (StubRepository) FindEmailLoginCodeByEmail(context.Context, string) (*EmailLoginCodeRecord, error)
func (StubRepository) FindEmailVerificationTokenByHash ¶
func (StubRepository) FindEmailVerificationTokenByHash(context.Context, string) (*EmailVerificationToken, error)
func (StubRepository) FindInvitationByHash ¶
func (StubRepository) FindInvitationByHash(context.Context, string) (*InvitationRecord, error)
func (StubRepository) FindPasswordResetTokenByHash ¶
func (StubRepository) FindPasswordResetTokenByHash(context.Context, string) (*PasswordResetToken, error)
func (StubRepository) FindPhoneVerificationCodeByUser ¶ added in v0.14.0
func (StubRepository) FindPhoneVerificationCodeByUser(context.Context, string) (*PhoneVerificationCodeRecord, error)
func (StubRepository) FindQrLoginSession ¶
func (StubRepository) FindQrLoginSession(context.Context, string) (*QrLoginSessionRecord, error)
func (StubRepository) FindRecoveryCodeByHash ¶
func (StubRepository) FindRecoveryCodeByHash(context.Context, string, string) (*RecoveryCodeRecord, error)
func (StubRepository) FindRefreshTokenByHash ¶
func (StubRepository) FindRefreshTokenByHash(context.Context, string) (*RefreshTokenRecord, error)
func (StubRepository) FindRefreshTokenByHashIncludingConsumed ¶
func (StubRepository) FindRefreshTokenByHashIncludingConsumed(context.Context, string) (*RefreshTokenRecord, error)
func (StubRepository) FindUserByEmail ¶
func (StubRepository) FindUserByProviderID ¶
func (StubRepository) GetIdentityVerification ¶ added in v0.4.0
func (StubRepository) GetIdentityVerification(context.Context, string) (*IdentityVerificationRecord, error)
func (StubRepository) GetLatestIdentityVerificationForUser ¶ added in v0.4.0
func (StubRepository) GetLatestIdentityVerificationForUser(context.Context, string) (*IdentityVerificationRecord, error)
func (StubRepository) GetLoginChallengeByChallengeID ¶
func (StubRepository) GetLoginChallengeByChallengeID(context.Context, string) (*LoginChallengeRecord, error)
func (StubRepository) GetPasskeyChallenge ¶
func (StubRepository) GetPasskeyChallenge(context.Context, string) (*PasskeyChallengeRecord, error)
func (StubRepository) GetPasskeyCredentialByCredID ¶
func (StubRepository) GetPasskeyCredentialByCredID(context.Context, string) (*PasskeyCredRecord, error)
func (StubRepository) GetSessionBySid ¶ added in v0.8.0
func (StubRepository) GetSessionBySid(context.Context, string) (*SessionRecord, error)
func (StubRepository) GetTotpCredential ¶
func (StubRepository) GetTotpCredential(context.Context, string) (*TotpCredRecord, error)
func (StubRepository) IncrementEmailLoginCodeAttempts ¶ added in v0.11.0
func (StubRepository) IncrementEmailLoginCodeAttempts(context.Context, string) error
func (StubRepository) IncrementFailedLoginCount ¶
func (StubRepository) IncrementPhoneVerificationCodeAttempts ¶ added in v0.14.0
func (StubRepository) IncrementPhoneVerificationCodeAttempts(context.Context, string) error
func (StubRepository) ListOAuthIdentitiesForUser ¶
func (StubRepository) ListOAuthIdentitiesForUser(context.Context, string) ([]*OAuthIdentity, error)
func (StubRepository) ListPasskeyCredentials ¶
func (StubRepository) ListPasskeyCredentials(context.Context, string) ([]*PasskeyCredRecord, error)
func (StubRepository) MarkEmailChangeTokenConsumed ¶
func (StubRepository) MarkEmailVerificationTokenConsumed ¶
func (StubRepository) MarkPasswordResetTokenConsumed ¶
func (StubRepository) ResetFailedLoginCount ¶
func (StubRepository) ResetFailedLoginCount(context.Context, string) error
func (StubRepository) RevokeSession ¶ added in v0.8.0
func (StubRepository) RevokeSessionsForUser ¶ added in v0.8.0
func (StubRepository) SetUserEmailVerified ¶
func (StubRepository) SetUserIDVVerified ¶ added in v0.4.2
func (StubRepository) SetUserLockedUntil ¶
func (StubRepository) SetUserPhoneVerified ¶ added in v0.14.0
func (StubRepository) UpdateIdentityVerificationStatus ¶ added in v0.4.0
func (StubRepository) UpdateInvitation ¶
func (StubRepository) UpdatePasskeyCredential ¶
func (StubRepository) UpdateQrLoginSession ¶
func (StubRepository) UpdateRecoveryCode ¶
func (StubRepository) UpdateTotpCredential ¶
func (StubRepository) UpdateUser ¶
func (StubRepository) UpdateUserEmail ¶
func (StubRepository) UpsertEmailLoginCode ¶ added in v0.11.0
func (StubRepository) UpsertEmailLoginCode(context.Context, *EmailLoginCodeRecord) (string, error)
func (StubRepository) UpsertPhoneVerificationCode ¶ added in v0.14.0
func (StubRepository) UpsertPhoneVerificationCode(context.Context, *PhoneVerificationCodeRecord) (string, error)
type Tenant ¶ added in v0.17.0
type Tenant struct {
ID string
ProjectID string
Name string
PrimaryDomain string
Status string
CreatedAtMs int64
UpdatedAtMs int64
}
Tenant is a company-governance entity within a Project, auto-formed per verified non-public email domain. It owns email Domains, a LoginPolicy, and tenant memberships. A Tenant is `latent` until one of its domains is verified, at which point it becomes `claimed` and authoritative.
type TenantAutoFormStore ¶ added in v0.17.0
type TenantAutoFormStore interface {
// EnsureTenantForDomain idempotently ensures a latent Tenant and its
// email Domain exist for (projectID, domain), then records a
// domain-derived membership for userID. One tenant per email domain
// within a project is enforced transactionally — the tenant and domain
// are created in a single transaction, so a lost race against a
// concurrent signer rolls BOTH back (no orphan tenant) and the winner's
// tenant is used. Returns the resolved tenant id.
//
// The caller must only invoke this for a non-public domain (see
// config.IsPublicEmailDomain); the store does not re-check.
EnsureTenantForDomain(ctx context.Context, projectID, domain, userID string) (string, error)
}
TenantAutoFormStore is the transactional auto-formation primitive: it turns "a user signed up with a company email domain" into the governance rows that represent it, atomically and idempotently.
type TenantInvitation ¶ added in v0.17.0
type TenantInvitation struct {
ID string
ProjectID string
TenantID string
TokenHash string
Email string
// InvitedBy is provenance only — intentionally not an FK, so deleting
// the inviter neither blocks the delete nor rewrites the audit trail.
InvitedBy string
Role string
Status string
ExpiresAtMs int64
AcceptedAtMs int64
CreatedAtMs int64
}
TenantInvitation is a pending offer to join a Tenant, addressed to an email and redeemed by a hashed token. At most one open (pending) invitation per (project, tenant, email).
type TenantMembership ¶ added in v0.17.0
type TenantMembership struct {
ID string
ProjectID string
TenantID string
UserID string
Source string
Role string
Status string
CreatedAtMs int64
UpdatedAtMs int64
}
TenantMembership is a user's materialized relationship to a Tenant within a Project. At most one row per (project, tenant, user).
type TenantStore ¶ added in v0.17.0
type TenantStore interface {
// CreateTenant inserts a tenant. ProjectID is required; a blank id is
// generated and written back. The assigned id is returned.
CreateTenant(ctx context.Context, t *Tenant) (string, error)
// GetTenant returns the tenant by id within a project, or (nil, nil).
GetTenant(ctx context.Context, projectID, tenantID string) (*Tenant, error)
// GetTenantByPrimaryDomain returns the tenant whose primary_domain
// equals domain (case-insensitive) within a project, or (nil, nil).
GetTenantByPrimaryDomain(ctx context.Context, projectID, domain string) (*Tenant, error)
// SetTenantStatus transitions a tenant's status (e.g. latent→claimed)
// and stamps updated_at. Unknown ids are a no-op.
SetTenantStatus(ctx context.Context, projectID, tenantID, status string) error
// ListTenants returns every tenant in a project, newest first.
ListTenants(ctx context.Context, projectID string) ([]*Tenant, error)
}
TenantStore persists Tenants within a Project. Reads that miss return (nil, nil), never an error; only infrastructure failures error.
type TotpCredRecord ¶
type TotpCredRecord struct {
NodeID string
UserID string
SecretEncrypted string
Verified bool
CreatedAt int64
LastUsedAt int64
}
TotpCredRecord represents a stored TOTP credential.
type User ¶
type User struct {
ID string
Email string
Name string
AvatarURL string
Role string
CreatedAt time.Time
UpdatedAt time.Time
TotpRequired bool
Status string // "active", "invited", "deactivated", "suspended"
RecoveryEmail string
QuotaBytes int64
LastLoginAtMs int64
PasswordHash string // never exposed via RPC
FailedLoginCount int
LockedUntil int64 // epoch ms; 0 = not locked
EmailVerified bool
EmailVerifiedAt int64 // epoch ms
IDVVerified bool // latest identity verification reached APPROVED
IDVVerifiedAt int64 // epoch ms; 0 = never verified
PhoneNumber string
PhoneVerified bool
PhoneVerifiedAt int64 // epoch ms; 0 = never verified
DateOfBirthMs int64 // epoch ms of date of birth; 0 = unknown (persisted)
// IsMinor and AgeBand are DERIVED from DateOfBirthMs + the age-gate
// configuration; they are NOT persisted. The service stamps them on a
// user before returning it so the handler/JWT layers can read a single
// authoritative value.
IsMinor bool
AgeBand string // "CHILD" | "TEEN" | "ADULT" | "" (unknown)
}
User represents a user in the identity system.
type UserDirectory ¶ added in v0.18.0
UserDirectory is the narrow user-lookup boundary MembershipService needs: resolving the accepting caller so its account email can be matched against the invitation. service.Repository satisfies it; injecting only this method keeps the service decoupled from the full repository surface and trivially fakeable. The redesign's governance plane is a single postgres control database, so the boot-time repository is the correct (and only) handle — there is no per-tenant repo sharding in redesign mode.
Source Files
¶
- admin.go
- admin_api.go
- admin_ops.go
- admin_policy.go
- auth.go
- auth_email.go
- auth_email_change.go
- auth_link_identity.go
- auth_login.go
- auth_oauth_hosted.go
- auth_passkey.go
- auth_passwordless.go
- auth_phone.go
- auth_qr.go
- auth_totp.go
- branding.go
- domain.go
- email_canonicalize.go
- email_throttle.go
- graphtypes.go
- groups.go
- help.go
- identity_verification.go
- login_policy.go
- login_policy_enforce.go
- membership.go
- membership_service.go
- minordata.go
- node_convert.go
- platform_admin.go
- profile.go
- profile_linked_identities.go
- profile_ops.go
- project_resolver_cache.go
- projectconfig.go
- projectctx.go
- projectscope.go
- redact.go
- return_allowlist.go
- stub.go
- tenant.go