Documentation
¶
Overview ¶
Package service implements the business logic for the identity service.
The AuthService sits between the Connect-Go handler layer and the EntDB 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 EntDB 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
- type AdminService
- func (s *AdminService) DeactivateUser(ctx context.Context, actorID, targetUserID, reason 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) 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) 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) Logout(ctx context.Context, rawRefreshToken string) error
- func (s *AuthService) OAuthLogin(ctx context.Context, ...) (*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) 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) RequestPasswordReset(ctx context.Context, emailAddr 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) VerifyTotp(ctx context.Context, challengeID, code, ipAddr, userAgent string) (*LoginResult, error)
- func (s *AuthService) VerifyTotpSetup(ctx context.Context, userID, code string) (bool, error)
- type BeginIdentityVerificationResult
- type DB
- type EmailChangeToken
- 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 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)
- type InitiateQrLoginResult
- type InvitationRecord
- type InviteResult
- type LoginChallengeRecord
- type LoginResult
- type OAuthBeginResult
- type OAuthIdentity
- type Organization
- type OrganizationMembership
- type OrganizationSignupResult
- type OrganizationSignupService
- type PasskeyChallengeRecord
- type PasskeyCredRecord
- type PasskeyInfo
- type PasswordResetToken
- 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) 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) UpdateProfile(ctx context.Context, userID, name, avatarURL string) (*User, error)
- type QrLoginSessionRecord
- type QrSessionInfo
- type RecoveryCodeRecord
- type RefreshTokenRecord
- type Repository
- type RepositoryForTenant
- type ResetPasswordResult
- type Session
- type SessionRecord
- type StubDB
- func (StubDB) ExecuteAtomic(context.Context, string, string, []entdb.Operation) (*entdb.CommitResult, error)
- func (StubDB) GetEdgesFrom(context.Context, string, string, string, int) ([]*entdb.Edge, error)
- func (StubDB) GetEdgesTo(context.Context, string, string, string, int) ([]*entdb.Edge, error)
- func (StubDB) GetNode(context.Context, string, string, int, string) (*entdb.Node, error)
- func (StubDB) QueryNodes(context.Context, string, string, int, map[string]any) ([]*entdb.Node, error)
- func (StubDB) RegisterUserInTenant(context.Context, string, string, string, string, string) error
- func (StubDB) SearchNodes(context.Context, string, string, int, string) ([]*entdb.Node, error)
- type StubRepository
- func (StubRepository) AddOrganizationMember(context.Context, *OrganizationMembership) (string, 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) CreateOAuthIdentity(context.Context, *OAuthIdentity) error
- func (StubRepository) CreateOrganization(context.Context, *Organization) (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) DeleteExpiredEmailVerificationTokens(context.Context, int64, int) error
- func (StubRepository) DeleteExpiredLoginChallenges(context.Context, int64, int) error
- func (StubRepository) DeleteExpiredPasswordResetTokens(context.Context, int64, int) error
- func (StubRepository) DeleteExpiredWebAuthnChallenges(context.Context, int64, int) error
- func (StubRepository) DeleteLoginChallenge(context.Context, 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) FindEmailChangeTokenByHash(context.Context, string) (*EmailChangeToken, 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) 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) GetOrganization(context.Context, string) (*Organization, error)
- func (StubRepository) GetOrganizationBySlug(context.Context, string) (*Organization, 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) IncrementFailedLoginCount(context.Context, string) (int32, error)
- func (StubRepository) ListOAuthIdentitiesForUser(context.Context, string) ([]*OAuthIdentity, error)
- func (StubRepository) ListOrganizationsForUser(context.Context, string) ([]*Organization, 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) 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
- type TenantAdmin
- type TotpCredRecord
- type User
Constants ¶
const ( IDVStatusPending = "pending" IDVStatusInReview = "in_review" IDVStatusApproved = "approved" IDVStatusRejected = "rejected" IDVStatusExpired = "expired" )
Identity-verification status string constants. Mirrored as proto enum values in IdentityVerificationStatus.
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") ErrWeakPassword = errors.New("password does not meet strength requirements") ErrTotpRequired = errors.New("totp required") 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") 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") // ErrUnimplemented signals that the requested RPC is intentionally // disabled in the current deployment mode (e.g. OrganizationSignup // in mode=single per docs/IDENTITY.md decision log §3). The Connect // handler layer maps this to CodeUnimplemented. ErrUnimplemented = errors.New("operation unimplemented in this deployment mode") )
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 ¶
This section is empty.
Types ¶
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(db DB, tenantID string, auditLog *audit.Logger, cfg *config.Config, mailer email.Transport, logger *zap.Logger) *AdminService
NewAdminService creates an AdminService.
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 and revokes sessions.
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, 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, 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) 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) 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) 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, code, provider, redirectURI, codeVerifier, state, stateToken, ipAddr, userAgent string, ) (*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) (*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) 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) 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) 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) 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).
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 DB ¶
type DB interface {
GetNode(ctx context.Context, tenantID, actor string, typeID int, nodeID string) (*entdb.Node, error)
QueryNodes(ctx context.Context, tenantID, actor string, typeID int, filter map[string]any) ([]*entdb.Node, error)
ExecuteAtomic(ctx context.Context, tenantID, actor string, ops []entdb.Operation) (*entdb.CommitResult, error)
GetEdgesFrom(ctx context.Context, tenantID, actor, fromNodeID string, edgeTypeID int) ([]*entdb.Edge, error)
GetEdgesTo(ctx context.Context, tenantID, actor, toNodeID string, edgeTypeID int) ([]*entdb.Edge, error)
SearchNodes(ctx context.Context, tenantID, actor string, typeID int, query string) ([]*entdb.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 EntDB Transport used by identity services.
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 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 EntDB node type is WorkingGroup (type_id 2).
func NewGroupService ¶
func NewGroupService(db DB, tenantID 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, tenantID 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 IdentityVerificationRecord ¶ added in v0.4.0
type IdentityVerificationRecord struct {
NodeID string
VerificationID string // public identifier returned to clients
UserID string
TenantID string
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, tenantID 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.
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 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 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 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 — EntDB does not currently expose composite unique constraints. CreateOAuthIdentity callers must lookup first.
type Organization ¶ added in v0.7.1
type Organization struct {
ID string // repository node id; opaque to callers
Slug string // URL-safe; matches the tenant-shard-db tenant id
DisplayName string
OwnerUserID string
CreatedAtMs int64 // epoch ms
UpdatedAtMs int64 // epoch ms
}
Organization is the identity-layer organisation entity used by `mode=multi` deployments. Each Organization maps 1:1 to a tenant-shard-db tenant (see docs/IDENTITY.md decision log §2): the Slug is reused as the tenant id when the underlying tenant is created, so collisions surface as an EntDB-level error at `OrganizationSignup` time.
Identity-layer admin/role decisions live in `OrganizationMembership` (separately persisted) and on the User.Role axis. `OwnerUserID` is provenance — the User who created the organisation — not authority.
type OrganizationMembership ¶ added in v0.7.1
type OrganizationMembership struct {
NodeID string
OrganizationID string
UserID string
Role string
CreatedAtMs int64
}
OrganizationMembership records that a User belongs to an Organization. There is exactly one row per (organization, user) pair within a tenant. The role here is identity's product-layer role (`admin` / `member` / `guest`) and is independent of tenant-shard-db's `TenantMember.Role` (see decision log §4).
type OrganizationSignupResult ¶ added in v0.8.0
type OrganizationSignupResult struct {
Organization *Organization
User *User
AccessToken string
RefreshToken string
ExpiresIn int32
}
OrganizationSignupResult is returned by Signup on success.
type OrganizationSignupService ¶ added in v0.8.0
type OrganizationSignupService struct {
// contains filtered or unexported fields
}
OrganizationSignupService implements the self-serve `mode=multi` organisation-signup flow: it creates a new tenant in tenant-shard-db, registers the admin user, and persists the identity-layer Organization / User / OrganizationMembership rows. Rollback story: compensating deletes when a step after Admin.CreateTenant fails. See docs/IDENTITY.md decision log.
func NewOrganizationSignupService ¶ added in v0.8.0
func NewOrganizationSignupService( tenantAdmin TenantAdmin, repoForTenant RepositoryForTenant, cfg *config.Config, signer jwt.Signer, auditLog *audit.Logger, logger *zap.Logger, ) *OrganizationSignupService
NewOrganizationSignupService constructs an OrganizationSignupService. May only be wired in `mode=multi`; the caller is responsible for not invoking it in `mode=single` (the Connect handler returns Unimplemented in that case before reaching this layer).
func (*OrganizationSignupService) Signup ¶ added in v0.8.0
func (s *OrganizationSignupService) Signup( ctx context.Context, slug, displayName, email, password, name string, ) (*OrganizationSignupResult, error)
Signup runs the OrganizationSignup flow. tenant-shard-db v1.12+ requires every actor to be both a registered user and a tenant member before they can issue tenant-scoped writes; the typed `Repository.CreateUser` already performs the global registration + tenant member ("member") add internally, so this flow piggybacks on that rather than calling `Admin.CreateUser` / `Admin.AddTenantMember` a second time from here.
Steps (see issue #93 DoD; the order is adapted to entdb's node-id model — the global registry id IS the typed node id):
- Validate inputs (slug shape, email shape, password strength).
- Admin.CreateTenant(slug, displayName) — slug uniqueness lives here.
- Repository(slug).CreateUser(admin) — creates the User row AND registers the user globally AND adds them as a "member" of the tenant (the v1.12 actor invariant). Returns the assigned id.
- Admin.PromoteTenantMember(slug, adminID, "admin") — upgrade the storage-layer role from the default "member" to "admin". The promotion is independent of identity's product role (decision log §4).
- Repository(slug).CreateOrganization(slug, displayName, ownerID).
- Repository(slug).AddOrganizationMember(orgID, adminID, "admin").
- Issue access + refresh tokens.
On a failure after step 2 the partial identity-layer state is rolled back (best-effort); the tenant itself stays — tenant-shard-db (through v1.14.0 today) does not expose DeleteTenant. See docs/IDENTITY.md decision log.
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
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 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(db DB, tenantID string, auditLog *audit.Logger, logger *zap.Logger) *ProfileService
NewProfileService creates a ProfileService.
func (*ProfileService) ChangePassword ¶
func (s *ProfileService) ChangePassword(ctx context.Context, userID, currentPassword, newPassword string) error
ChangePassword changes the user's password. Requires current password verification. Invalidates all refresh tokens.
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) 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) UpdateProfile ¶
func (s *ProfileService) UpdateProfile(ctx context.Context, userID, name, avatarURL string) (*User, error)
UpdateProfile updates the authenticated user's name and/or avatar.
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 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
// 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
// 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)
// 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, entdb) 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
// Organizations — identity-layer entity used by `mode=multi`
// deployments. CreateOrganization writes the Organization row and
// the owner's OrganizationMembership atomically (from the caller's
// point of view; the underlying tenant is created by the
// OrganizationSignup RPC before this is reached). Returns the new
// organisation id. Slug uniqueness is enforced.
//
// In `mode=single`, the OrganizationSignup RPC is unimplemented
// (per decision log §3), so these methods are exercised only by
// `mode=multi` deployments and by the conformance suite.
CreateOrganization(ctx context.Context, org *Organization) (string, error)
GetOrganization(ctx context.Context, orgID string) (*Organization, error)
GetOrganizationBySlug(ctx context.Context, slug string) (*Organization, error)
ListOrganizationsForUser(ctx context.Context, userID string) ([]*Organization, error)
// AddOrganizationMember inserts the (org, user, role) membership
// row. Returns ErrAlreadyExists if the user is already a member
// of the organisation. Used by OrganizationSignup (slice 2) and
// the invitation-redemption flow (slice 4) — exposed on the
// Repository so both call sites can rely on the same
// at-most-one-membership-per-pair invariant.
AddOrganizationMember(ctx context.Context, m *OrganizationMembership) (string, error)
}
Repository abstracts all persistence operations for the auth service.
type RepositoryForTenant ¶ added in v0.8.0
type RepositoryForTenant func(tenantID string) Repository
RepositoryForTenant returns a service.Repository scoped to the given tenant id. In `mode=multi`, OrganizationSignup constructs a repository against the freshly-created tenant before writing the Organization, User, and OrganizationMembership rows. Implementations must return a distinct Repository per call so the new tenant's per-tenant state stays isolated from any other tenant's writes.
type ResetPasswordResult ¶
ResetPasswordResult is returned by AdminService.ResetUserPassword.
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 EntDB-backed repository is implemented.
func (StubRepository) AddOrganizationMember ¶ added in v0.7.1
func (StubRepository) AddOrganizationMember(context.Context, *OrganizationMembership) (string, 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) CreateOAuthIdentity ¶
func (StubRepository) CreateOAuthIdentity(context.Context, *OAuthIdentity) error
func (StubRepository) CreateOrganization ¶ added in v0.7.1
func (StubRepository) CreateOrganization(context.Context, *Organization) (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) DeleteExpiredEmailVerificationTokens ¶ added in v0.7.1
func (StubRepository) DeleteExpiredLoginChallenges ¶ added in v0.7.1
func (StubRepository) DeleteExpiredPasswordResetTokens ¶ added in v0.7.1
func (StubRepository) DeleteExpiredWebAuthnChallenges ¶ added in v0.7.1
func (StubRepository) DeleteLoginChallenge ¶
func (StubRepository) DeleteLoginChallenge(context.Context, string) error
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) FindEmailChangeTokenByHash ¶
func (StubRepository) FindEmailChangeTokenByHash(context.Context, string) (*EmailChangeToken, 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) 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) GetOrganization ¶ added in v0.7.1
func (StubRepository) GetOrganization(context.Context, string) (*Organization, error)
func (StubRepository) GetOrganizationBySlug ¶ added in v0.7.1
func (StubRepository) GetOrganizationBySlug(context.Context, string) (*Organization, 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) IncrementFailedLoginCount ¶
func (StubRepository) ListOAuthIdentitiesForUser ¶
func (StubRepository) ListOAuthIdentitiesForUser(context.Context, string) ([]*OAuthIdentity, error)
func (StubRepository) ListOrganizationsForUser ¶ added in v0.7.1
func (StubRepository) ListOrganizationsForUser(context.Context, string) ([]*Organization, 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) 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 ¶
type TenantAdmin ¶ added in v0.8.0
type TenantAdmin interface {
CreateTenant(ctx context.Context, tenantID, displayName string) error
// PromoteTenantMember upgrades userID's storage-layer role inside
// tenantID. Maps to tenant-shard-db's Admin.ChangeMemberRole. Must
// be a no-op when the user already holds the requested role.
PromoteTenantMember(ctx context.Context, tenantID, userID, role string) error
// RemoveTenantMember drops the membership row. Compensating rollback
// only; must tolerate "member not present" without error.
RemoveTenantMember(ctx context.Context, tenantID, userID string) error
}
TenantAdmin is the cross-tenant admin surface required by OrganizationSignup. Implementations wrap the tenant-shard-db `Admin.CreateTenant`, `Admin.ChangeMemberRole`, and `Admin.RemoveTenantMember` calls (the first to provision the tenant, the second to promote the admin's storage-layer role from the default "member" assigned by Repository.CreateUser to "admin", the third for compensating rollback when later steps fail).
Global-registry user creation and the default tenant-member row are handled by Repository.CreateUser (which calls Admin.CreateUser + Admin.AddTenantMember internally for the v1.12+ actor invariant). Identity does not duplicate those calls from OrganizationSignup.
All methods must tolerate ALREADY_EXISTS / not-found shapes idempotently so partial-rollback re-runs stay safe.
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
}
User represents a user in the identity system.