Documentation
¶
Index ¶
- Constants
- Variables
- func APIKeyMiddleware(store APIKeyStore, requiredScopes []string) func(http.Handler) http.Handler
- func AccessTokenFromRequest(r *http.Request) string
- func BearerToken(header string) string
- func CSRFMiddleware(cfg HTTPConfig) func(http.Handler) http.Handler
- func CookieValue(r *http.Request, base string) string
- func DecodeOptionalJSON(w http.ResponseWriter, r *http.Request, dst any) bool
- func DecodeOptionalJSONBody(r *http.Request, dst any) error
- func EmailChangeConfirmURL(base, token string) string
- func EmailVerificationURL(base, token string) string
- func GenerateOpenAPISpec(info OpenAPIInfo) map[string]any
- func IsBearerRequest(r *http.Request) bool
- func MagicLinkURL(base, token string) string
- func OAuthNotConfiguredError(provider string) error
- func PasswordResetURL(base, token string) string
- func RefreshTokenFromRequest(r *http.Request) string
- func SMSCodeMessage(code string) string
- func ServeAdminUI() http.Handler
- func ServeAuthJS() http.Handler
- func ServeAuthUI() http.Handler
- func ServeSSE(hub *SseHub, channel string) http.HandlerFunc
- func SessionHandleParam(r *http.Request, raw string) string
- func TwoFactorMode(mode string) bool
- func VerifyEmailToken(r *http.Request) string
- func VerifyWebhookSignature(secret string, body []byte, sigHeader string) error
- func WriteHTTPError(w http.ResponseWriter, e HTTPError)
- func WriteJSON(w http.ResponseWriter, status int, payload any)
- func WriteServiceError(w http.ResponseWriter, err error)
- func WriteSuccess(w http.ResponseWriter, status int, extra map[string]any)
- func WriteTwoFactorChallenge(w http.ResponseWriter, challenge TwoFactorChallenge)
- type APICompatibilityNotes
- type APIKeyRecord
- type APIKeyService
- type APIKeyStore
- type AddPhoneInput
- type Auth
- func (a *Auth) ChangePassword(ctx context.Context, in ChangePasswordInput) error
- func (a *Auth) CleanupExpiredSessions(ctx context.Context) (int, error)
- func (a *Auth) ConfirmEmailChange(ctx context.Context, in ConfirmEmailChangeInput) error
- func (a *Auth) DeleteAccount(ctx context.Context, in DeleteAccountInput) error
- func (a *Auth) DisableTOTP(ctx context.Context, userID, tenantID string) error
- func (a *Auth) FindUser(ctx context.Context, userID, email, tenantID string) (User, error)
- func (a *Auth) ForgotPassword(ctx context.Context, in ForgotPasswordInput) (string, error)
- func (a *Auth) IssueTempToken(ctx context.Context, user User) (string, error)
- func (a *Auth) IssueTokensForUser(ctx context.Context, userID, tenantID string) (User, AuthTokens, error)
- func (a *Auth) LinkRequest(ctx context.Context, in LinkRequestInput) (LinkRequestResult, error)
- func (a *Auth) LinkVerify(ctx context.Context, in LinkVerifyInput) (LinkVerifyResult, error)
- func (a *Auth) ListLinkedAccounts(ctx context.Context, userID string) ([]PublicLinkedAccount, error)
- func (a *Auth) ListSessions(ctx context.Context, userID, tenantID string) ([]Session, error)
- func (a *Auth) Login(ctx context.Context, in LoginInput) (User, AuthTokens, error)
- func (a *Auth) LoginWithChallenge(ctx context.Context, in LoginInput) (LoginResult, error)
- func (a *Auth) Logout(ctx context.Context, refreshToken string) error
- func (a *Auth) LogoutAccessToken(ctx context.Context, accessToken string) error
- func (a *Auth) LogoutRequest(ctx context.Context, r *http.Request)
- func (a *Auth) Me(ctx context.Context, accessToken string) (User, error)
- func (a *Auth) OAuthBegin(ctx context.Context, in OAuthBeginInput) (OAuthBeginResult, error)
- func (a *Auth) OAuthComplete(ctx context.Context, in OAuthCompleteInput) (OAuthCompleteResult, error)
- func (a *Auth) OAuthConfigured() bool
- func (a *Auth) Refresh(ctx context.Context, refreshToken string) (AuthTokens, error)
- func (a *Auth) Register(ctx context.Context, in RegisterInput) (User, AuthTokens, error)
- func (a *Auth) RequestEmailChange(ctx context.Context, in ChangeEmailRequestInput) (string, error)
- func (a *Auth) ResetPassword(ctx context.Context, in ResetPasswordInput) error
- func (a *Auth) ResolveHTTPConfig(cfg HTTPConfig) HTTPConfig
- func (a *Auth) RevokeUserSession(ctx context.Context, userID, tenantID, sessionID string) error
- func (a *Auth) SMSConfigured() bool
- func (a *Auth) SendMagicLink(ctx context.Context, in MagicLinkSendInput) (string, error)
- func (a *Auth) SendSMSCode(ctx context.Context, in SMSCodeSendInput) (string, error)
- func (a *Auth) SendVerificationEmailToken(ctx context.Context, in EmailVerificationInput) (string, error)
- func (a *Auth) Service() *Service
- func (a *Auth) StartTOTPEnrolment(ctx context.Context, userID, tenantID string) (TOTPSetup, error)
- func (a *Auth) StepUpSubject(tempToken string, missing, invalid HTTPError) (TempTokenSubject, HTTPError, bool)
- func (a *Auth) TwoFactorPolicy() bool
- func (a *Auth) UnlinkAccount(ctx context.Context, userID, provider, providerAccountID string) error
- func (a *Auth) UpdatePhoneNumber(ctx context.Context, in AddPhoneInput) (User, error)
- func (a *Auth) UpdateProfile(ctx context.Context, in UpdateProfileInput) (User, error)
- func (a *Auth) VerifyEmail(ctx context.Context, in VerifyEmailInput) error
- func (a *Auth) VerifyMagicLink(ctx context.Context, in MagicLinkVerifyInput) (User, AuthTokens, error)
- func (a *Auth) VerifyMagicLinkForUser(ctx context.Context, in MagicLinkVerifyInput, userID string) (User, AuthTokens, error)
- func (a *Auth) VerifySMSCode(ctx context.Context, in SMSCodeVerifyInput) (User, AuthTokens, error)
- func (a *Auth) VerifyTOTP(ctx context.Context, userID, tenantID, code string) (User, AuthTokens, error)
- func (a *Auth) VerifyTOTPSetup(ctx context.Context, userID, tenantID, secret, code string) error
- type AuthTokens
- type CSRFConfig
- type ChangeEmailRequestInput
- type ChangePasswordInput
- type Config
- type ConfirmEmailChangeInput
- type CookieOptions
- func (o CookieOptions) ClearAuthCookies(w http.ResponseWriter, includeCSRF bool)
- func (o CookieOptions) CookieName(base string) string
- func (o CookieOptions) SetAccessTokenCookie(w http.ResponseWriter, value string)
- func (o CookieOptions) SetCSRFTokenCookie(w http.ResponseWriter, value string)
- func (o CookieOptions) SetRefreshTokenCookie(w http.ResponseWriter, value string)
- type DeleteAccountInput
- type Deviation
- type DeviationNote
- type EmailChangeDelivery
- type EmailChangeMailer
- type EmailChangeSender
- type EmailChangeStore
- type EmailVerificationDelivery
- type EmailVerificationInput
- type EmailVerificationMailer
- type EmailVerificationSender
- type EmailVerificationStore
- type Event
- type EventBus
- type ForgotPasswordInput
- type HTTPConfig
- type HTTPError
- func AccessHTTPError(err error) HTTPError
- func ChangeEmailConfirmHTTPError(err error) HTTPError
- func ChangeEmailInlineError(user User) (HTTPError, bool)
- func ChangeEmailRequestHTTPError(err error) HTTPError
- func ChangePasswordHTTPError(err error) HTTPError
- func ChangePasswordInlineError(user User, currentPassword, newPassword string) (HTTPError, bool)
- func ForgotPasswordHTTPError(error) HTTPError
- func HTTPErrorFor(err error) HTTPError
- func MagicLinkVerifyHTTPError(err error) HTTPError
- func OAuthHTTPError(err error) HTTPError
- func RefreshHTTPError(err error) HTTPError
- func ResetPasswordHTTPError(err error) HTTPError
- func SMSVerifyHTTPError(err error) HTTPError
- func SendVerificationEmailHTTPError(err error) HTTPError
- func TOTPSetupHTTPError(err error) HTTPError
- func TOTPVerifyHTTPError(err error) HTTPError
- func VerifyEmailHTTPError(err error) HTTPError
- type HTTPMailerTransport
- type HTTPSMSTransport
- type IDP
- type IDPClient
- type IDPConfig
- type LinkRequestInput
- type LinkRequestResult
- type LinkTokenDelivery
- type LinkVerifyInput
- type LinkVerifyResult
- type LinkedAccountStore
- type LoginInput
- type LoginResult
- type MCPServer
- type MagicLinkDelivery
- type MagicLinkMailer
- type MagicLinkSendInput
- type MagicLinkSender
- type MagicLinkStore
- type MagicLinkVerifyInput
- type MailMessage
- type MailTemplateData
- type MailTemplater
- type MailerTransport
- type MemoryLinkedAccounts
- func (m *MemoryLinkedAccounts) Delete(_ context.Context, id string) error
- func (m *MemoryLinkedAccounts) FindByProvider(_ context.Context, provider, providerID string) (OAuthLinkedAccount, error)
- func (m *MemoryLinkedAccounts) ListForUser(_ context.Context, userID string) ([]OAuthLinkedAccount, error)
- func (m *MemoryLinkedAccounts) Save(_ context.Context, link OAuthLinkedAccount) error
- type MemoryMetadataStore
- type MemoryPendingLinks
- type MemoryRolesPermissionsStore
- func (s *MemoryRolesPermissionsStore) AddPermissionToRole(_ context.Context, role, permission string) error
- func (s *MemoryRolesPermissionsStore) AddRoleToUser(_ context.Context, userID, role string, tenantID string) error
- func (s *MemoryRolesPermissionsStore) CreateRole(_ context.Context, role string, permissions []string) error
- func (s *MemoryRolesPermissionsStore) DeleteRole(_ context.Context, role string) error
- func (s *MemoryRolesPermissionsStore) GetPermissionsForRole(_ context.Context, role string) ([]string, error)
- func (s *MemoryRolesPermissionsStore) GetPermissionsForUser(ctx context.Context, userID, tenantID string) ([]string, error)
- func (s *MemoryRolesPermissionsStore) GetRolesForUser(_ context.Context, userID, tenantID string) ([]string, error)
- func (s *MemoryRolesPermissionsStore) RemovePermissionFromRole(_ context.Context, role, permission string) error
- func (s *MemoryRolesPermissionsStore) RemoveRoleFromUser(_ context.Context, userID, role string, tenantID string) error
- func (s *MemoryRolesPermissionsStore) UserHasPermission(ctx context.Context, userID, permission, tenantID string) (bool, error)
- type MemorySessionStore
- func (s *MemorySessionStore) CreateSession(_ context.Context, session Session) (Session, error)
- func (s *MemorySessionStore) DeleteExpiredSessions(_ context.Context, now time.Time) (int, error)
- func (s *MemorySessionStore) GetSessionByID(_ context.Context, sessionID string) (Session, error)
- func (s *MemorySessionStore) GetSessionByRefreshTokenHash(_ context.Context, tokenHash string) (Session, error)
- func (s *MemorySessionStore) ListSessionsForUser(_ context.Context, userID, tenantID string) ([]Session, error)
- func (s *MemorySessionStore) RevokeSessionByID(_ context.Context, sessionID string) error
- func (s *MemorySessionStore) UpdateSession(_ context.Context, session Session) error
- type MemoryTelemetryStore
- type MemoryTenantStore
- func (s *MemoryTenantStore) AssociateUserWithTenant(_ context.Context, userID, tenantID string) error
- func (s *MemoryTenantStore) CreateTenant(_ context.Context, tenant Tenant) (Tenant, error)
- func (s *MemoryTenantStore) DeleteTenant(_ context.Context, id string) error
- func (s *MemoryTenantStore) DisassociateUserFromTenant(_ context.Context, userID, tenantID string) error
- func (s *MemoryTenantStore) GetAllTenants(_ context.Context) ([]Tenant, error)
- func (s *MemoryTenantStore) GetTenantByID(_ context.Context, id string) (Tenant, error)
- func (s *MemoryTenantStore) GetTenantsForUser(_ context.Context, userID string) ([]Tenant, error)
- func (s *MemoryTenantStore) GetUsersForTenant(_ context.Context, tenantID string) ([]string, error)
- func (s *MemoryTenantStore) UpdateTenant(_ context.Context, id string, update Tenant) error
- type MemoryUserStore
- func (s *MemoryUserStore) ApplyEmailChange(_ context.Context, userID, tenantID string) error
- func (s *MemoryUserStore) ClearEmailChangeToken(_ context.Context, userID, tenantID string) error
- func (s *MemoryUserStore) ClearEmailVerificationToken(_ context.Context, userID, tenantID string) error
- func (s *MemoryUserStore) ClearMagicLinkToken(_ context.Context, userID, tenantID string) error
- func (s *MemoryUserStore) ClearResetToken(_ context.Context, userID, tenantID string) error
- func (s *MemoryUserStore) ClearSMSCode(_ context.Context, userID, tenantID string) error
- func (s *MemoryUserStore) CreateUser(_ context.Context, user User) (User, error)
- func (s *MemoryUserStore) DeleteUser(_ context.Context, userID, tenantID string) error
- func (s *MemoryUserStore) GetUserByEmail(_ context.Context, email, tenantID string) (User, error)
- func (s *MemoryUserStore) GetUserByEmailChangeTokenHash(_ context.Context, tokenHash string) (User, error)
- func (s *MemoryUserStore) GetUserByEmailVerificationTokenHash(_ context.Context, tokenHash string) (User, error)
- func (s *MemoryUserStore) GetUserByID(_ context.Context, id, tenantID string) (User, error)
- func (s *MemoryUserStore) GetUserByMagicLinkTokenHash(_ context.Context, tokenHash string) (User, error)
- func (s *MemoryUserStore) GetUserByResetTokenHash(_ context.Context, tokenHash string) (User, error)
- func (s *MemoryUserStore) GetUserBySMSCodeHash(_ context.Context, userID, tenantID, codeHash string) (User, error)
- func (s *MemoryUserStore) MarkEmailVerified(_ context.Context, userID, tenantID string, verified bool) error
- func (s *MemoryUserStore) UpdateEmailChangeToken(_ context.Context, userID, tenantID, pendingEmail, tokenHash string, ...) error
- func (s *MemoryUserStore) UpdateEmailVerificationToken(_ context.Context, userID, tenantID, tokenHash string, expiry time.Time) error
- func (s *MemoryUserStore) UpdateMagicLinkToken(_ context.Context, userID, tenantID, tokenHash string, expiry time.Time) error
- func (s *MemoryUserStore) UpdatePassword(_ context.Context, userID, tenantID, passwordHash string) error
- func (s *MemoryUserStore) UpdatePhoneNumber(_ context.Context, userID, tenantID, phoneNumber string) (User, error)
- func (s *MemoryUserStore) UpdateProfile(_ context.Context, userID, tenantID, firstName, lastName string) (User, error)
- func (s *MemoryUserStore) UpdateResetToken(_ context.Context, userID, tenantID, tokenHash string, expiry time.Time) error
- func (s *MemoryUserStore) UpdateSMSCode(_ context.Context, userID, tenantID, codeHash string, expiry time.Time) error
- func (s *MemoryUserStore) UpdateTOTPSecret(_ context.Context, userID, tenantID, secret string, enabled bool) error
- type OAuthBeginInput
- type OAuthBeginResult
- type OAuthCompleteInput
- type OAuthCompleteResult
- type OAuthLinkedAccount
- type OAuthPendingMeta
- type OAuthProvider
- type OAuthService
- func (s *OAuthService) AuthorizeURL(providerName, state string) (string, error)
- func (s *OAuthService) AuthorizeURLPKCE(providerName, state, codeChallenge string) (string, error)
- func (s *OAuthService) ExchangeCode(ctx context.Context, providerName, code string) (OAuthUserInfo, error)
- func (s *OAuthService) ExchangeCodePKCE(ctx context.Context, providerName, code, codeVerifier string) (OAuthUserInfo, error)
- func (s *OAuthService) HandleCallback(ctx context.Context, authSvc *Service, linkedAccounts LinkedAccountStore, ...) (User, AuthTokens, error)
- func (s *OAuthService) Has(name string) bool
- type OAuthUserInfo
- type OAuthWiring
- type OpenAPIInfo
- type Option
- func WithBcryptCost(cost int) Option
- func WithEmailChangeSender(sender EmailChangeSender) Option
- func WithEmailVerificationSender(sender EmailVerificationSender) Option
- func WithIssuer(issuer string) Option
- func WithLogger(fn func(format string, args ...any)) Option
- func WithMagicLinkSender(sender MagicLinkSender) Option
- func WithMetadataProvider(store UserMetadataStore) Option
- func WithOAuth(wiring OAuthWiring) Option
- func WithPasswordResetSender(sender PasswordResetSender) Option
- func WithRBACProvider(store RolesPermissionsStore) Option
- func WithRequire2FA(enabled bool) Option
- func WithSMSCodeSender(sender SMSCodeSender) Option
- func WithSecret(secret string) Option
- func WithSessionCheckOn(mode string) Option
- func WithSessionStore(store SessionStore) Option
- func WithTenantProvider(store TenantStore) Option
- func WithTokenClaimsBuilder(fn func(ctx context.Context, user User) (map[string]any, error)) Option
- func WithTokenTTLs(access, refresh time.Duration) Option
- func WithUserStore(store UserStore) Option
- type PasswordResetDelivery
- type PasswordResetMailer
- type PasswordResetSender
- type PendingLinkStore
- type PublicLinkedAccount
- type PublicSession
- type PublicTenant
- type PublicUser
- type RegisterInput
- type ResetPasswordInput
- type RolesPermissionsStore
- type SMSCodeDelivery
- type SMSCodeSendInput
- type SMSCodeSender
- type SMSCodeVerifyInput
- type SMSStore
- type SMSTransport
- type Service
- func (s *Service) AddUserToTenant(ctx context.Context, userID, tenantID string) error
- func (s *Service) AssignRole(ctx context.Context, userID, role, tenantID string) error
- func (s *Service) ChangePassword(ctx context.Context, in ChangePasswordInput) error
- func (s *Service) CleanupExpiredSessions(ctx context.Context) (int, error)
- func (s *Service) ConfirmEmailChange(ctx context.Context, in ConfirmEmailChangeInput) error
- func (s *Service) CreateRole(ctx context.Context, role string, permissions []string) error
- func (s *Service) CreateTenant(ctx context.Context, name string, config map[string]any) (Tenant, error)
- func (s *Service) DeleteAccount(ctx context.Context, in DeleteAccountInput) error
- func (s *Service) DisableTOTP(ctx context.Context, userID, tenantID string) error
- func (s *Service) FindUser(ctx context.Context, userID, email, tenantID string) (User, error)
- func (s *Service) ForgotPassword(ctx context.Context, in ForgotPasswordInput) (string, error)
- func (s *Service) GetMetadata(ctx context.Context, userID string) (map[string]any, error)
- func (s *Service) IssueTempToken(ctx context.Context, user User) (string, error)
- func (s *Service) IssueTokensForUser(ctx context.Context, userID, tenantID string) (User, AuthTokens, error)
- func (s *Service) ListSessions(ctx context.Context, userID, tenantID string) ([]Session, error)
- func (s *Service) Login(ctx context.Context, in LoginInput) (User, AuthTokens, error)
- func (s *Service) LoginWithChallenge(ctx context.Context, in LoginInput) (LoginResult, error)
- func (s *Service) Logout(ctx context.Context, refreshToken string) error
- func (s *Service) LogoutAccessToken(ctx context.Context, accessToken string) error
- func (s *Service) Me(ctx context.Context, accessToken string) (User, error)
- func (s *Service) Refresh(ctx context.Context, refreshToken string) (AuthTokens, error)
- func (s *Service) Register(ctx context.Context, in RegisterInput) (User, AuthTokens, error)
- func (s *Service) RequestEmailChange(ctx context.Context, in ChangeEmailRequestInput) (string, error)
- func (s *Service) ResetPassword(ctx context.Context, in ResetPasswordInput) error
- func (s *Service) RevokeSessionByID(ctx context.Context, sessionID string) error
- func (s *Service) RevokeUserSession(ctx context.Context, userID, tenantID, sessionID string) error
- func (s *Service) SendMagicLink(ctx context.Context, in MagicLinkSendInput) (string, error)
- func (s *Service) SendSMSCode(ctx context.Context, in SMSCodeSendInput) (string, error)
- func (s *Service) SendVerificationEmailToken(ctx context.Context, in EmailVerificationInput) (string, error)
- func (s *Service) SetupTOTP(ctx context.Context, userID, tenantID string) (string, error)
- func (s *Service) StartTOTPEnrolment(ctx context.Context, userID, tenantID string) (TOTPSetup, error)
- func (s *Service) TokenTTLs() (access, refresh time.Duration)
- func (s *Service) UpdateMetadata(ctx context.Context, userID string, metadata map[string]any) error
- func (s *Service) UpdatePhoneNumber(ctx context.Context, in AddPhoneInput) (User, error)
- func (s *Service) UpdateProfile(ctx context.Context, in UpdateProfileInput) (User, error)
- func (s *Service) UserHasPermission(ctx context.Context, userID, permission, tenantID string) (bool, error)
- func (s *Service) VerifyEmail(ctx context.Context, in VerifyEmailInput) error
- func (s *Service) VerifyMagicLink(ctx context.Context, in MagicLinkVerifyInput) (User, AuthTokens, error)
- func (s *Service) VerifyMagicLinkForUser(ctx context.Context, in MagicLinkVerifyInput, userID string) (User, AuthTokens, error)
- func (s *Service) VerifySMSCode(ctx context.Context, in SMSCodeVerifyInput) (User, AuthTokens, error)
- func (s *Service) VerifyTOTP(ctx context.Context, userID, tenantID, code string) (User, AuthTokens, error)
- func (s *Service) VerifyTOTPSetup(ctx context.Context, userID, tenantID, secret, code string) error
- func (s *Service) VerifyTempToken(token string) (TempTokenSubject, error)
- type ServiceOption
- type Session
- type SessionAdminStore
- type SessionLookupStore
- type SessionStore
- type SseDistributor
- type SseHub
- type SseMessage
- type TOTPSetup
- type TOTPStore
- type TelemetryEvent
- type TelemetryFilter
- type TelemetryStore
- type TempTokenSubject
- type Tenant
- type TenantStore
- type TokenMailer
- type TwoFactorChallenge
- type UpdateProfileInput
- type User
- type UserAccountStore
- type UserMetadataStore
- type UserPasswordStore
- type UserPhoneStore
- type UserStore
- type VerifyEmailInput
- type WebhookDispatcher
- type WebhookEndpoint
- type WebhookPayload
Constants ¶
const ( SessionCheckOnAllCalls = "allcalls" SessionCheckOnRefresh = "refresh" SessionCheckOnNone = "none" )
const ( EmailVerificationModeNone = "none" EmailVerificationModeLazy = "lazy" EmailVerificationModeStrict = "strict" )
Email verification modes accepted by Config.EmailVerificationMode.
none : Register marks the address verified, so verification never comes up
on the self-registration path. An address left unverified by some
other route (admin provisioning, a data import, a custom UserStore
whose column defaults to false) is still refused at login. This is
the default and is exactly how the service behaved before the mode
existed.
lazy : Register leaves the address unverified and login is allowed anyway.
This is the only mode in which an unverified address may log in.
strict : Register leaves the address unverified and login is refused until
the address is confirmed.
const ( PasswordResetPath = "/reset-password" EmailVerificationPath = "/verify-email" EmailChangeConfirmPath = "/change-email/confirm" )
The routes the three tokens are spent on, relative to the mount prefix.
const ( TwoFactorMethodTOTP = "totp" TwoFactorMethodSMS = "sms" TwoFactorMethodMagicLink = "magic-link" )
Second-factor method names, exactly as the reference spells them in available2faMethods (auth.router.ts:557-559). A client picks its next request from this list, so these strings are wire: "magic-link" is hyphenated and nothing is capitalised.
const ( CodeOAuthStateInvalid = "OAUTH_STATE_INVALID" CodeOAuthTokenExchangeFail = "OAUTH_TOKEN_EXCHANGE_FAILED" CodeOAuthProfileFailed = "OAUTH_PROFILE_FAILED" CodeEmailRequired = "EMAIL_REQUIRED" CodeTokenRequired = "TOKEN_REQUIRED" CodeInvalidLinkToken = "INVALID_LINK_TOKEN" CodeLinkTokenExpired = "LINK_TOKEN_EXPIRED" )
Error codes for this route group. The first five are the reference's own literals; OAUTH_STATE_INVALID has no counterpart because the reference never rejects a state.
const ( CodeTempTokenRequired = "TEMP_TOKEN_REQUIRED" CodeInvalidTempToken = "INVALID_TEMP_TOKEN" // CodeInvalidAccessToken is what /2fa/verify answers for a bad step-up // token where its siblings answer INVALID_TEMP_TOKEN. See // HTTPErrInvalidStepUpToken. CodeInvalidAccessToken = "INVALID_ACCESS_TOKEN" CodeInvalidMagicLink = "INVALID_MAGIC_LINK" CodeTokenMismatch = "TOKEN_MISMATCH" CodePhoneNotSet = "PHONE_NOT_SET" // CodeEmailNotConfigured and CodeSMSNotConfigured report a deployment that // cannot deliver what the send route just minted. See delivery.go. CodeEmailNotConfigured = "EMAIL_NOT_CONFIGURED" CodeSMSNotConfigured = "SMS_NOT_CONFIGURED" )
Error codes for this section of the contract.
const ( // AuthStrategyHeader opts a caller into bearer delivery. The reference reads // this header in exactly one place and compares the value exactly, so the // match here is case-sensitive on purpose. AuthStrategyHeader = "X-Auth-Strategy" // AuthStrategyBearer is the only recognised AuthStrategyHeader value. AuthStrategyBearer = "bearer" // CSRFHeaderName carries the double-submit token. CSRFHeaderName = "X-CSRF-Token" )
const ( AccessTokenCookieName = "accessToken" RefreshTokenCookieName = "refreshToken" CSRFTokenCookieName = "csrf-token" )
Cookie base names. The prefix a cookie actually carries is resolved by CookieOptions.CookieName.
const ( CodeInvalidCredentials = "INVALID_CREDENTIALS" CodeEmailNotVerified = "EMAIL_NOT_VERIFIED" CodeSessionRevoked = "SESSION_REVOKED" CodeInvalidToken = "INVALID_TOKEN" CodeInvalidRefreshToken = "INVALID_REFRESH_TOKEN" CodeCSRFInvalid = "CSRF_INVALID" CodeTwoFactorRequired = "2FA_REQUIRED" CodeUserNotFound = "USER_NOT_FOUND" CodeNotImplemented = "NOT_IMPLEMENTED" )
Error codes emitted by the HTTP layer. The first block is the reference catalog; a client keying on any of these keeps working across the family.
const ( CodeUserExists = "USER_EXISTS" CodeWeakPassword = "WEAK_PASSWORD" CodeInvalidBody = "INVALID_BODY" )
Codes with no counterpart in the reference catalog. The reference delegates registration to a host-supplied callback and therefore has no code of its own for a duplicate address or a rejected password; these fill that hole. No shipped client branches on them.
const CodePasswordRequired = "PASSWORD_REQUIRED"
CodePasswordRequired is the one coded error in this group. Every other failure here is code-less in the reference, and a client is documented not to pattern-match on an absent code.
const CodeTwoFactorSetupRequired = "2FA_SETUP_REQUIRED"
CodeTwoFactorSetupRequired tells a client that a second factor is demanded of this account and it has none to present, so enrolment has to come first. It is declared here rather than in wire.go's catalog because the body it appears in is not the error envelope — see WriteTwoFactorChallenge — and because passwordless.go sets the precedent of a feature file owning its own codes.
const DefaultAPIPrefix = "/auth"
DefaultAPIPrefix is where the auth routes mount unless told otherwise.
const MagicLinkVerifyPath = "/magic-link/verify"
MagicLinkVerifyPath is the route a magic link points at, relative to the mount prefix.
const ReferenceRevision = "awesome-node-auth@cc01e997 (npm 1.9.0)"
ReferenceRevision names the revision every Deviation.Citations entry resolves against. The citations are file:line, so they mean nothing without it.
const StepUpMode = "2fa"
StepUpMode is the only value of the `mode` field the routes special-case.
Variables ¶
var ( ErrInvalidCredentials = errors.New("auth: invalid credentials") ErrUserExists = errors.New("auth: user already exists") ErrInvalidToken = errors.New("auth: invalid token") ErrSessionNotFound = errors.New("auth: session not found") ErrSessionRevoked = errors.New("auth: session revoked") ErrWeakPassword = errors.New("auth: password is too weak") ErrFeatureNotSupported = errors.New("auth: feature not supported by configured stores") ErrEmailNotVerified = errors.New("auth: email not verified") ErrInvalidCode = errors.New("auth: invalid code") ErrTwoFactorRequired = errors.New("auth: two-factor authentication required") ErrAlreadyExists = errors.New("auth: resource already exists") ErrTenantNotFound = errors.New("auth: tenant not found") ErrRoleNotFound = errors.New("auth: role not found") // ErrEmailNotConfigured and ErrSMSNotConfigured mean the deployment has no // way to deliver the credential a send route just asked for. They are // deliberately not ErrFeatureNotSupported: that one says the configured // store cannot hold the column, and it is already on the wire as // NOT_IMPLEMENTED on these same routes. See delivery.go. ErrEmailNotConfigured = errors.New("auth: email delivery is not configured") ErrSMSNotConfigured = errors.New("auth: sms delivery is not configured") )
var ( // HTTPErrOAuthState is the fix for #10. It is a 401 with a code because a // browser mid-flow has to be able to tell a tampered state from a provider // failure, and because the reference's own choice on this route is a JSON // error rather than a redirect (wire-contract.md §4, gap 5). HTTPErrOAuthState = HTTPError{Status: http.StatusUnauthorized, Message: "Invalid or expired OAuth state", Code: CodeOAuthStateInvalid} // Coded, unlike its passwordless sibling. The reference throws AuthError with // EMAIL_REQUIRED here (auth.router.ts:1498) but answers uncoded on // /magic-link/send (auth.router.ts:1111), so the two are not interchangeable. // See HTTPErrPasswordlessEmailRequired. HTTPErrLinkEmailRequired = HTTPError{Status: http.StatusBadRequest, Message: "email is required", Code: CodeEmailRequired} HTTPErrTokenRequired = HTTPError{Status: http.StatusBadRequest, Message: "token is required", Code: CodeTokenRequired} HTTPErrInvalidLinkToken = HTTPError{Status: http.StatusBadRequest, Message: "Invalid account-link token", Code: CodeInvalidLinkToken} HTTPErrLinkTokenExpired = HTTPError{Status: http.StatusBadRequest, Message: "Account-link token has expired", Code: CodeLinkTokenExpired} HTTPErrTargetUserNotFound = HTTPError{Status: http.StatusNotFound, Message: "Target user not found", Code: CodeUserNotFound} // failure. Its first one ("Authentication required", emitted when no pending // link store is configured at all) is unreachable here: this port resolves // both the stash and the link-token storage through the same // PendingLinkStore, so an absent store answers NOT_IMPLEMENTED before // identity resolution is ever attempted. HTTPErrLinkUnauthorized = HTTPError{Status: http.StatusUnauthorized, Message: "Authentication required or no pending link found", Code: CodeUnauthorized} )
The error catalog for this group. Messages are the reference's strings where the reference has one.
var ( HTTPErrTempTokenRequired = HTTPError{Status: http.StatusBadRequest, Message: "tempToken is required for 2FA mode", Code: CodeTempTokenRequired} HTTPErrInvalidTempToken = HTTPError{Status: http.StatusUnauthorized, Message: "Invalid or expired temp token", Code: CodeInvalidTempToken} // HTTPErrInvalidStepUpToken is /2fa/verify's answer to the failure the // magic-link and SMS routes answer with HTTPErrInvalidTempToken. The // reference verifies the tempToken there through verifyAccessToken and lets // its AuthError through unchanged (auth.router.ts:862 vs :1096, :1144, // :1201, :1264), so the same failure has two codes depending on the 2FA // method. It is reproduced rather than harmonised: a client that special- // cases TOTP today would break if this route started agreeing with its // siblings. HTTPErrInvalidStepUpToken = HTTPError{Status: http.StatusUnauthorized, Message: "Invalid or expired access token", Code: CodeInvalidAccessToken} // Uncoded on purpose. The reference emits this same message WITHOUT a code on // /magic-link/send (auth.router.ts:1111) but WITH code EMAIL_REQUIRED on // /link-request (auth.router.ts:1498), so this and HTTPErrLinkEmailRequired are // not interchangeable. Do not merge them into one entry. HTTPErrPasswordlessEmailRequired = HTTPError{Status: http.StatusBadRequest, Message: "email is required"} HTTPErrUserIDOrEmailRequired = HTTPError{Status: http.StatusBadRequest, Message: "userId or email is required"} HTTPErrUserIDRequired = HTTPError{Status: http.StatusBadRequest, Message: "userId is required"} HTTPErrPhoneNotSet = HTTPError{Status: http.StatusBadRequest, Message: "User does not have a phone number configured", Code: CodePhoneNotSet} HTTPErrInvalidSMSCode = HTTPError{Status: http.StatusUnauthorized, Message: "Invalid or expired SMS code"} // HTTPErrEmailNotConfigured is /magic-link/send's answer when nothing can // mail the link (magic-link.strategy.ts:12-14). HTTPErrEmailNotConfigured = HTTPError{Status: http.StatusInternalServerError, Message: "Email not configured", Code: CodeEmailNotConfigured} // HTTPErrSMSNotConfigured is /sms/send's answer when nothing can text the // code. The reference states this one twice with two different messages: the // router checks config.sms first and says "SMS is not configured" // (auth.router.ts:1179), and the strategy behind it says "SMS not // configured" (sms.strategy.ts:12) — no "is". Only the router's string is // ever on the wire, because its check runs before the strategy is reached, // so that is the one reproduced. The strategy's wording is not a second // catalog entry here; it has no route that can emit it. HTTPErrSMSNotConfigured = HTTPError{Status: http.StatusInternalServerError, Message: "SMS is not configured", Code: CodeSMSNotConfigured} // HTTPErrInvalidMagicLink covers both of the reference's magic-link // failures. The reference splits them — INVALID_MAGIC_LINK for a token it // cannot find, MAGIC_LINK_EXPIRED for one past its TTL // (magic-link.strategy.ts:41-49) — but Service.VerifyMagicLink collapses // the two into ErrInvalidToken, so the expiry code is not reachable from // here. Splitting the sentinel is the fix; both are 401 either way. HTTPErrInvalidMagicLink = HTTPError{Status: http.StatusUnauthorized, Message: "Invalid magic link token", Code: CodeInvalidMagicLink} HTTPErrTokenMismatch = HTTPError{Status: http.StatusUnauthorized, Message: "Token mismatch", Code: CodeTokenMismatch} // The two "Invalid TOTP code" entries differ only in status: /2fa/verify // answers 401 (auth.router.ts:869-872), /2fa/verify-setup answers 400 // (:847-850). HTTPErrInvalidTOTPCode = HTTPError{Status: http.StatusUnauthorized, Message: "Invalid TOTP code"} HTTPErrInvalidTOTPSetupCode = HTTPError{Status: http.StatusBadRequest, Message: "Invalid TOTP code"} HTTPErrTOTPNotSetUp = HTTPError{Status: http.StatusBadRequest, Message: "User not found or 2FA not set up"} HTTPErrTwoFactorRequiredForUser = HTTPError{Status: http.StatusForbidden, Message: "Cannot disable 2FA: required for your account", Code: CodeTwoFactorRequired} HTTPErrTwoFactorRequiredByPolicy = HTTPError{Status: http.StatusForbidden, Message: "Cannot disable 2FA: required by system policy", Code: CodeTwoFactorRequired} )
The catalog entries these routes write. Several carry no code: the reference answers those inline rather than through its AuthError path, and the omission is exact, not an oversight (wire-contract §3, "Shared mechanics").
var ( // HTTPErrNoAccessToken and HTTPErrInvalidAccessToken are 403 and carry no // code. That looks wrong next to the 401s around them, but it is what the // reference middleware emits and the clients are built against it. HTTPErrNoAccessToken = HTTPError{Status: http.StatusForbidden, Message: "No access token provided"} HTTPErrInvalidAccessToken = HTTPError{Status: http.StatusForbidden, Message: "Invalid or expired access token"} // HTTPErrSessionRevoked is the family's fast-logout signal: both browser // clients stop refreshing and log out on this exact code, so nothing else // may be returned for a revoked session. HTTPErrSessionRevoked = HTTPError{Status: http.StatusUnauthorized, Message: "Session has been revoked", Code: CodeSessionRevoked} HTTPErrNoRefreshToken = HTTPError{Status: http.StatusUnauthorized, Message: "No refresh token provided"} HTTPErrInvalidRefreshToken = HTTPError{Status: http.StatusUnauthorized, Message: "Invalid refresh token"} HTTPErrExpiredRefreshToken = HTTPError{Status: http.StatusUnauthorized, Message: "Invalid or expired refresh token", Code: CodeInvalidRefreshToken} HTTPErrInvalidCredentials = HTTPError{Status: http.StatusUnauthorized, Message: "Invalid credentials", Code: CodeInvalidCredentials} HTTPErrEmailNotVerified = HTTPError{Status: http.StatusForbidden, Message: "Email address is not verified", Code: CodeEmailNotVerified} HTTPErrTwoFactorRequired = HTTPError{Status: http.StatusForbidden, Message: "Two-factor authentication required", Code: CodeTwoFactorRequired} HTTPErrInvalidToken = HTTPError{Status: http.StatusUnauthorized, Message: "Invalid or expired token", Code: CodeInvalidToken} HTTPErrCSRFInvalid = HTTPError{Status: http.StatusForbidden, Message: "CSRF token validation failed", Code: CodeCSRFInvalid} // HTTPErrUserNotFound carries no code: every reference site that emits the // literal "User not found" emits it code-less (GET /me, /change-password, // /sms/send, the magic-link 2fa branch). The one coded USER_NOT_FOUND in the // reference is a different message ("Target user not found", /link-request), // so a route reusing this value must not gain a code by accident. HTTPErrUserNotFound = HTTPError{Status: http.StatusNotFound, Message: "User not found"} HTTPErrNotImplemented = HTTPError{Status: http.StatusNotImplemented, Message: "Feature not supported by the configured stores", Code: CodeNotImplemented} // HTTPErrInternal deliberately carries neither a code nor the underlying // error text: an unexpected failure must not describe itself to the caller. HTTPErrInternal = HTTPError{Status: http.StatusInternalServerError, Message: "Internal server error"} HTTPErrUserExists = HTTPError{Status: http.StatusConflict, Message: "User already exists", Code: CodeUserExists} HTTPErrWeakPassword = HTTPError{Status: http.StatusBadRequest, Message: "Password is too weak", Code: CodeWeakPassword} HTTPErrInvalidBody = HTTPError{Status: http.StatusBadRequest, Message: "Invalid request body", Code: CodeInvalidBody} )
The error catalog. Messages are the reference's own strings, because a client that surfaces the server message must not read differently per implementation.
var ( // HTTPErrResetTokenStoreMissing and the two below it are 500, not 501: the // reference reports a store that cannot perform the flow as an internal // error (auth.router.ts:805-808, 937-940, 1000-1003, 1042-1045). A client // that treats 501 as "feature absent" and 500 as "server broken" therefore // has to see 500 here, which is why these routes do not go through // HTTPErrorFor's ErrFeatureNotSupported → 501 mapping. HTTPErrResetTokenStoreMissing = HTTPError{Status: http.StatusInternalServerError, Message: "UserStore does not implement findByResetToken"} HTTPErrEmailVerificationStoreMissing = HTTPError{Status: http.StatusInternalServerError, Message: "UserStore does not implement email verification"} HTTPErrChangeEmailStoreMissing = HTTPError{Status: http.StatusInternalServerError, Message: "UserStore does not implement change-email"} HTTPErrInvalidResetToken = HTTPError{Status: http.StatusBadRequest, Message: "Invalid reset token"} HTTPErrInvalidVerifyToken = HTTPError{Status: http.StatusBadRequest, Message: "Invalid verification token"} HTTPErrInvalidEmailChangeToken = HTTPError{Status: http.StatusBadRequest, Message: "Invalid email-change token"} HTTPErrVerifyTokenRequired = HTTPError{Status: http.StatusBadRequest, Message: "Token is required"} HTTPErrEmailAlreadyVerified = HTTPError{Status: http.StatusBadRequest, Message: "Email is already verified"} // HTTPErrCurrentPasswordIncorrect is a 401 with no code, unlike the coded // INVALID_CREDENTIALS a failed login returns. Reproduced as-is even though it // is a known hazard: /change-password is absent from the Angular // interceptor's no-retry list, so a wrong current password costs a pointless // refresh-and-replay round trip (wire-contract §2, [MISMATCH]). HTTPErrCurrentPasswordIncorrect = HTTPError{Status: http.StatusUnauthorized, Message: "Current password is incorrect"} HTTPErrNewPasswordRequired = HTTPError{Status: http.StatusBadRequest, Message: "New password is required"} // HTTPErrEmailInUse carries no code, so a client cannot branch on it — only // on the 409 status. HTTPErrEmailInUse = HTTPError{Status: http.StatusConflict, Message: "Email address is already in use"} HTTPErrPasswordRequired = HTTPError{ Status: http.StatusForbidden, Message: "You must set a password before you can change your email address.", Code: CodePasswordRequired, } )
The §2 error catalog. Messages are the reference's own strings, down to the "UserStore does not implement …" wording it uses for an incapable store.
var ErrDeliveryFailed = errors.New("auth: delivery failed after the credential was stored")
ErrDeliveryFailed marks a send that failed after the credential was already stored. It is joined to the transport's own error rather than replacing it, so a caller can still read the cause, and it exists for exactly one reason: /forgot-password has to tell a delivery failure apart from a store failure in order to swallow the first and report the second (Auth.ForgotPassword).
It maps to no catalog entry of its own. HTTPErrorFor does not know it, so it falls through to the generic 500 — which is what the reference answers for a throwing mailer, and what the two authenticated routes therefore answer.
var ErrMagicLinkOwnerMismatch = errors.New("auth: magic link belongs to another user")
ErrMagicLinkOwnerMismatch is a valid magic link presented as a second factor for somebody else: the link verified, but it does not belong to the user the step-up token names. It is a distinct sentinel rather than ErrInvalidToken because the wire answer differs (TOKEN_MISMATCH, not INVALID_MAGIC_LINK) and because the two failures mean different things — one is a bad link, the other is a good link on the wrong account.
var HTTPErrSessionNotFound = HTTPError{Status: http.StatusNotFound, Message: "Session not found"}
HTTPErrSessionNotFound is the answer to DELETE /sessions/{handle} for a handle that does not exist and for one that belongs to somebody else — deliberately the same body, so the route cannot be used to probe for live sessions (auth.router.ts:762-767). It is written by the route rather than mapped in HTTPErrorFor, because ErrSessionNotFound means "unusable refresh token" (401) everywhere else.
Functions ¶
func APIKeyMiddleware ¶
func AccessTokenFromRequest ¶ added in v0.2.0
AccessTokenFromRequest applies the reference's extraction order: the Authorization header first, then the access-token cookie.
func BearerToken ¶ added in v0.2.0
BearerToken extracts the token from an Authorization header.
func CSRFMiddleware ¶
func CSRFMiddleware(cfg HTTPConfig) func(http.Handler) http.Handler
CSRFMiddleware distributes and enforces the double-submit token.
Distribution mirrors the reference's router-level auto-init: a request that arrives without a readable CSRF cookie gets a fresh one.
Enforcement follows the reference matrix. A request is checked when it is an unsafe method, on a mounted route, that route is not exempt, the request presents no bearer credential, and either it carries an access-token cookie or the route does its own manual check in the reference:
enforced = mutating && mounted && !exempt && !usingBearerCredential
&& (accessTokenCookiePresent || manualCheckRoute)
Safe methods, bearer requests, the unauthenticated routes in csrfExemptRoutes and — for every route but csrfManualCheckRoutes — requests with no cookie credential are passed through. The last of those is the reference's ordering: see csrfEnforced.
func CookieValue ¶ added in v0.2.0
CookieValue reads a cookie honouring the family's __Host- → __Secure- → bare priority. Every read site must go through it, otherwise a deployment that switches Secure on stops recognising its own cookies.
func DecodeOptionalJSON ¶ added in v0.2.0
DecodeOptionalJSON decodes a JSON request body into dst, tolerating an absent or empty one, and writes the error envelope when the body is present but malformed.
Every route in this group accepts an empty body: the reference runs express.json(), which leaves req.body as {} rather than failing, and the Flutter client posts /send-verification-email with no body at all. Rejecting a malformed body with 400 INVALID_BODY is the port's own convention, already applied by /register and /login.
func DecodeOptionalJSONBody ¶ added in v0.2.0
DecodeOptionalJSONBody decodes r's JSON body into dst, treating an absent or zero-length body as the empty object rather than as an error.
The reference runs behind express.json(), which leaves req.body = {} when the request carried no body at all; PATCH /profile and POST /add-phone then read optional fields off it, so a bodyless call is a 200 no-op there and not a 400 (§3.5, §3.6). Without a shared decoder the four adapters disagree about exactly this: echo's Bind returns nil for a zero-length body, while encoding/json and gin's ShouldBindJSON both surface io.EOF as a decode error.
A body that is present but is not valid JSON is still an error: express.json() rejects that too, and tolerating it would let a typo'd payload be silently applied as "change nothing".
func EmailChangeConfirmURL ¶ added in v0.3.0
EmailChangeConfirmURL builds the change-email confirmation link.
func EmailVerificationURL ¶ added in v0.3.0
EmailVerificationURL builds the verify-email link.
func GenerateOpenAPISpec ¶
func GenerateOpenAPISpec(info OpenAPIInfo) map[string]any
GenerateOpenAPISpec returns an OpenAPI 3.0 spec for the mounted auth endpoints.
func IsBearerRequest ¶ added in v0.2.0
IsBearerRequest reports whether the caller asked for token delivery in the response body instead of cookies.
func MagicLinkURL ¶ added in v0.3.0
MagicLinkURL builds the link the reference mails: the base with any trailing slash removed, then "/magic-link/verify?token=<token>" (magic-link.strategy.ts:23-27). Reproduced here so that a deployment writing its own sender does not have to guess the shape the verify route and the shipped clients expect.
base is the address the auth routes are reachable at from a mailbox — "https://app.example.com/auth". The reference derives it per request from email.siteUrl and the request Origin (resolveSiteUrl / buildUiLink, wire-contract §4); this port has neither a siteUrl config field nor origin resolution yet, so the base is the sender's to supply. An empty base yields a relative link, which the reference also produces when siteUrl is unset — it is a misconfiguration in both, not a special case handled here.
The token is interpolated raw. randomToken emits base64url, whose alphabet needs no query escaping, and the reference interpolates its hex token raw for the same reason.
func OAuthNotConfiguredError ¶ added in v0.2.0
OAuthNotConfiguredError is the "<Provider> OAuth not configured" stub as an error, so an adapter can answer it without reaching into the flow methods.
func PasswordResetURL ¶ added in v0.3.0
PasswordResetURL, EmailVerificationURL and EmailChangeConfirmURL build the links the reference mails, so a deployment writing its own sender does not have to guess the shape.
base is the address the auth routes are reachable at from a mailbox. The reference derives it per request (resolveSiteUrl / buildUiLink, wire-contract §4); this port has neither a siteUrl config field nor origin resolution yet, so the base is the sender's to supply, exactly as for MagicLinkURL.
One caveat carries over from the reference and is worth knowing before these links reach a real mailbox: /reset-password and /change-email/confirm are POST-only routes, so a browser following either link lands on something it cannot GET (wire-contract §2 notes the same about the reference with its UI disabled). A deployment points base at the front end that owns those two pages; /verify-email is a GET and works directly.
func RefreshTokenFromRequest ¶ added in v0.2.0
RefreshTokenFromRequest applies the reference's acceptance order for /refresh: a refreshToken in the body wins, otherwise the cookie. A missing or empty body is not an error — cookie-mode refresh sends no body at all.
func SMSCodeMessage ¶ added in v0.3.0
SMSCodeMessage is the text the reference texts (sms.strategy.ts:20). It is on a wire of its own — a real handset — so the literal is reproduced rather than reworded, and it lives here so a custom SMSCodeSender can reuse it instead of drifting from the rest of the family.
func ServeAdminUI ¶
ServeAdminUI returns an http.Handler serving the embedded admin dashboard HTML.
func ServeAuthJS ¶
ServeAuthJS returns an http.Handler serving the embedded auth.js browser SDK.
func ServeAuthUI ¶
ServeAuthUI returns an http.Handler serving the embedded auth UI HTML (login/register).
func ServeSSE ¶
func ServeSSE(hub *SseHub, channel string) http.HandlerFunc
ServeSSE is an http.HandlerFunc that streams SSE to a connected client. channel is typically the authenticated user's ID.
func SessionHandleParam ¶ added in v0.2.0
SessionHandleParam normalises the {handle} path parameter: exactly one percent-decode, the way the reference's decodeURIComponent does (auth.router.ts:761). One family client URI-escapes the handle and the other posts it verbatim, so both forms have to name the same session.
The decode deliberately does not start from the router's own path parameter, because the four routers disagree about what they hand over: chi (via r.PathValue, which chi populates from its escaped route path) and echo (which routes on r.URL.RawPath when it is set) pass the segment still encoded, while net/http and gin pass it already decoded. Unescaping whatever arrives is therefore one decode on two adapters and two on the other two, and no inspection of the string can tell the cases apart — "ses_50%25off" is both a valid decoded handle and the encoded form of "ses_50%off".
r.URL.EscapedPath() is router-independent (RawPath when the request carried one, the re-encoded Path otherwise), so taking the last segment of it and decoding once gives all four adapters the same handle. raw is kept as the fallback for a path that yields no segment.
A handle containing an encoded slash is still not recoverable — the path has already been split on it — but the handles this package issues are "ses_" plus hex.
func TwoFactorMode ¶ added in v0.2.0
TwoFactorMode reports whether a magic-link or SMS request asked for the second-factor branch.
The comparison is literal and case-sensitive because the reference's is (`mode === '2fa'`, auth.router.ts:1087, :1134, :1192, :1255): "login", the empty string, and anything else all take the login branch. That is what makes the two shipped clients interchangeable — Angular sends mode:"login", Flutter omits the field — so loosening this comparison silently reroutes one of them.
func VerifyEmailToken ¶ added in v0.2.0
VerifyEmailToken reads the token GET /verify-email carries. It is a query parameter, not a body field, and the route is the only GET in this group.
The value is used exactly as sent: the reference tests it with `if (!token)` (auth.router.ts:972), so a token of one space is truthy there and fails the lookup with 400 "Invalid verification token" rather than 400 "Token is required". Trimming here would be friendlier but would move that request into the other error, so the reference's answer wins.
func VerifyWebhookSignature ¶
VerifyWebhookSignature validates an inbound webhook signature.
func WriteHTTPError ¶ added in v0.2.0
func WriteHTTPError(w http.ResponseWriter, e HTTPError)
WriteHTTPError writes the error envelope.
func WriteJSON ¶ added in v0.2.0
func WriteJSON(w http.ResponseWriter, status int, payload any)
WriteJSON writes payload as the entire response body.
func WriteServiceError ¶ added in v0.2.0
func WriteServiceError(w http.ResponseWriter, err error)
WriteServiceError maps a service sentinel and writes it.
func WriteSuccess ¶ added in v0.2.0
func WriteSuccess(w http.ResponseWriter, status int, extra map[string]any)
WriteSuccess writes the {"success": true} envelope, merging any extra top-level fields a route contributes.
func WriteTwoFactorChallenge ¶ added in v0.3.0
func WriteTwoFactorChallenge(w http.ResponseWriter, challenge TwoFactorChallenge)
WriteTwoFactorChallenge writes the login challenge as the entire response body.
No cookie is set on either branch, in either delivery mode: nothing has been issued yet, so there is nothing to deliver. That is the reference's behaviour too — the 2FA branch returns before issueTokens is ever reached (auth.router.ts:571-578 precedes :580-581).
Types ¶
type APICompatibilityNotes ¶ added in v0.3.0
type APICompatibilityNotes struct {
// ContractTarget names the clients the wire shape is pinned to.
ContractTarget string `json:"contractTarget"`
// ReferenceRevision is ReferenceRevision, carried in the value so a caller
// that serialises these notes ships the revision its citations need.
ReferenceRevision string `json:"referenceRevision"`
// CookieConventions and BearerConventions summarise the two delivery modes.
CookieConventions string `json:"cookieConventions"`
BearerConventions string `json:"bearerConventions"`
// KnownDeviations is the register, in no significant order.
KnownDeviations []Deviation `json:"knownDeviations"`
}
APICompatibilityNotes is the contract this port targets, plus every known deviation from it.
func CompatibilityNotes ¶ added in v0.3.0
func CompatibilityNotes() APICompatibilityNotes
CompatibilityNotes returns the wire contract this port targets and the known deviations from it.
The result is freshly built on every call and shares nothing with the package, so a caller may sort, filter or edit it without affecting anyone else's view.
type APIKeyRecord ¶
type APIKeyRecord struct {
ID string
Prefix string
Name string
ServiceID string
KeyHash string
Scopes []string
AllowedIPs []string
IsActive bool
ExpiresAt *time.Time
LastUsedAt *time.Time
}
APIKeyRecord stores hashed key material and policy constraints.
type APIKeyService ¶
type APIKeyService struct {
// contains filtered or unexported fields
}
APIKeyService issues and verifies API keys. Create stores a bcrypt hash of the key, so it carries the same cost knob as Config.BcryptCost — an operator who raises the password cost would otherwise still get key hashes pinned at the default, with no way to reach them.
func NewAPIKeyService ¶
func NewAPIKeyService(bcryptCost int) *APIKeyService
NewAPIKeyService returns a service that hashes new keys at bcryptCost.
As with Config.BcryptCost, zero means unset and resolves to bcrypt.DefaultCost. Verify does not hash, so a service used only for verification may be built with 0 regardless of the cost its keys were created at: bcrypt reads the cost out of the stored hash.
func (*APIKeyService) Create ¶
func (s *APIKeyService) Create(ctx context.Context, store APIKeyStore, name, serviceID string, scopes, allowedIPs []string, expiresAt *time.Time) (rawKey string, record APIKeyRecord, err error)
func (*APIKeyService) Verify ¶
func (s *APIKeyService) Verify(ctx context.Context, store APIKeyStore, rawKey string, ip string, requiredScopes []string) (APIKeyRecord, error)
type APIKeyStore ¶
type APIKeyStore interface {
Save(ctx context.Context, key APIKeyRecord) error
FindByPrefix(ctx context.Context, prefix string) (APIKeyRecord, error)
Revoke(ctx context.Context, id string) error
UpdateLastUsed(ctx context.Context, id string, when time.Time) error
}
APIKeyStore persists and resolves API keys.
type AddPhoneInput ¶ added in v0.2.0
AddPhoneInput is the payload behind POST /add-phone. An empty PhoneNumber clears the number, as the reference's nullable phoneNumber does.
type Auth ¶
type Auth struct {
// contains filtered or unexported fields
}
Auth is the top-level entrypoint configured with functional options.
func NewWithConfig ¶ added in v0.2.0
NewWithConfig creates a configured Auth instance from a pre-populated Config, with any Options applied in order on top of it.
New builds its Config internally and the exported Option set has always been narrower than Config, so fields with no matching Option — the individual TTLs, EmailVerificationMode, and until now BcryptCost — were unreachable from outside the package (#25). Accepting the struct closes that whole class of gap at once and cannot drift as Config grows, which one-Option-per-field would. The Options remain as sugar and still compose with a supplied Config.
cfg is validated by NewService exactly as New's is; a hand-built Config that omits, say, the TTLs is rejected rather than silently defaulted.
func (*Auth) ChangePassword ¶ added in v0.2.0
func (a *Auth) ChangePassword(ctx context.Context, in ChangePasswordInput) error
ChangePassword performs POST /change-password.
A passwordless account — OAuth-only, or magic-link-only — skips the current-password comparison entirely and may set an initial password by supplying newPassword alone: in the reference the compare sits inside `if (user.password)` and the `else if` only fires when *both* fields are falsy, so such a caller falls through to the hash-and-store (wire-contract §2 "Passwordless-account path", auth.router.ts:916-928). It is the only way an account with no password ever acquires one.
Service.ChangePassword compares unconditionally (service.go:276), and verifyPassword("", "") is a bcrypt error, so delegating the passwordless case to it reports a password the account does not have as incorrect. The passwordless half is therefore handled here rather than by widening Service.ChangePassword: a library consumer calling the service directly may deliberately not want a caller setting a password without presenting one, and this PR's contract is the HTTP surface. The service-level divergence is filed upstream.
func (*Auth) CleanupExpiredSessions ¶ added in v0.2.0
CleanupExpiredSessions delegates to Service.CleanupExpiredSessions.
func (*Auth) ConfirmEmailChange ¶ added in v0.2.0
func (a *Auth) ConfirmEmailChange(ctx context.Context, in ConfirmEmailChangeInput) error
ConfirmEmailChange delegates to Service.ConfirmEmailChange.
func (*Auth) DeleteAccount ¶
func (a *Auth) DeleteAccount(ctx context.Context, in DeleteAccountInput) error
DeleteAccount delegates to Service.DeleteAccount.
func (*Auth) DisableTOTP ¶ added in v0.2.0
DisableTOTP delegates to Service.DisableTOTP.
func (*Auth) ForgotPassword ¶ added in v0.2.0
ForgotPassword delegates to Service.ForgotPassword and then throws away one class of failure: a send that failed after the token was stored. The route answers 200 {"success":true} for it, exactly as it does for an address that does not exist.
This is a deliberate divergence, and the only one on this route. The reference's send sits inside the route's try block, so a throwing mailer reaches handleError and answers 500 (auth.router.ts:787-798) — which means a known address 500s while an unknown one 200s, and an attacker with a broken mail gateway has the enumeration oracle the route exists to deny. The spec records exactly that and marks it [UNTESTED] (wire-contract §2, "Anti-enumeration caveat: a throwing mailer produces a 500 only for existing users, which is an observable oracle"), so no client can depend on it. The port already refuses to ship an oracle the reference leaves open elsewhere — SMSVerifyHTTPError answers 401 where the reference would answer 404 for the same reason.
Store failures are NOT swallowed: those still 500, and that oracle *is* reproduced (see ForgotPasswordHTTPError and the suite case that pins it). The difference is that a store that cannot persist a reset token has minted no credential and served no request correctly, while a stored-but-undelivered token is harmless — unguessable, single-use, expiring — so silence costs the caller nothing but a mail that never arrives.
Divergence from Service.ForgotPassword is the same arrangement as Auth.ChangePassword's: the Auth methods are the HTTP surface, and a library consumer calling the service directly still learns that delivery failed.
func (*Auth) IssueTempToken ¶ added in v0.2.0
IssueTempToken delegates to Service.IssueTempToken.
func (*Auth) IssueTokensForUser ¶ added in v0.2.0
func (a *Auth) IssueTokensForUser(ctx context.Context, userID, tenantID string) (User, AuthTokens, error)
IssueTokensForUser delegates to Service.IssueTokensForUser.
func (*Auth) LinkRequest ¶ added in v0.2.0
func (a *Auth) LinkRequest(ctx context.Context, in LinkRequestInput) (LinkRequestResult, error)
LinkRequest issues a single-use account-link token and hands the verification link to the configured delivery hook.
func (*Auth) LinkVerify ¶ added in v0.2.0
func (a *Auth) LinkVerify(ctx context.Context, in LinkVerifyInput) (LinkVerifyResult, error)
LinkVerify consumes an account-link token and writes the association.
func (*Auth) ListLinkedAccounts ¶ added in v0.2.0
func (a *Auth) ListLinkedAccounts(ctx context.Context, userID string) ([]PublicLinkedAccount, error)
ListLinkedAccounts returns a user's provider associations, always as a non-nil slice so the JSON is [] rather than null.
func (*Auth) ListSessions ¶ added in v0.2.0
ListSessions returns a user's *active* sessions, which is what the reference's GET /sessions serves: ISessionStore.getSessionsForUser is documented "Return all active sessions for the given user" (session-store.interface.ts:58-61), and because revokeSession is documented to "Invalidate (delete) a single session" (:70-76) — which the family's own store does literally, `async revokeSession(h) { this.sessions.delete(h); }` (ng-awesome-node-auth/src/server/in-memory-session-store.ts:29-31) — a revoked session is simply gone from the next list.
This port's MemorySessionStore tombstones instead of deleting, and correctly so: /refresh needs the tombstone to answer 401 SESSION_REVOKED rather than "not found". The filter therefore has to live above the store, and this is the accessor the routes go through.
Expired-but-not-revoked sessions are deliberately NOT filtered. The reference's store does not filter them out of getSessionsForUser either — only deleteExpiredSessions removes them — so a device-manager UI sees them there too.
func (*Auth) Login ¶
func (a *Auth) Login(ctx context.Context, in LoginInput) (User, AuthTokens, error)
Login delegates to Service.Login.
func (*Auth) LoginWithChallenge ¶ added in v0.3.0
func (a *Auth) LoginWithChallenge(ctx context.Context, in LoginInput) (LoginResult, error)
LoginWithChallenge delegates to Service.LoginWithChallenge. It is what the adapters call, so that a 2FA-gated account is answered with the challenge rather than with the bare sentinel.
func (*Auth) LogoutAccessToken ¶ added in v0.2.0
LogoutAccessToken revokes the session an access token belongs to. It is the revocation path a cookie client's logout takes, since the refresh cookie is out of scope for <prefix>/logout whenever it is path-scoped.
func (*Auth) LogoutRequest ¶ added in v0.2.0
LogoutRequest ends the session the caller presents, best effort, and is the single logout revocation path all four adapters call.
The order matters. A refresh token in the body or the refresh cookie revokes the whole session, so it is tried first. But the refresh cookie is scoped to <prefix>/refresh in every configuration whose name does not resolve to __Host- — a Domain, or a non-root cookie Path — and a browser therefore sends nothing at all to <prefix>/logout. That is why the reference logout reads the *access* token cookie (auth.router.ts:592) and revokes payload.sid: the access cookie is scoped to "/" and is always in scope. Without the fallback, logout answers 200 {"success":true}, clears the cookies, and leaves the refresh token and the session live server-side.
Every failure is swallowed: an absent or already-unusable credential must still leave the caller logged out rather than stranded with live cookies.
func (*Auth) OAuthBegin ¶ added in v0.2.0
func (a *Auth) OAuthBegin(ctx context.Context, in OAuthBeginInput) (OAuthBeginResult, error)
OAuthBegin mints a signed state, derives the PKCE challenge and builds the provider authorization URL.
func (*Auth) OAuthComplete ¶ added in v0.2.0
func (a *Auth) OAuthComplete(ctx context.Context, in OAuthCompleteInput) (OAuthCompleteResult, error)
OAuthComplete verifies the state, exchanges the code with the PKCE verifier and resolves the user.
func (*Auth) OAuthConfigured ¶ added in v0.2.0
OAuthConfigured reports whether the OAuth routes have anything to serve. The adapters mount the routes unconditionally, so they need this to answer the reference's "not configured" stub.
func (*Auth) Register ¶
func (a *Auth) Register(ctx context.Context, in RegisterInput) (User, AuthTokens, error)
Register delegates to Service.Register.
func (*Auth) RequestEmailChange ¶ added in v0.2.0
RequestEmailChange delegates to Service.RequestEmailChange.
func (*Auth) ResetPassword ¶ added in v0.2.0
func (a *Auth) ResetPassword(ctx context.Context, in ResetPasswordInput) error
ResetPassword delegates to Service.ResetPassword.
func (*Auth) ResolveHTTPConfig ¶ added in v0.2.0
func (a *Auth) ResolveHTTPConfig(cfg HTTPConfig) HTTPConfig
ResolveHTTPConfig fills cfg's derived defaults from this instance's token lifetimes. Adapters call it once at mount time.
func (*Auth) RevokeUserSession ¶ added in v0.2.0
RevokeUserSession delegates to Service.RevokeUserSession.
func (*Auth) SMSConfigured ¶ added in v0.3.0
SMSConfigured reports whether the deployment can deliver an SMS code.
/sms/send has to ask before it does anything else, because the reference checks config.sms at the very top of the route (auth.router.ts:1178-1181) and not in the strategy behind it. The ordering is client-visible, and not in a cosmetic way: the route answers 200 to an unknown address as an anti-enumeration measure, so a handler that only found out at the service call would tell an unconfigured deployment's caller "sent" for every address it does not have. The magic-link route needs no equivalent — the reference checks email configuration inside the strategy there, which Service.SendMagicLink reproduces, and WriteServiceError maps the sentinel.
func (*Auth) SendMagicLink ¶ added in v0.2.0
SendMagicLink delegates to Service.SendMagicLink.
func (*Auth) SendSMSCode ¶ added in v0.2.0
SendSMSCode delegates to Service.SendSMSCode.
func (*Auth) SendVerificationEmailToken ¶ added in v0.2.0
func (a *Auth) SendVerificationEmailToken(ctx context.Context, in EmailVerificationInput) (string, error)
SendVerificationEmailToken delegates to Service.SendVerificationEmailToken. An empty token with no error means the address is already verified.
func (*Auth) StartTOTPEnrolment ¶ added in v0.2.0
StartTOTPEnrolment delegates to Service.StartTOTPEnrolment.
func (*Auth) StepUpSubject ¶ added in v0.2.0
func (a *Auth) StepUpSubject(tempToken string, missing, invalid HTTPError) (TempTokenSubject, HTTPError, bool)
StepUpSubject resolves the tempToken a 2FA-mode request carries, reporting the envelope to write when it cannot.
The two error arguments are what keeps the reference's inconsistency reproducible without spreading it through the handlers: the magic-link and SMS routes pass HTTPErrTempTokenRequired and HTTPErrInvalidTempToken, while /2fa/verify passes HTTPErrInvalidStepUpToken for both — it has no missing-token branch at all, so an absent tempToken simply fails verification there.
func (*Auth) TwoFactorPolicy ¶ added in v0.2.0
TwoFactorPolicy reports whether the deployment requires a second factor of every user. It is the port's equivalent of the reference's settingsStore require2FA flag, which is what /2fa/disable refuses on.
func (*Auth) UnlinkAccount ¶ added in v0.2.0
UnlinkAccount removes one provider association.
The reference performs no existence check and answers success unconditionally (wire-contract.md §4, DELETE /linked-accounts). This keeps that, and adds the ownership check the reference gets for free by passing the user id into its store: a link that belongs to somebody else is left alone.
func (*Auth) UpdatePhoneNumber ¶ added in v0.2.0
UpdatePhoneNumber delegates to Service.UpdatePhoneNumber.
func (*Auth) UpdateProfile ¶
UpdateProfile delegates to Service.UpdateProfile.
func (*Auth) VerifyEmail ¶ added in v0.2.0
func (a *Auth) VerifyEmail(ctx context.Context, in VerifyEmailInput) error
VerifyEmail delegates to Service.VerifyEmail.
func (*Auth) VerifyMagicLink ¶ added in v0.2.0
func (a *Auth) VerifyMagicLink(ctx context.Context, in MagicLinkVerifyInput) (User, AuthTokens, error)
VerifyMagicLink delegates to Service.VerifyMagicLink.
func (*Auth) VerifyMagicLinkForUser ¶ added in v0.2.0
func (a *Auth) VerifyMagicLinkForUser(ctx context.Context, in MagicLinkVerifyInput, userID string) (User, AuthTokens, error)
VerifyMagicLinkForUser delegates to Service.VerifyMagicLinkForUser. The step-up branch of POST /magic-link/verify uses it so that a link belonging to another user is refused before a session exists.
func (*Auth) VerifySMSCode ¶ added in v0.2.0
func (a *Auth) VerifySMSCode(ctx context.Context, in SMSCodeVerifyInput) (User, AuthTokens, error)
VerifySMSCode delegates to Service.VerifySMSCode.
func (*Auth) VerifyTOTP ¶ added in v0.2.0
func (a *Auth) VerifyTOTP(ctx context.Context, userID, tenantID, code string) (User, AuthTokens, error)
VerifyTOTP delegates to Service.VerifyTOTP.
type AuthTokens ¶
AuthTokens are the issued access/refresh pair.
type CSRFConfig ¶
type CSRFConfig struct {
// Enabled gates both enforcement and cookie distribution, mirroring the
// reference's config.csrf.enabled.
Enabled bool
CookieName string
HeaderName string
}
CSRFConfig configures the cookie+header double-submit CSRF protection. The cookie attributes come from HTTPConfig.Cookies so that the CSRF cookie is serialised by the same matrix as the token cookies.
func DefaultCSRFConfig ¶
func DefaultCSRFConfig() CSRFConfig
DefaultCSRFConfig returns defaults compatible with browser cookie auth flows.
type ChangeEmailRequestInput ¶
type ChangePasswordInput ¶
type Config ¶
type Config struct {
Secret string
Issuer string
AccessTokenTTL time.Duration
RefreshTokenTTL time.Duration
SessionCheckOn string
ClockSkew time.Duration
MinPasswordLen int
// BcryptCost is the bcrypt cost used for every password hash the library
// writes. It is this port's spelling of the reference's
// AuthConfig.bcryptSaltRounds; "cost" is what x/crypto/bcrypt calls the
// parameter, and what bcrypt.DefaultCost, MinCost and MaxCost name.
//
// Zero means unset, and unset means bcrypt.DefaultCost — never
// bcrypt.MinCost. A Config that was never told a cost must not silently get
// the weakest one. Any other value must lie in
// [bcrypt.MinCost, bcrypt.MaxCost]; validate rejects the rest.
BcryptCost int
ResetTokenTTL time.Duration
MagicLinkTTL time.Duration
SMSCodeTTL time.Duration
EmailVerificationTTL time.Duration
EmailVerificationMode string
EmailChangeTTL time.Duration
TempTokenTTL time.Duration
Require2FA bool
BuildTokenClaims func(ctx context.Context, user User) (map[string]any, error)
// SendMagicLink and SendSMSCode are the delivery seam. Both are optional to
// construct a service with and required to use the route that needs them:
// leaving one nil is what makes POST <prefix>/magic-link/send answer 500
// EMAIL_NOT_CONFIGURED and POST <prefix>/sms/send answer 500
// SMS_NOT_CONFIGURED. See delivery.go for why they live here.
SendMagicLink MagicLinkSender
SendSMSCode SMSCodeSender
// SendPasswordReset, SendEmailVerification and SendEmailChange are the other
// half of the same seam, for POST <prefix>/forgot-password,
// POST <prefix>/send-verification-email and POST <prefix>/change-email/request.
//
// Leaving one nil is silence rather than an error: the reference's routes send
// no mail and still answer 200 when neither a callback nor a mailer is
// configured, so these three have no NOT_CONFIGURED code. See
// delivery_password_email.go.
SendPasswordReset PasswordResetSender
SendEmailVerification EmailVerificationSender
SendEmailChange EmailChangeSender
Logger func(format string, args ...any)
}
Config contains security and token settings for the auth service.
func DefaultConfig ¶
DefaultConfig returns secure defaults for development and production bootstrap.
type ConfirmEmailChangeInput ¶
type ConfirmEmailChangeInput struct {
Token string
}
type CookieOptions ¶ added in v0.2.0
type CookieOptions struct {
Secure bool
SameSite http.SameSite
Path string
Domain string
// RefreshTokenPath scopes the refresh cookie. Empty resolves to
// <APIPrefix>/refresh, matching the reference's router-time default.
RefreshTokenPath string
AccessTokenMaxAge time.Duration
RefreshTokenMaxAge time.Duration
CSRFTokenMaxAge time.Duration
}
CookieOptions is the cookie serialisation matrix. Every adapter writes cookies through it, so the attributes cannot diverge between frameworks.
func DefaultCookieOptions ¶ added in v0.2.0
func DefaultCookieOptions() CookieOptions
DefaultCookieOptions returns the port's defaults. Secure defaults to true — stricter than the reference's false — because every cookie here is either a credential or a CSRF token.
func (CookieOptions) ClearAuthCookies ¶ added in v0.2.0
func (o CookieOptions) ClearAuthCookies(w http.ResponseWriter, includeCSRF bool)
ClearAuthCookies expires every name variant of the auth cookies. Expiring a cookie requires an exact name+path+domain match, so a cookie written under a different prefix — by an older release, or before Secure was switched on — would otherwise survive logout.
func (CookieOptions) CookieName ¶ added in v0.2.0
func (o CookieOptions) CookieName(base string) string
CookieName resolves the prefix a cookie must carry: an insecure cookie stays bare, a secure cookie that is host-scoped to "/" earns __Host-, and any other secure cookie earns __Secure-. The refresh cookie is resolved from Path, not RefreshTokenPath, exactly as the reference does.
func (CookieOptions) SetAccessTokenCookie ¶ added in v0.2.0
func (o CookieOptions) SetAccessTokenCookie(w http.ResponseWriter, value string)
SetAccessTokenCookie writes the access-token cookie.
func (CookieOptions) SetCSRFTokenCookie ¶ added in v0.2.0
func (o CookieOptions) SetCSRFTokenCookie(w http.ResponseWriter, value string)
SetCSRFTokenCookie writes the CSRF cookie. It is deliberately not HttpOnly: double-submit requires the page to read it and mirror it into the header.
func (CookieOptions) SetRefreshTokenCookie ¶ added in v0.2.0
func (o CookieOptions) SetRefreshTokenCookie(w http.ResponseWriter, value string)
SetRefreshTokenCookie writes the refresh-token cookie.
type DeleteAccountInput ¶
type Deviation ¶ added in v0.3.0
type Deviation struct {
// ID is the stable handle for this deviation. It never changes once
// published: the README keys its mirror on it and the tests pin the set, so
// renaming one is a deliberate, visible act rather than a silent edit.
ID string `json:"id"`
// Title is the one-line heading a human reads first. It becomes the README
// section heading for this entry.
Title string `json:"title"`
// Surface is the route or API surface affected, written as it is mounted —
// paths are relative to the configured prefix, spelled <prefix> here.
Surface string `json:"surface"`
// Behaviour is what this port does.
Behaviour string `json:"behaviour"`
// Reference is what awesome-node-auth does instead.
Reference string `json:"reference"`
// Citations locate Reference in the reference source, as file:line or
// file:line-line, resolved against ReferenceRevision.
Citations []string `json:"citations"`
// Why is the reason the standing rule was set aside. An entry without one is
// not a decision, it is a bug that has not been recognised yet.
Why string `json:"why"`
// Notes carries any further labelled paragraphs this entry needs beyond the
// four above — how to observe the difference, what a host can do about it.
// Most entries need none.
Notes []DeviationNote `json:"notes,omitempty"`
}
Deviation is one place this port deliberately answers differently from the reference.
The two behaviours are recorded separately, and the reference's carries a citation, so that a reader can check the claim rather than take it — and so that a maintainer who changes one of them has to come here and say so.
type DeviationNote ¶ added in v0.3.0
DeviationNote is one extra labelled paragraph on a Deviation.
type EmailChangeDelivery ¶ added in v0.3.0
type EmailChangeDelivery struct {
UserID string
TenantID string
// NewEmail is the pending address the message goes to.
NewEmail string
// Token is the plaintext token POST <prefix>/change-email/confirm accepts.
Token string
ExpiresAt time.Time
}
EmailChangeDelivery is what an EmailChangeSender is handed.
NewEmail is the only address here, and it is the recipient: this message verifies that the new mailbox exists, so it goes to the new mailbox (auth.router.ts:1027-1032 mails newEmail; wire-contract §2 "sent to the new address"). The current address is deliberately absent — the delivery carries only what the message needs, and a sender that wants the old address has UserID to read it back with.
The reference also mails the *old* address a notice once the change is applied (config.email.sendEmailChanged from /change-email/confirm, auth.router.ts:1060-1066). This port has no sender for that notice and no template for it either — MailTemplater's email_change template is the confirm-the-new-address mail, not the notice — so the gap is recorded here rather than filled by inventing one. It belongs with a template addition.
type EmailChangeMailer ¶ added in v0.3.0
type EmailChangeMailer struct{ TokenMailer }
EmailChangeMailer is the ready-made EmailChangeSender. It mails the pending address and renders the built-in email_change template.
The reference has no template of its own here: it reuses its verification sender and verification template for this mail (wire-contract §2, "there is no dedicated change-email template"). This port ships email_change already, in en and it, and nothing called it until now — so the better-worded template wins. The choice is invisible on the wire: the route answers {"success":true} either way, and only the mail body differs.
func NewEmailChangeMailer ¶ added in v0.3.0
func NewEmailChangeMailer(transport MailerTransport, appName, baseURL string) *EmailChangeMailer
NewEmailChangeMailer builds an EmailChangeMailer with the built-in templates.
func (*EmailChangeMailer) Send ¶ added in v0.3.0
func (m *EmailChangeMailer) Send(ctx context.Context, delivery EmailChangeDelivery) error
Send renders and delivers the email-change mail to the new address. Its signature is EmailChangeSender's.
type EmailChangeSender ¶ added in v0.3.0
type EmailChangeSender func(ctx context.Context, delivery EmailChangeDelivery) error
EmailChangeSender delivers an email-change token to the pending address. See EmailVerificationSender for the failure contract.
type EmailChangeStore ¶
type EmailChangeStore interface {
UpdateEmailChangeToken(ctx context.Context, userID, tenantID, pendingEmail, tokenHash string, expiry time.Time) error
GetUserByEmailChangeTokenHash(ctx context.Context, tokenHash string) (User, error)
ApplyEmailChange(ctx context.Context, userID, tenantID string) error
ClearEmailChangeToken(ctx context.Context, userID, tenantID string) error
}
EmailChangeStore persists pending email change flow.
type EmailVerificationDelivery ¶ added in v0.3.0
type EmailVerificationDelivery struct {
UserID string
TenantID string
Email string
// Token is the plaintext token GET <prefix>/verify-email accepts.
Token string
ExpiresAt time.Time
}
EmailVerificationDelivery is what an EmailVerificationSender is handed. The route behind it is authenticated, so Email is the caller's own stored address.
type EmailVerificationInput ¶
type EmailVerificationMailer ¶ added in v0.3.0
type EmailVerificationMailer struct{ TokenMailer }
EmailVerificationMailer is the ready-made EmailVerificationSender, rendering the built-in verify_email template. See PasswordResetMailer for the usage shape.
func NewEmailVerificationMailer ¶ added in v0.3.0
func NewEmailVerificationMailer(transport MailerTransport, appName, baseURL string) *EmailVerificationMailer
NewEmailVerificationMailer builds an EmailVerificationMailer with the built-in templates.
func (*EmailVerificationMailer) Send ¶ added in v0.3.0
func (m *EmailVerificationMailer) Send(ctx context.Context, delivery EmailVerificationDelivery) error
Send renders and delivers the verification mail. Its signature is EmailVerificationSender's.
type EmailVerificationSender ¶ added in v0.3.0
type EmailVerificationSender func(ctx context.Context, delivery EmailVerificationDelivery) error
EmailVerificationSender delivers an email-verification token. An error fails the route with the reference's generic 500 and leaves the stored token in place, as MagicLinkSender does.
type EmailVerificationStore ¶
type EmailVerificationStore interface {
UpdateEmailVerificationToken(ctx context.Context, userID, tenantID, tokenHash string, expiry time.Time) error
GetUserByEmailVerificationTokenHash(ctx context.Context, tokenHash string) (User, error)
MarkEmailVerified(ctx context.Context, userID, tenantID string, verified bool) error
ClearEmailVerificationToken(ctx context.Context, userID, tenantID string) error
}
EmailVerificationStore persists verification flow.
type Event ¶
type Event struct {
Name string
UserID string
TenantID string
Timestamp time.Time
Data map[string]any
}
Event is a generic auth domain event payload.
type EventBus ¶
type EventBus struct {
// contains filtered or unexported fields
}
EventBus is a tiny in-process pub/sub for auth events.
func NewEventBus ¶
func NewEventBus() *EventBus
type ForgotPasswordInput ¶
type HTTPConfig ¶ added in v0.2.0
type HTTPConfig struct {
APIPrefix string
Cookies CookieOptions
CSRF CSRFConfig
}
HTTPConfig is the wire configuration an adapter mounts with. The four adapters take the same value, so a change here reaches all of them.
func DefaultHTTPConfig ¶ added in v0.2.0
func DefaultHTTPConfig() HTTPConfig
DefaultHTTPConfig returns the conventions an adapter uses when the host app does not configure them.
func (HTTPConfig) Prefix ¶ added in v0.2.0
func (c HTTPConfig) Prefix() string
Prefix returns the normalised mount prefix.
func (HTTPConfig) WriteTokens ¶ added in v0.2.0
func (c HTTPConfig) WriteTokens(w http.ResponseWriter, r *http.Request, status int, tokens AuthTokens, extra map[string]any)
WriteTokens delivers an issued token pair. A bearer caller gets the tokens in the body and no cookies; a cookie caller gets the cookies and a body that never contains a token. extra carries any route-specific top-level fields, such as register's userId.
type HTTPError ¶ added in v0.2.0
type HTTPError struct {
Status int `json:"-"`
Message string `json:"error"`
Code string `json:"code,omitempty"`
}
HTTPError is the family error envelope together with its status code.
The "code" field is omitted when the reference emits no code for that failure; clients are documented not to pattern-match on an absent code.
func AccessHTTPError ¶ added in v0.2.0
AccessHTTPError maps an access-token verification failure the way the reference auth middleware does.
func ChangeEmailConfirmHTTPError ¶ added in v0.2.0
ChangeEmailConfirmHTTPError maps a Service.ConfirmEmailChange failure.
func ChangeEmailInlineError ¶ added in v0.2.0
ChangeEmailInlineError reproduces the reference's PASSWORD_REQUIRED guard on /change-email/request: an account whose only credential is the address itself may not move that address (auth.router.ts:1015-1021).
The reference runs this check after the 409 and 404 branches; here it runs before them, because the service persists the pending change in the same call that would report those two failures and there is no point at which the guard could still run without leaving a token behind. The status and code are therefore exact, and only the precedence between two simultaneous failures differs.
func ChangeEmailRequestHTTPError ¶ added in v0.2.0
ChangeEmailRequestHTTPError maps a Service.RequestEmailChange failure.
As on /send-verification-email, the 404 branch is a store-race path only: this port's auth gate answers 403 where the reference's handler would answer 404.
func ChangePasswordHTTPError ¶ added in v0.2.0
ChangePasswordHTTPError maps a Service.ChangePassword failure.
ErrInvalidCredentials becomes 401 "Current password is incorrect" rather than the reference's 404 "User not found", because the service returns the same sentinel for both and the auth middleware resolved this user from the store moments earlier: a wrong password is the reachable case, a row that vanished in between is not.
func ChangePasswordInlineError ¶ added in v0.2.0
ChangePasswordInlineError reproduces the reference's inline validation on /change-password: an account with no stored password that submits neither field gets 400 "New password is required" (auth.router.ts:922-925). An account that does have a password never reaches it — a missing current password fails the comparison instead.
func ForgotPasswordHTTPError ¶ added in v0.2.0
ForgotPasswordHTTPError maps a Service.ForgotPassword failure.
Every failure is a bare 500: the reference has no error branch on this route at all, so a throwing store reaches handleError and becomes {"error":"Internal server error"} (auth.router.ts:796-798). ErrFeatureNotSupported is included on purpose — a store that cannot persist a reset token is a deployment fault, not a feature the caller can be told about, and answering 501 would tell an anonymous caller something the 200-always rule is there to hide.
A failed *delivery* never reaches this mapper: Auth.ForgotPassword absorbs it so the route keeps answering 200. See there for why that one diverges.
func HTTPErrorFor ¶ added in v0.2.0
HTTPErrorFor maps a service sentinel onto the envelope. Routes whose failure means something more specific than the sentinel says — /refresh and the auth middleware both narrow "invalid token" — use their own mapper below.
Anything unmapped becomes a 500, so a sentinel that reaches a route before it reaches this switch turns a client-visible failure into an internal error. The sentinels deliberately left out are the ones whose reference wire string is route-specific and must not be guessed here: ErrInvalidCode is "Invalid or expired SMS code" on /sms/verify and "Invalid TOTP code" on /2fa/verify (both 401, both code-less), and ErrTenantNotFound / ErrRoleNotFound belong to admin routes. A route that can return one of those has to write it itself. TestUnmappedSentinelsAreDeliberate keeps that list honest.
func MagicLinkVerifyHTTPError ¶ added in v0.2.0
MagicLinkVerifyHTTPError maps a Service.VerifyMagicLink or Service.VerifyMagicLinkForUser failure. See HTTPErrInvalidMagicLink for why the expiry code does not appear.
func OAuthHTTPError ¶ added in v0.2.0
OAuthHTTPError maps a flow failure from this file onto the wire envelope. Routes call it instead of WriteServiceError because every literal here is route-specific: HTTPErrorFor would turn each of them into a 500.
func RefreshHTTPError ¶ added in v0.2.0
RefreshHTTPError maps a Service.Refresh failure. A revoked session stays distinguishable from an unusable token, which is the whole point of the code.
func ResetPasswordHTTPError ¶ added in v0.2.0
ResetPasswordHTTPError maps a Service.ResetPassword failure.
func SMSVerifyHTTPError ¶ added in v0.2.0
SMSVerifyHTTPError maps a Service.VerifySMSCode failure.
ErrInvalidCredentials means the userId or tempToken named a user the store does not have, and that is 401 "Invalid or expired SMS code", not 404. The reference looks the user up inside the strategy, where a miss is indistinguishable from a bad code: sms.strategy.ts:24-25 returns false for an unknown id, and the router turns false into 401 {"error":"Invalid or expired SMS code"} (auth.router.ts:1275-1279). Its 404 (:1281-1284) sits *after* that check and only fires if the user vanished between the verify and the follow-up findById — a race this port cannot reproduce, because Service.VerifySMSCode does both in one call. Answering 404 here would both change the status a client sees for a stale userId and turn an unauthenticated route into a user-existence oracle the reference does not have.
func SendVerificationEmailHTTPError ¶ added in v0.2.0
SendVerificationEmailHTTPError maps a Service.SendVerificationEmailToken failure.
The 404 "User not found" branch is not reachable over HTTP in this port and is kept only for a store race: the reference's auth gate trusts the JWT and never reads the store (auth.middleware.ts:60), so its handler's own lookup can miss and answer 404, whereas this port's gate resolves the user through Me() and answers 403 "Invalid or expired access token" for a row that no longer exists. The divergence belongs to the shared middleware, not to this route.
func TOTPSetupHTTPError ¶ added in v0.2.0
TOTPSetupHTTPError maps a Service.VerifyTOTPSetup failure (POST /2fa/verify-setup), where a bad code is a 400 rather than the 401 the step-up route answers.
func TOTPVerifyHTTPError ¶ added in v0.2.0
TOTPVerifyHTTPError maps a Service.VerifyTOTP failure (POST /2fa/verify).
func VerifyEmailHTTPError ¶ added in v0.2.0
VerifyEmailHTTPError maps a Service.VerifyEmail failure.
type HTTPMailerTransport ¶
type HTTPMailerTransport struct {
EndpointURL string
Secret string
// contains filtered or unexported fields
}
HTTPMailerTransport sends mail via any HTTP endpoint (no SMTP needed).
func NewHTTPMailerTransport ¶
func NewHTTPMailerTransport(endpointURL, secret string) *HTTPMailerTransport
NewHTTPMailerTransport creates a transport that POSTs to a webhook-style HTTP endpoint.
func (*HTTPMailerTransport) Send ¶
func (t *HTTPMailerTransport) Send(ctx context.Context, msg MailMessage) error
type HTTPSMSTransport ¶ added in v0.3.0
type HTTPSMSTransport struct {
EndpointURL string
APIKey string
Username string
Password string
// contains filtered or unexported fields
}
HTTPSMSTransport talks to the gateway contract the rest of this family talks to: a GET against the endpoint carrying username, password, phone and message as query parameters, with the API key in an X-API-Key header (awesome-node-auth src/services/sms.service.ts:16-46, documented in config-schema.md §1.6).
The credentials travel in the URL. That is a real hazard — query strings reach access logs, proxies and Referer headers — and the spec calls it out as one (config-schema.md §1.6, "credentials-in-URL hazard the product must fix"). It is reproduced here anyway, because the endpoint on the other side is the deployment's existing gateway and it is the request that gateway is built to accept; a "fixed" request would simply fail to send. The fix belongs at the gateway, and until it happens the way out is the seam itself: Config.SendSMSCode takes any SMSCodeSender, so a deployment whose provider accepts a safer shape supplies its own transport and never constructs this one.
func NewHTTPSMSTransport ¶ added in v0.3.0
func NewHTTPSMSTransport(endpointURL, apiKey, username, password string) *HTTPSMSTransport
NewHTTPSMSTransport creates a transport for a query-parameter SMS gateway.
type IDP ¶
type IDP struct {
// contains filtered or unexported fields
}
IDP is an OIDC Identity Provider.
type LinkRequestInput ¶ added in v0.2.0
type LinkRequestInput struct {
Email string
Provider string
// EmailLang is the reference's third body field. It selects the language of
// the verification mail and is forwarded verbatim to DeliverLinkToken, the way
// the reference passes it as sendVerificationEmail's fourth argument
// (auth.router.ts:1530-1535).
EmailLang string
AccessToken string
// Origin and Referer resolve which origin the verification link points at, as
// the reference's resolveSiteUrl(req, config, allowedOrigins) does
// (auth.router.ts:1528). A multi-origin deployment must email a link back to
// the origin the request came from, not always to the default one.
Origin string
Referer string
// APIPrefix is the mount prefix, needed to build the verification link.
APIPrefix string
}
LinkRequestInput is POST <prefix>/link-request.
type LinkRequestResult ¶ added in v0.2.0
LinkRequestResult reports what was issued. The token never reaches the HTTP response; it is returned so an embedder driving the service directly can send it themselves.
type LinkTokenDelivery ¶ added in v0.2.0
type LinkTokenDelivery struct {
Email string
Provider string
Token string
URL string
// EmailLang is the /link-request body's emailLang, passed through untouched so
// the host's mail transport can localise. Empty when the caller omitted it.
EmailLang string
}
LinkTokenDelivery is the payload handed to OAuthWiring.DeliverLinkToken. The four members are the reference's sendVerificationEmail(email, token, link, emailLang) arguments, plus the provider the link records.
type LinkVerifyInput ¶ added in v0.2.0
LinkVerifyInput is POST <prefix>/link-verify.
type LinkVerifyResult ¶ added in v0.2.0
type LinkVerifyResult struct {
User User
Tokens AuthTokens
LoggedIn bool
}
LinkVerifyResult carries the tokens when loginAfterLinking was asked for.
type LinkedAccountStore ¶
type LinkedAccountStore interface {
Save(ctx context.Context, link OAuthLinkedAccount) error
FindByProvider(ctx context.Context, provider, providerID string) (OAuthLinkedAccount, error)
ListForUser(ctx context.Context, userID string) ([]OAuthLinkedAccount, error)
Delete(ctx context.Context, id string) error
}
LinkedAccountStore persists OAuth provider associations per user.
type LoginInput ¶
LoginInput is the payload used for user authentication.
type LoginResult ¶ added in v0.3.0
type LoginResult struct {
// User is whose password verified. It is populated on both outcomes, because
// on both outcomes the password did verify and a host that audit-logs a
// challenge has to know whose it was.
User User
// Tokens is the issued session. It is zero when Challenge is set: a challenge
// is not a login, and no session exists until the second factor lands.
Tokens AuthTokens
// Challenge is nil for a login that completed.
Challenge *TwoFactorChallenge
}
LoginResult is the outcome of a password login: either a session, or a second-factor challenge.
type MCPServer ¶
type MCPServer struct {
// contains filtered or unexported fields
}
MCPServer exposes auth configuration as MCP tools for AI editors. It speaks JSON-RPC 2.0 over HTTP and implements the Model Context Protocol tools/list and tools/call methods.
func NewMCPServer ¶
NewMCPServer creates an MCP server wrapping an auth service.
type MagicLinkDelivery ¶ added in v0.3.0
type MagicLinkDelivery struct {
UserID string
TenantID string
Email string
// Token is the plaintext token the matching verify route accepts.
Token string
// ExpiresAt is the stored expiry, Config.MagicLinkTTL from now. Verification
// additionally allows Config.ClockSkew past it, so a message that quotes this
// value understates the window slightly rather than overstating it.
ExpiresAt time.Time
}
MagicLinkDelivery is what a MagicLinkSender is handed.
It carries the plaintext magic-link token — the store only ever sees its hash — and deliberately nothing else that is credential material. A sender is host code that may log or forward what it receives, so it is given the one secret it has to transmit and no other: no password hash, no TOTP secret, no User. A sender that needs more about the recipient has UserID and TenantID to read it back with.
type MagicLinkMailer ¶ added in v0.3.0
type MagicLinkMailer struct {
// Transport delivers the rendered message. Required.
Transport MailerTransport
// BaseURL is what MagicLinkURL builds the link under.
BaseURL string
// Locale selects the built-in template set: "en" (the default) or "it". An
// unknown locale falls back to English, as MailTemplater.Render does.
//
// It is static because the reference's per-request override (the emailLang
// body field) has no counterpart on these routes yet: the port's send
// requests do not carry the field, so threading a locale from the wire is a
// change to the request shape and belongs with the rest of the template
// configuration.
Locale string
// Templates renders subject and body. NewMagicLinkMailer fills this in.
Templates *MailTemplater
}
MagicLinkMailer is the ready-made MagicLinkSender: it renders the built-in magic_link template and hands the result to a MailerTransport. It is the port's equivalent of the reference's config.email.mailer path, where a deployment configures a transport instead of writing a callback.
Use it as a sender through its Send method:
auth.WithMagicLinkSender(auth.NewMagicLinkMailer( auth.NewHTTPMailerTransport(endpoint, secret), "Example App", "https://app.example.com/auth", ).Send)
func NewMagicLinkMailer ¶ added in v0.3.0
func NewMagicLinkMailer(transport MailerTransport, appName, baseURL string) *MagicLinkMailer
NewMagicLinkMailer builds a MagicLinkMailer with the built-in templates. appName is the name those templates greet the recipient with.
func (*MagicLinkMailer) Send ¶ added in v0.3.0
func (m *MagicLinkMailer) Send(ctx context.Context, delivery MagicLinkDelivery) error
Send renders and delivers the magic-link mail. Its signature is MagicLinkSender's.
type MagicLinkSendInput ¶
type MagicLinkSender ¶ added in v0.3.0
type MagicLinkSender func(ctx context.Context, delivery MagicLinkDelivery) error
MagicLinkSender delivers a magic link. An error fails the route with the reference's generic 500 — a transport failure must not describe itself to the caller — and leaves the stored token in place, which is what the reference does too: an undelivered token is unguessable and expires on its own.
type MagicLinkStore ¶
type MagicLinkStore interface {
UpdateMagicLinkToken(ctx context.Context, userID, tenantID, tokenHash string, expiry time.Time) error
GetUserByMagicLinkTokenHash(ctx context.Context, tokenHash string) (User, error)
ClearMagicLinkToken(ctx context.Context, userID, tenantID string) error
}
MagicLinkStore persists magic link state.
type MagicLinkVerifyInput ¶
type MagicLinkVerifyInput struct {
Token string
}
type MailMessage ¶
MailMessage is a single email to be sent.
type MailTemplateData ¶
type MailTemplateData struct {
AppName string
UserName string
Token string
URL string
Code string
Extra map[string]any
}
MailTemplateData holds the common variables injected into mail templates.
type MailTemplater ¶
type MailTemplater struct {
// contains filtered or unexported fields
}
MailTemplater renders localized mail templates.
func NewMailTemplater ¶
func NewMailTemplater(appName string) *MailTemplater
NewMailTemplater creates a templater with built-in en and it templates.
func (*MailTemplater) Render ¶
func (m *MailTemplater) Render(locale, name string, data MailTemplateData) (subject, body string, err error)
Render produces the subject and HTML body for the given locale and template name.
type MailerTransport ¶
type MailerTransport interface {
Send(ctx context.Context, msg MailMessage) error
}
MailerTransport delivers email messages.
type MemoryLinkedAccounts ¶
type MemoryLinkedAccounts struct {
// contains filtered or unexported fields
}
MemoryLinkedAccounts is an in-memory implementation of LinkedAccountStore, suitable for development, testing, and embedded deployments.
func NewMemoryLinkedAccounts ¶
func NewMemoryLinkedAccounts() *MemoryLinkedAccounts
NewMemoryLinkedAccounts creates an in-memory LinkedAccountStore.
func (*MemoryLinkedAccounts) Delete ¶
func (m *MemoryLinkedAccounts) Delete(_ context.Context, id string) error
func (*MemoryLinkedAccounts) FindByProvider ¶
func (m *MemoryLinkedAccounts) FindByProvider(_ context.Context, provider, providerID string) (OAuthLinkedAccount, error)
func (*MemoryLinkedAccounts) ListForUser ¶
func (m *MemoryLinkedAccounts) ListForUser(_ context.Context, userID string) ([]OAuthLinkedAccount, error)
func (*MemoryLinkedAccounts) Save ¶
func (m *MemoryLinkedAccounts) Save(_ context.Context, link OAuthLinkedAccount) error
type MemoryMetadataStore ¶
type MemoryMetadataStore struct {
// contains filtered or unexported fields
}
MemoryMetadataStore is an in-memory implementation of UserMetadataStore.
func NewMemoryMetadataStore ¶
func NewMemoryMetadataStore() *MemoryMetadataStore
func (*MemoryMetadataStore) ClearMetadata ¶
func (s *MemoryMetadataStore) ClearMetadata(_ context.Context, userID string) error
func (*MemoryMetadataStore) GetMetadata ¶
func (*MemoryMetadataStore) UpdateMetadata ¶
type MemoryPendingLinks ¶ added in v0.2.0
type MemoryPendingLinks struct {
// contains filtered or unexported fields
}
MemoryPendingLinks is an in-memory PendingLinkStore, the counterpart of MemoryLinkedAccounts. It enforces the ttl, which is what makes an OAuth state nonce single-use and an account-link token expire.
func NewMemoryPendingLinks ¶ added in v0.2.0
func NewMemoryPendingLinks() *MemoryPendingLinks
NewMemoryPendingLinks creates an in-memory PendingLinkStore.
func (*MemoryPendingLinks) Delete ¶ added in v0.2.0
func (m *MemoryPendingLinks) Delete(_ context.Context, key string) error
func (*MemoryPendingLinks) Get ¶ added in v0.2.0
func (m *MemoryPendingLinks) Get(_ context.Context, key string) (OAuthPendingMeta, error)
func (*MemoryPendingLinks) Save ¶ added in v0.2.0
func (m *MemoryPendingLinks) Save(_ context.Context, key string, meta OAuthPendingMeta, ttl time.Duration) error
type MemoryRolesPermissionsStore ¶
type MemoryRolesPermissionsStore struct {
// contains filtered or unexported fields
}
MemoryRolesPermissionsStore is an in-memory RBAC store.
func NewMemoryRolesPermissionsStore ¶
func NewMemoryRolesPermissionsStore() *MemoryRolesPermissionsStore
func (*MemoryRolesPermissionsStore) AddPermissionToRole ¶
func (s *MemoryRolesPermissionsStore) AddPermissionToRole(_ context.Context, role, permission string) error
func (*MemoryRolesPermissionsStore) AddRoleToUser ¶
func (*MemoryRolesPermissionsStore) CreateRole ¶
func (*MemoryRolesPermissionsStore) DeleteRole ¶
func (s *MemoryRolesPermissionsStore) DeleteRole(_ context.Context, role string) error
func (*MemoryRolesPermissionsStore) GetPermissionsForRole ¶
func (*MemoryRolesPermissionsStore) GetPermissionsForUser ¶
func (*MemoryRolesPermissionsStore) GetRolesForUser ¶
func (*MemoryRolesPermissionsStore) RemovePermissionFromRole ¶
func (s *MemoryRolesPermissionsStore) RemovePermissionFromRole(_ context.Context, role, permission string) error
func (*MemoryRolesPermissionsStore) RemoveRoleFromUser ¶
func (*MemoryRolesPermissionsStore) UserHasPermission ¶
type MemorySessionStore ¶
type MemorySessionStore struct {
// contains filtered or unexported fields
}
MemorySessionStore is a thread-safe in-memory implementation of SessionStore.
func NewMemorySessionStore ¶
func NewMemorySessionStore() *MemorySessionStore
func (*MemorySessionStore) CreateSession ¶
func (*MemorySessionStore) DeleteExpiredSessions ¶
func (*MemorySessionStore) GetSessionByID ¶
func (*MemorySessionStore) GetSessionByRefreshTokenHash ¶
func (*MemorySessionStore) ListSessionsForUser ¶
func (*MemorySessionStore) RevokeSessionByID ¶
func (s *MemorySessionStore) RevokeSessionByID(_ context.Context, sessionID string) error
func (*MemorySessionStore) UpdateSession ¶
func (s *MemorySessionStore) UpdateSession(_ context.Context, session Session) error
type MemoryTelemetryStore ¶
type MemoryTelemetryStore struct {
// contains filtered or unexported fields
}
MemoryTelemetryStore is an in-memory implementation of TelemetryStore.
func NewMemoryTelemetryStore ¶
func NewMemoryTelemetryStore() *MemoryTelemetryStore
NewMemoryTelemetryStore creates an in-memory telemetry store.
func (*MemoryTelemetryStore) Query ¶
func (m *MemoryTelemetryStore) Query(_ context.Context, f TelemetryFilter) ([]TelemetryEvent, error)
func (*MemoryTelemetryStore) Record ¶
func (m *MemoryTelemetryStore) Record(_ context.Context, event TelemetryEvent) error
type MemoryTenantStore ¶
type MemoryTenantStore struct {
// contains filtered or unexported fields
}
MemoryTenantStore is an in-memory tenant/membership store.
func NewMemoryTenantStore ¶
func NewMemoryTenantStore() *MemoryTenantStore
func (*MemoryTenantStore) AssociateUserWithTenant ¶
func (s *MemoryTenantStore) AssociateUserWithTenant(_ context.Context, userID, tenantID string) error
func (*MemoryTenantStore) CreateTenant ¶
func (*MemoryTenantStore) DeleteTenant ¶
func (s *MemoryTenantStore) DeleteTenant(_ context.Context, id string) error
func (*MemoryTenantStore) DisassociateUserFromTenant ¶
func (s *MemoryTenantStore) DisassociateUserFromTenant(_ context.Context, userID, tenantID string) error
func (*MemoryTenantStore) GetAllTenants ¶
func (s *MemoryTenantStore) GetAllTenants(_ context.Context) ([]Tenant, error)
func (*MemoryTenantStore) GetTenantByID ¶
func (*MemoryTenantStore) GetTenantsForUser ¶
func (*MemoryTenantStore) GetUsersForTenant ¶
func (*MemoryTenantStore) UpdateTenant ¶
type MemoryUserStore ¶
type MemoryUserStore struct {
// contains filtered or unexported fields
}
MemoryUserStore is a thread-safe in-memory implementation of UserStore.
func NewMemoryUserStore ¶
func NewMemoryUserStore() *MemoryUserStore
func (*MemoryUserStore) ApplyEmailChange ¶
func (s *MemoryUserStore) ApplyEmailChange(_ context.Context, userID, tenantID string) error
func (*MemoryUserStore) ClearEmailChangeToken ¶
func (s *MemoryUserStore) ClearEmailChangeToken(_ context.Context, userID, tenantID string) error
func (*MemoryUserStore) ClearEmailVerificationToken ¶
func (s *MemoryUserStore) ClearEmailVerificationToken(_ context.Context, userID, tenantID string) error
func (*MemoryUserStore) ClearMagicLinkToken ¶
func (s *MemoryUserStore) ClearMagicLinkToken(_ context.Context, userID, tenantID string) error
func (*MemoryUserStore) ClearResetToken ¶
func (s *MemoryUserStore) ClearResetToken(_ context.Context, userID, tenantID string) error
func (*MemoryUserStore) ClearSMSCode ¶
func (s *MemoryUserStore) ClearSMSCode(_ context.Context, userID, tenantID string) error
func (*MemoryUserStore) CreateUser ¶
func (*MemoryUserStore) DeleteUser ¶
func (s *MemoryUserStore) DeleteUser(_ context.Context, userID, tenantID string) error
func (*MemoryUserStore) GetUserByEmail ¶
func (*MemoryUserStore) GetUserByEmailChangeTokenHash ¶
func (*MemoryUserStore) GetUserByEmailVerificationTokenHash ¶
func (*MemoryUserStore) GetUserByID ¶
func (*MemoryUserStore) GetUserByMagicLinkTokenHash ¶
func (*MemoryUserStore) GetUserByResetTokenHash ¶
func (*MemoryUserStore) GetUserBySMSCodeHash ¶
func (*MemoryUserStore) MarkEmailVerified ¶
func (*MemoryUserStore) UpdateEmailChangeToken ¶
func (*MemoryUserStore) UpdateEmailVerificationToken ¶
func (*MemoryUserStore) UpdateMagicLinkToken ¶
func (*MemoryUserStore) UpdatePassword ¶
func (s *MemoryUserStore) UpdatePassword(_ context.Context, userID, tenantID, passwordHash string) error
func (*MemoryUserStore) UpdatePhoneNumber ¶ added in v0.2.0
func (s *MemoryUserStore) UpdatePhoneNumber(_ context.Context, userID, tenantID, phoneNumber string) (User, error)
UpdatePhoneNumber makes MemoryUserStore satisfy UserPhoneStore. It lives beside the interface rather than in memory_store.go so that the capability and its in-memory implementation stay in one place.
func (*MemoryUserStore) UpdateProfile ¶
func (*MemoryUserStore) UpdateResetToken ¶
func (*MemoryUserStore) UpdateSMSCode ¶
func (*MemoryUserStore) UpdateTOTPSecret ¶
type OAuthBeginInput ¶ added in v0.2.0
OAuthBeginInput is GET <prefix>/oauth/{provider}.
There is deliberately no "link to the caller's account" field. The reference's /oauth/{provider} has no auth gate and reads no credential (wire-contract.md §4: "Auth gate: none"); the intent is not carried in the state either, and the callback resolves the identity purely through the provider account. Reading the initiating request's access token and linking the provider identity to it is an account-takeover shape: whoever happened to be signed in when the flow started would get the session, and the (provider, providerAccountId) binding would be re-pointed at them. Linking an existing account to a provider goes through /link-request and /link-verify, where an emailed token proves the address.
type OAuthBeginResult ¶ added in v0.2.0
OAuthBeginResult is what the route redirects to.
type OAuthCompleteInput ¶ added in v0.2.0
OAuthCompleteInput is GET <prefix>/oauth/{provider}/callback.
type OAuthCompleteResult ¶ added in v0.2.0
type OAuthCompleteResult struct {
User User
Tokens AuthTokens
RedirectTo string
}
OAuthCompleteResult carries the issued session and where to send the browser.
type OAuthLinkedAccount ¶
type OAuthLinkedAccount struct {
ID string
UserID string
Provider string
ProviderID string
Email string
Name string
Picture string
CreatedAt time.Time
}
OAuthLinkedAccount stores a linked provider<->user association.
Email, Name and Picture are the reference's optional LinkedAccount profile columns (linked-accounts-store.interface.ts:9-14). GET /linked-accounts projects all three, and clients render them: the family admin UI falls back through `a.name || a.email || a.providerAccountId`. The library fills Email at every link it writes, as the reference's linkAccount calls do; Name and Picture are left for a host store that wants to keep them, again as in the reference.
type OAuthPendingMeta ¶
type OAuthPendingMeta struct {
Provider string
RedirectURL string
TenantID string
UserID string // non-empty when linking to existing account
// Email and ProviderAccountID carry the account-linking stash: the address
// a /link-request token was issued for, and the provider account a callback
// conflict parked for it.
Email string
ProviderAccountID string
// ExpiresAt lets a consumer enforce the deadline itself rather than trusting
// the store to have honoured the ttl argument.
ExpiresAt time.Time
}
OAuthPendingMeta is the metadata stored during OAuth flow.
type OAuthProvider ¶
type OAuthProvider struct {
Name string
ClientID string
ClientSecret string
RedirectURL string
AuthURL string
TokenURL string
UserInfoURL string
Scopes []string
}
OAuthProvider holds configuration for a single OAuth 2.0 provider.
func GitHubProvider ¶
func GitHubProvider(clientID, clientSecret, redirectURL string) OAuthProvider
GitHubProvider builds a pre-configured GitHub OAuth2 provider.
func GoogleProvider ¶
func GoogleProvider(clientID, clientSecret, redirectURL string) OAuthProvider
GoogleProvider builds a pre-configured Google OAuth2 provider.
type OAuthService ¶
type OAuthService struct {
// contains filtered or unexported fields
}
OAuthService handles OAuth 2.0 authorization code flow.
func NewOAuthService ¶
func NewOAuthService(providers ...OAuthProvider) *OAuthService
NewOAuthService creates an OAuthService for the given providers.
func (*OAuthService) AuthorizeURL ¶
func (s *OAuthService) AuthorizeURL(providerName, state string) (string, error)
AuthorizeURL builds the redirect URL for the given provider and state token.
func (*OAuthService) AuthorizeURLPKCE ¶ added in v0.2.0
func (s *OAuthService) AuthorizeURLPKCE(providerName, state, codeChallenge string) (string, error)
AuthorizeURLPKCE builds the redirect URL and, when codeChallenge is non-empty, binds the exchange to it with the S256 method (RFC 7636). The HTTP routes always pass a challenge; the plain AuthorizeURL stays for embedders driving the service themselves.
func (*OAuthService) ExchangeCode ¶
func (s *OAuthService) ExchangeCode(ctx context.Context, providerName, code string) (OAuthUserInfo, error)
ExchangeCode exchanges an authorization code for user info.
func (*OAuthService) ExchangeCodePKCE ¶ added in v0.2.0
func (s *OAuthService) ExchangeCodePKCE(ctx context.Context, providerName, code, codeVerifier string) (OAuthUserInfo, error)
ExchangeCodePKCE exchanges an authorization code, sending the PKCE verifier when one was used to build the authorization URL.
func (*OAuthService) HandleCallback ¶
func (s *OAuthService) HandleCallback( ctx context.Context, authSvc *Service, linkedAccounts LinkedAccountStore, info OAuthUserInfo, tenantID string, linkToUserID string, ) (User, AuthTokens, error)
HandleCallback resolves/creates a user after OAuth callback. If linkToUserID is non-empty the provider is linked to that existing account.
func (*OAuthService) Has ¶ added in v0.2.0
func (s *OAuthService) Has(name string) bool
Has reports whether a provider is registered.
type OAuthUserInfo ¶
type OAuthUserInfo struct {
ProviderID string
Provider string
Email string
Name string
AvatarURL string
Raw map[string]any
}
OAuthUserInfo is the normalized profile returned after token exchange.
type OAuthWiring ¶ added in v0.2.0
type OAuthWiring struct {
// Service resolves providers, builds authorization URLs and exchanges codes.
Service *OAuthService
// LinkedAccounts persists the provider ↔ user associations the
// /linked-accounts routes read and write.
LinkedAccounts LinkedAccountStore
// PendingLinks is the stash. It carries two kinds of entry, both keyed by an
// opaque string and both single-use: the in-flight OAuth state (so a nonce
// cannot be replayed) and the account-link token issued by /link-request.
PendingLinks PendingLinkStore
// AllowedOrigins is the redirect allowlist. An empty allowlist accepts the
// origin embedded in the state, matching the reference — safe here only
// because the state is signed.
AllowedOrigins []string
// SiteURL is the fallback redirect target, the reference's first
// config.email.siteUrl entry.
SiteURL string
// TenantID scopes users created or resolved by the callback.
TenantID string
// StateTTL bounds how long a signed state stays acceptable. Default 10m.
StateTTL time.Duration
// LinkTokenTTL bounds the /link-request token. Default 1h, as the reference.
LinkTokenTTL time.Duration
// DeliverLinkToken hands the /link-request verification link to the host's
// mail transport. The route answers success either way, as the reference
// does when no transport is configured.
DeliverLinkToken func(ctx context.Context, delivery LinkTokenDelivery) error
}
OAuthWiring is everything the OAuth and account-linking routes need beyond the core service. The reference gates each route on the presence of the corresponding option; the equivalent here is that an absent member turns its routes into NOT_IMPLEMENTED rather than leaving them unmounted, because a single dynamic /oauth/{provider} route cannot be registered per provider.
type OpenAPIInfo ¶
type OpenAPIInfo struct {
Title string
Description string
Version string
ServerURL string
// APIPrefix is where the routes are mounted. Empty means DefaultAPIPrefix,
// and it must match the HTTPConfig the adapter was mounted with, otherwise
// the spec documents paths the server does not serve.
APIPrefix string
}
OpenAPIInfo holds metadata for the generated OpenAPI spec.
type Option ¶
type Option func(*authBuilder) error
Option configures Auth initialization.
func WithBcryptCost ¶ added in v0.2.0
WithBcryptCost sets the bcrypt cost used for every password hash the library writes. See Config.BcryptCost.
Unlike the zero value of Config.BcryptCost, which means "unset" and resolves to bcrypt.DefaultCost, calling this Option with 0 is an error: an explicit setter reached with an unset value is a caller mistake, not a request for the default. Omit the Option to get the default.
func WithEmailChangeSender ¶ added in v0.3.0
func WithEmailChangeSender(sender EmailChangeSender) Option
WithEmailChangeSender wires delivery for POST <prefix>/change-email/request. The message goes to the *new* address: it is a verification of the new mailbox. Without it the route mails nothing and still answers 200.
Pass a callback, or EmailChangeMailer.Send for the built-in email_change template.
func WithEmailVerificationSender ¶ added in v0.3.0
func WithEmailVerificationSender(sender EmailVerificationSender) Option
WithEmailVerificationSender wires verification delivery for POST <prefix>/send-verification-email. Without it the route mails nothing and still answers 200.
Pass a callback, or EmailVerificationMailer.Send for the built-in verify_email template.
func WithLogger ¶
WithLogger provides optional library logging callback.
func WithMagicLinkSender ¶ added in v0.3.0
func WithMagicLinkSender(sender MagicLinkSender) Option
WithMagicLinkSender wires magic-link delivery. Without it POST <prefix>/magic-link/send answers 500 EMAIL_NOT_CONFIGURED, because the route cannot put the link in its response body.
Pass a callback to send the mail yourself, or MagicLinkMailer.Send to use the built-in templates over a MailerTransport.
func WithMetadataProvider ¶
func WithMetadataProvider(store UserMetadataStore) Option
WithMetadataProvider wires optional metadata support.
func WithOAuth ¶ added in v0.2.0
func WithOAuth(wiring OAuthWiring) Option
WithOAuth wires the OAuth service and the linking stores.
func WithPasswordResetSender ¶ added in v0.3.0
func WithPasswordResetSender(sender PasswordResetSender) Option
WithPasswordResetSender wires password-reset delivery for POST <prefix>/forgot-password. Without it the route still answers 200 {"success":true} and mails nothing, which is what a reference deployment with no email block does — see delivery_password_email.go.
Pass a callback to send the mail yourself, or PasswordResetMailer.Send to use the built-in reset_password template over a MailerTransport.
func WithRBACProvider ¶
func WithRBACProvider(store RolesPermissionsStore) Option
WithRBACProvider wires optional role/permission support.
func WithRequire2FA ¶
WithRequire2FA enables mandatory second-factor login checks.
func WithSMSCodeSender ¶ added in v0.3.0
func WithSMSCodeSender(sender SMSCodeSender) Option
WithSMSCodeSender wires SMS delivery. Without it POST <prefix>/sms/send answers 500 SMS_NOT_CONFIGURED.
Pass a callback to send the message yourself, or SMSTransportSender over an SMSTransport such as HTTPSMSTransport.
func WithSecret ¶
WithSecret configures the JWT signing secret.
func WithSessionCheckOn ¶ added in v0.2.0
WithSessionCheckOn selects when a session's revocation state is re-checked: allcalls, refresh (the default) or none. Without allcalls the auth middleware cannot report a revoked session, so a client that relies on the SESSION_REVOKED signal has to opt in here.
func WithSessionStore ¶
func WithSessionStore(store SessionStore) Option
WithSessionStore sets a custom SessionStore implementation.
func WithTenantProvider ¶
func WithTenantProvider(store TenantStore) Option
WithTenantProvider wires optional tenant support.
func WithTokenClaimsBuilder ¶
WithTokenClaimsBuilder customizes token claims creation.
func WithTokenTTLs ¶
WithTokenTTLs configures access and refresh TTL.
func WithUserStore ¶
WithUserStore sets a custom UserStore implementation.
type PasswordResetDelivery ¶ added in v0.3.0
type PasswordResetDelivery struct {
UserID string
TenantID string
// Email is the stored address of the account the reset was requested for,
// not the address as the request spelled it. An unknown address never
// reaches a sender at all: the route is silent about who is registered.
Email string
// Token is the plaintext token POST <prefix>/reset-password accepts.
Token string
// ExpiresAt is the stored expiry, Config.ResetTokenTTL from now (1 hour, as
// in the reference). Verification allows Config.ClockSkew past it, so a
// message quoting this value understates the window rather than overstating
// it.
ExpiresAt time.Time
}
PasswordResetDelivery is what a PasswordResetSender is handed.
Like MagicLinkDelivery it carries the plaintext token — the store holds only its hash — plus the address the mail goes to and the ids a sender needs to read anything else back with. No User, no password hash: a sender is host code that may log what it receives.
type PasswordResetMailer ¶ added in v0.3.0
type PasswordResetMailer struct{ TokenMailer }
PasswordResetMailer is the ready-made PasswordResetSender: it renders the built-in reset_password template and hands the result to a MailerTransport.
Use it as a sender through its Send method:
auth.WithPasswordResetSender(auth.NewPasswordResetMailer( auth.NewHTTPMailerTransport(endpoint, secret), "Example App", "https://app.example.com/auth", ).Send)
func NewPasswordResetMailer ¶ added in v0.3.0
func NewPasswordResetMailer(transport MailerTransport, appName, baseURL string) *PasswordResetMailer
NewPasswordResetMailer builds a PasswordResetMailer with the built-in templates. appName is the name those templates greet the recipient with.
func (*PasswordResetMailer) Send ¶ added in v0.3.0
func (m *PasswordResetMailer) Send(ctx context.Context, delivery PasswordResetDelivery) error
Send renders and delivers the reset mail. Its signature is PasswordResetSender's.
type PasswordResetSender ¶ added in v0.3.0
type PasswordResetSender func(ctx context.Context, delivery PasswordResetDelivery) error
PasswordResetSender delivers a password-reset token.
An error does NOT fail POST <prefix>/forgot-password: that route answers 200 {"success":true} whatever happens. See Auth.ForgotPassword.
type PendingLinkStore ¶
type PendingLinkStore interface {
Save(ctx context.Context, state string, meta OAuthPendingMeta, ttl time.Duration) error
Get(ctx context.Context, state string) (OAuthPendingMeta, error)
Delete(ctx context.Context, state string) error
}
PendingLinkStore holds temporary OAuth link state.
type PublicLinkedAccount ¶ added in v0.2.0
type PublicLinkedAccount struct {
Provider string `json:"provider"`
ProviderAccountID string `json:"providerAccountId"`
Email string `json:"email,omitempty"`
Name string `json:"name,omitempty"`
Picture string `json:"picture,omitempty"`
LinkedAt time.Time `json:"linkedAt"`
}
PublicLinkedAccount is the wire projection of a linked account. The field names and their optionality are the reference's LinkedAccount interface (linked-accounts-store.interface.ts:4-17), which is what the Angular client, the served auth.js and the family admin UI read.
Email is populated by every link the library writes, because the reference's linkAccount calls always pass `email: user.email` from the callback profile (auth.router.ts:1336-1343, :1578-1583). Name and Picture are optional in the reference's interface and its own linkAccount calls never set them, so they only ever appear when a host-supplied LinkedAccountStore fills them in; the admin UI's `a.name || a.email || a.providerAccountId` therefore falls through to the email, as it does against the reference.
type PublicSession ¶ added in v0.2.0
type PublicSession struct {
SessionHandle string `json:"sessionHandle"`
UserID string `json:"userId"`
TenantID string `json:"tenantId,omitempty"`
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
}
PublicSession is the response-safe projection of Session, and the only session shape an adapter may serialise: Session carries the refresh-token hash, which is credential material and must never reach a response body.
The field names and the key set are the reference's SessionInfo (session.model.ts:8-27, wire-contract §3.9): sessionHandle, userId, createdAt, expiresAt and an optional tenantId. Both browser clients read sessionHandle off each entry and post it straight back to DELETE /sessions/{handle}.
There is deliberately no revokedAt key. SessionInfo has no such field, so no shipped client knows it exists or could filter on it, and the reference never needs one: revokeSession deletes the record, so a revoked session is absent from the next list rather than flagged in it. Emitting the tombstone as an extra key would be an unrequested addition to the contract and, worse, the only way a client could tell a live session from a dead one — see Auth.ListSessions, which drops them instead.
func NewPublicSession ¶ added in v0.2.0
func NewPublicSession(session Session) PublicSession
NewPublicSession projects a Session onto its response-safe representation.
func NewPublicSessions ¶ added in v0.2.0
func NewPublicSessions(sessions []Session) []PublicSession
NewPublicSessions projects a session list. The result is never nil: a store that returns no sessions must serialise as [] and not as null, because a client iterating response.sessions breaks on null.
type PublicTenant ¶
type PublicTenant struct {
ID string `json:"id"`
Name string `json:"name"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
}
PublicTenant is the response-safe projection of Tenant. The tenant Config blob is server-side configuration and is never serialised.
type PublicUser ¶
type PublicUser struct {
ID string `json:"id"`
Email string `json:"email"`
TenantID string `json:"tenantId,omitempty"`
FirstName string `json:"firstName,omitempty"`
LastName string `json:"lastName,omitempty"`
PhoneNumber string `json:"phoneNumber,omitempty"`
IsEmailVerified bool `json:"isEmailVerified"`
IsTOTPEnabled bool `json:"isTotpEnabled"`
Roles []string `json:"roles,omitempty"`
Permissions []string `json:"permissions,omitempty"`
Tenants []PublicTenant `json:"tenants,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
CustomClaims map[string]any `json:"customClaims,omitempty"`
CreatedAt time.Time `json:"createdAt"`
}
PublicUser is the response-safe projection of User. It is the only user shape adapters may serialise: credential material (password hash, TOTP secret, one-time token hashes and the pending email change) is deliberately absent.
func NewPublicUser ¶
func NewPublicUser(user User) PublicUser
NewPublicUser projects a User onto its response-safe representation.
type RegisterInput ¶
RegisterInput is the payload to create a user and initial session.
type ResetPasswordInput ¶
type RolesPermissionsStore ¶
type RolesPermissionsStore interface {
AddRoleToUser(ctx context.Context, userID, role string, tenantID string) error
RemoveRoleFromUser(ctx context.Context, userID, role string, tenantID string) error
GetRolesForUser(ctx context.Context, userID, tenantID string) ([]string, error)
CreateRole(ctx context.Context, role string, permissions []string) error
DeleteRole(ctx context.Context, role string) error
AddPermissionToRole(ctx context.Context, role, permission string) error
RemovePermissionFromRole(ctx context.Context, role, permission string) error
GetPermissionsForRole(ctx context.Context, role string) ([]string, error)
GetPermissionsForUser(ctx context.Context, userID, tenantID string) ([]string, error)
UserHasPermission(ctx context.Context, userID, permission, tenantID string) (bool, error)
}
RolesPermissionsStore manages RBAC assignments and permissions.
type SMSCodeDelivery ¶ added in v0.3.0
type SMSCodeDelivery struct {
UserID string
TenantID string
// Phone is the recipient's stored number. The send route refuses a user
// without one before it gets this far (PHONE_NOT_SET), so it is never empty.
Phone string
// Code is the plaintext one-time code; the store holds only its hash.
Code string
ExpiresAt time.Time
}
SMSCodeDelivery is what an SMSCodeSender is handed. See MagicLinkDelivery for why it carries so little.
type SMSCodeSendInput ¶
type SMSCodeSender ¶ added in v0.3.0
type SMSCodeSender func(ctx context.Context, delivery SMSCodeDelivery) error
SMSCodeSender delivers an SMS one-time code. See MagicLinkSender for the failure contract.
func SMSTransportSender ¶ added in v0.3.0
func SMSTransportSender(transport SMSTransport) SMSCodeSender
SMSTransportSender adapts an SMSTransport into an SMSCodeSender, formatting the code with SMSCodeMessage. It is the SMS counterpart of MagicLinkMailer.
type SMSCodeVerifyInput ¶
type SMSStore ¶
type SMSStore interface {
UpdateSMSCode(ctx context.Context, userID, tenantID, codeHash string, expiry time.Time) error
GetUserBySMSCodeHash(ctx context.Context, userID, tenantID, codeHash string) (User, error)
ClearSMSCode(ctx context.Context, userID, tenantID string) error
}
SMSStore persists sms code state.
type SMSTransport ¶ added in v0.3.0
SMSTransport delivers a text message to a handset. It is the SMS counterpart of MailerTransport: SMSTransportSender turns one into the SMSCodeSender that Config.SendSMSCode wants.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the main entry point for authentication operations.
func NewService ¶
func NewService(cfg Config, users UserStore, sessions SessionStore, opts ...ServiceOption) (*Service, error)
NewService builds an auth service with validated configuration.
func (*Service) AddUserToTenant ¶
func (*Service) AssignRole ¶
func (*Service) ChangePassword ¶
func (s *Service) ChangePassword(ctx context.Context, in ChangePasswordInput) error
func (*Service) CleanupExpiredSessions ¶
func (*Service) ConfirmEmailChange ¶
func (s *Service) ConfirmEmailChange(ctx context.Context, in ConfirmEmailChangeInput) error
func (*Service) CreateRole ¶
func (*Service) CreateTenant ¶
func (*Service) DeleteAccount ¶
func (s *Service) DeleteAccount(ctx context.Context, in DeleteAccountInput) error
DeleteAccount runs the reference's account cleanup in the reference's order (auth.router.ts:1597-1636): sessions, then role assignments, then tenant memberships, then metadata, and the user record last. The order is not cosmetic — deleting the identity first and failing anywhere afterwards leaves sessions that still authenticate a user who no longer exists.
func (*Service) DisableTOTP ¶
func (*Service) FindUser ¶ added in v0.2.0
FindUser resolves a user by id, or by email when no id is given. The passwordless routes need it because they have to tell "no such user" apart from "that user has no phone number" — two different statuses in the contract — and the send calls answer both with silence.
func (*Service) ForgotPassword ¶
func (*Service) GetMetadata ¶
func (*Service) IssueTempToken ¶ added in v0.2.0
IssueTempToken mints the step-up token a 2FA challenge hands the client. Its lifetime is Config.TempTokenTTL, 5 minutes by default, matching the reference's hardcoded '5m'.
func (*Service) IssueTokensForUser ¶ added in v0.2.0
func (s *Service) IssueTokensForUser(ctx context.Context, userID, tenantID string) (User, AuthTokens, error)
IssueTokensForUser starts a session for an identity that has already been proven by something other than a password — here, an emailed account-link token. It is the one service method this route group needed and the core did not have: every other issuance path (Register, Login, VerifyMagicLink, VerifySMSCode, VerifyTOTP) reaches newSessionTokens through a credential check that does not apply.
func (*Service) ListSessions ¶
func (*Service) Login ¶
func (s *Service) Login(ctx context.Context, in LoginInput) (User, AuthTokens, error)
Login verifies a password and issues a session, reporting a 2FA-gated account as ErrTwoFactorRequired.
It is the narrow form of LoginWithChallenge, which is what the HTTP adapters call: the sentinel can say that a second factor is needed and nothing more, and the step-up routes need the token that comes with the challenge. Kept as it was for direct callers. See login_2fa.go.
It shares the password path with LoginWithChallenge but stops at the decision rather than at the built challenge, so the sentinel is reported whenever a second factor is *required* and not merely whenever one could be minted. Going through the challenge would make this wrapper fail wherever minting fails — Config.BuildTokenClaims returning an error is enough (token.go:66-71) — and answer a 2FA account with that error instead of ErrTwoFactorRequired. Nothing is minted here: the token this call cannot return is never built.
func (*Service) LoginWithChallenge ¶ added in v0.3.0
func (s *Service) LoginWithChallenge(ctx context.Context, in LoginInput) (LoginResult, error)
LoginWithChallenge is the full POST /login path: it verifies the password and then either issues a session or mints the step-up challenge.
Service.Login is the narrower form of the same call, kept for direct callers: it collapses a challenge back into ErrTwoFactorRequired, which says a second factor is needed and cannot say anything more.
A challenge that cannot be minted is returned as the error, not as a challenge with an empty TempToken and not as ErrTwoFactorRequired. This call's whole point is the token: a client handed a challenge without one would believe it had a step-up credential and be refused by every §3 route, and the sentinel would spell the 403 2FA_REQUIRED body this PR removed from /login — a wire answer no longer served on this route, telling the client to step up with a token it was never given. So the failure surfaces: HTTPErrorFor maps it to HTTPErrInternal (wire.go:172-173), which is what a broken host hook is.
func (*Service) LogoutAccessToken ¶ added in v0.2.0
LogoutAccessToken revokes the session named by an access token's sid claim.
func (*Service) Register ¶
func (s *Service) Register(ctx context.Context, in RegisterInput) (User, AuthTokens, error)
func (*Service) RequestEmailChange ¶
func (*Service) ResetPassword ¶
func (s *Service) ResetPassword(ctx context.Context, in ResetPasswordInput) error
func (*Service) RevokeSessionByID ¶
func (*Service) RevokeUserSession ¶ added in v0.2.0
RevokeUserSession revokes one of a user's own sessions. Ownership is part of the lookup rather than a check bolted on afterwards: a handle belonging to another user is indistinguishable from one that never existed.
A handle that resolves to a session which is already revoked, or which has expired, is also indistinguishable from one that never existed. The reference resolves the handle with getSession, documented to return null "when the session does not exist or has expired" (session-store.interface.ts:51-54), and its revokeSession deletes the record — so both cases reach the same `if (!session || …) → 404` as an unknown handle (auth.router.ts:762-767). A second DELETE of the same handle is therefore *not* idempotently successful, and neither is a DELETE of an expired one.
func (*Service) SendMagicLink ¶
SendMagicLink mints a magic link, stores its hash and delivers it through Config.SendMagicLink.
The returned plaintext token is redundant for an HTTP caller — the routes answer {"success":true} and drop it — and is kept for a direct caller that delivers the link itself, and for tests.
func (*Service) SendSMSCode ¶
SendSMSCode mints a one-time code, stores its hash and delivers it through Config.SendSMSCode. See SendMagicLink on the returned plaintext.
The HTTP routes refuse an unconfigured deployment before they reach this call (Auth.SMSConfigured), because the reference checks config.sms at the top of the route rather than in the strategy. This check is what protects a direct caller, and it is why a handler that skipped the precheck still cannot store a code nobody can deliver.
func (*Service) SendVerificationEmailToken ¶
func (*Service) StartTOTPEnrolment ¶ added in v0.2.0
func (s *Service) StartTOTPEnrolment(ctx context.Context, userID, tenantID string) (TOTPSetup, error)
StartTOTPEnrolment mints an enrolment secret and its provisioning URI.
func (*Service) TokenTTLs ¶ added in v0.2.0
TokenTTLs reports the configured access and refresh token lifetimes. The HTTP layer needs them to give the auth cookies the same lifetime as the tokens they carry.
func (*Service) UpdateMetadata ¶
func (*Service) UpdatePhoneNumber ¶ added in v0.2.0
UpdatePhoneNumber sets or clears a user's phone number.
func (*Service) UpdateProfile ¶
UpdateProfile applies a partial profile update: a nil field in the input is one the caller did not submit and must be left as it is stored (§3.5).
UserAccountStore.UpdateProfile takes both names by value, so a nil field is resolved against the current record before the write rather than by widening the store interface — a store written against this package keeps compiling and still gets partial semantics. The read-modify-write is not atomic; two concurrent partial patches of *different* fields can still lose one. Making it atomic needs a store-level patch primitive, which is a breaking interface change and its own item; silently erasing a field on every single partial call is the failure this closes.
func (*Service) UserHasPermission ¶
func (*Service) VerifyEmail ¶
func (s *Service) VerifyEmail(ctx context.Context, in VerifyEmailInput) error
func (*Service) VerifyMagicLink ¶
func (s *Service) VerifyMagicLink(ctx context.Context, in MagicLinkVerifyInput) (User, AuthTokens, error)
func (*Service) VerifyMagicLinkForUser ¶ added in v0.2.0
func (s *Service) VerifyMagicLinkForUser(ctx context.Context, in MagicLinkVerifyInput, userID string) (User, AuthTokens, error)
VerifyMagicLinkForUser consumes a magic link that must belong to userID, the step-up case: the caller has already identified the user through a step-up token and the link is only the second factor.
It exists because the identity check has to happen before a session is created. Service.VerifyMagicLink issues one as part of verifying, so a caller that compared the ids afterwards would refuse the request on the wire and still leave a live session for the link's owner — a session that shows up in GET /sessions and that only the owner can revoke. A mismatch here returns ErrMagicLinkOwnerMismatch and issues nothing.
The link is consumed even on a mismatch, matching the reference, which burns it in the strategy before the router compares ids (magic-link.strategy.ts:50 versus auth.router.ts:1150-1152).
func (*Service) VerifySMSCode ¶
func (s *Service) VerifySMSCode(ctx context.Context, in SMSCodeVerifyInput) (User, AuthTokens, error)
func (*Service) VerifyTOTP ¶
func (*Service) VerifyTOTPSetup ¶
func (*Service) VerifyTempToken ¶ added in v0.2.0
func (s *Service) VerifyTempToken(token string) (TempTokenSubject, error)
VerifyTempToken resolves the identity a step-up token names, or ErrInvalidToken.
type ServiceOption ¶
type ServiceOption func(*Service)
func WithMetadataStore ¶
func WithMetadataStore(store UserMetadataStore) ServiceOption
func WithRolesPermissionsStore ¶
func WithRolesPermissionsStore(store RolesPermissionsStore) ServiceOption
func WithTenantStore ¶
func WithTenantStore(store TenantStore) ServiceOption
type Session ¶
type Session struct {
ID string
UserID string
TenantID string
RefreshTokenHash string
CreatedAt time.Time
ExpiresAt time.Time
RevokedAt *time.Time
}
Session stores refresh-token bound session metadata.
type SessionAdminStore ¶
type SessionAdminStore interface {
ListSessionsForUser(ctx context.Context, userID, tenantID string) ([]Session, error)
RevokeSessionByID(ctx context.Context, sessionID string) error
DeleteExpiredSessions(ctx context.Context, now time.Time) (int, error)
}
SessionAdminStore exposes session listing and cleanup primitives.
type SessionLookupStore ¶
type SessionLookupStore interface {
GetSessionByID(ctx context.Context, sessionID string) (Session, error)
}
SessionLookupStore resolves sessions by session ID.
type SessionStore ¶
type SessionStore interface {
CreateSession(ctx context.Context, session Session) (Session, error)
GetSessionByRefreshTokenHash(ctx context.Context, tokenHash string) (Session, error)
UpdateSession(ctx context.Context, session Session) error
}
SessionStore persists refresh-token sessions.
type SseDistributor ¶
type SseDistributor interface {
Publish(ctx context.Context, channel string, msg SseMessage) error
Subscribe(ctx context.Context, channel string) (<-chan SseMessage, error)
}
SseDistributor broadcasts SSE messages to all connected clients. Implement this interface for Redis/Kafka-based multi-instance scaling.
type SseHub ¶
type SseHub struct {
// contains filtered or unexported fields
}
SseHub manages in-process SSE subscriptions.
func (*SseHub) Unsubscribe ¶
func (h *SseHub) Unsubscribe(channel string, ch <-chan SseMessage)
Unsubscribe removes a client channel from the hub.
type SseMessage ¶
SseMessage is a single SSE payload.
type TOTPSetup ¶ added in v0.2.0
TOTPSetup is the enrolment material POST /2fa/setup hands back. Enrolment is stateless in the reference and stateless here: nothing is persisted until the client returns the secret to /2fa/verify-setup together with a code that verifies against it.
The reference also returns a `qrCode` PNG data URL. It is absent here: the root package is stdlib plus golang.org/x/crypto and a QR encoder is neither. A client renders OTPAuthURL itself, which is what the Rust port of this family does too.
type TOTPStore ¶
type TOTPStore interface {
UpdateTOTPSecret(ctx context.Context, userID, tenantID, secret string, enabled bool) error
}
TOTPStore persists authenticator setup.
type TelemetryEvent ¶
type TelemetryEvent struct {
ID string
EventName string
UserID string
TenantID string
IP string
UserAgent string
Success bool
Error string
Timestamp time.Time
Meta map[string]any
}
TelemetryEvent represents a single persisted auth event for analytics.
type TelemetryFilter ¶
type TelemetryFilter struct {
UserID string
TenantID string
EventName string
Since time.Time
Until time.Time
Limit int
}
TelemetryFilter restricts query results.
type TelemetryStore ¶
type TelemetryStore interface {
Record(ctx context.Context, event TelemetryEvent) error
Query(ctx context.Context, filter TelemetryFilter) ([]TelemetryEvent, error)
}
TelemetryStore persists auth telemetry events.
type TempTokenSubject ¶ added in v0.2.0
TempTokenSubject is the identity a step-up token names. It is deliberately not a User: the routes that accept a tempToken re-read the user from the store, as the reference does, so a flag set since the token was minted is honoured.
type Tenant ¶
type Tenant struct {
ID string
Name string
IsActive bool
Config map[string]any
CreatedAt time.Time
}
Tenant represents an isolated workspace or organization.
type TenantStore ¶
type TenantStore interface {
CreateTenant(ctx context.Context, tenant Tenant) (Tenant, error)
GetTenantByID(ctx context.Context, id string) (Tenant, error)
GetAllTenants(ctx context.Context) ([]Tenant, error)
UpdateTenant(ctx context.Context, id string, update Tenant) error
DeleteTenant(ctx context.Context, id string) error
AssociateUserWithTenant(ctx context.Context, userID, tenantID string) error
DisassociateUserFromTenant(ctx context.Context, userID, tenantID string) error
GetTenantsForUser(ctx context.Context, userID string) ([]Tenant, error)
GetUsersForTenant(ctx context.Context, tenantID string) ([]string, error)
}
TenantStore manages tenants and user memberships.
type TokenMailer ¶ added in v0.3.0
type TokenMailer struct {
// Transport delivers the rendered message. Required.
Transport MailerTransport
// BaseURL is what the URL builders above build the link under.
BaseURL string
// Locale selects the built-in template set: "en" (the default) or "it". An
// unknown locale falls back to English, as MailTemplater.Render does.
//
// It is static for the same reason MagicLinkMailer.Locale is: the reference's
// per-request override is the emailLang body field, which these routes accept
// on the wire and no adapter threads any further yet.
Locale string
// Templates renders subject and body. The constructors fill this in.
Templates *MailTemplater
}
TokenMailer is the shared body of the three ready-made senders below and the port's equivalent of the reference's config.email.mailer path, where a deployment configures a transport instead of writing a callback. It is MagicLinkMailer's shape, factored out because three copies of it would be three places for a template name or a Content-Type to drift.
type TwoFactorChallenge ¶ added in v0.3.0
type TwoFactorChallenge struct {
// TempToken is the step-up credential the §3 routes accept. It is typed
// tokenTypeTemp, so unlike the reference's it is not also an access token —
// see the tokenTypeTemp comment for why that deliberate difference is kept.
TempToken string
// Methods is available2faMethods: the factors this user can use, in the
// reference's order. It is empty only when SetupRequired is set.
Methods []string
// SetupRequired selects the second of the reference's two branches: a policy
// demands a second factor and the account has no method at all, so the client
// must enrol one before it can be challenged.
SetupRequired bool
}
TwoFactorChallenge is what POST /login answers a user who has a second factor: the step-up token, and the factors they can actually present it with.
It carries what goes on the wire and nothing else, for the reason MagicLinkDelivery carries so little (delivery.go): the token is a credential, and a value that also held the User would hand a password hash and a TOTP secret to anything that logs a challenge.
type UpdateProfileInput ¶
UpdateProfileInput is the payload behind PATCH /profile. FirstName and LastName are pointers because the route is a *partial* update: the reference types the body { firstName?: string | null, lastName?: string | null } and passes it through verbatim, so a key the caller omitted arrives at the store as undefined and leaves the stored column alone (wire-contract §3.5, user-store.interface.ts:136).
nil therefore means "not submitted, leave as it is" and a pointer to "" means "clear it" — the distinction a plain string cannot carry, and without which sending only firstName silently erases the stored lastName.
type User ¶
type User struct {
ID string
Email string
PasswordHash string
TenantID string
PhoneNumber string
FirstName string
LastName string
Role string
IsEmailVerified bool
Require2FA bool
IsTOTPEnabled bool
TOTPSecret string
ResetTokenHash string
ResetTokenExpiresAt *time.Time
MagicLinkTokenHash string
MagicLinkTokenExpiresAt *time.Time
SMSCodeHash string
SMSCodeExpiresAt *time.Time
EmailVerificationTokenHash string
EmailVerificationTokenExpiry *time.Time
PendingEmail string
EmailChangeTokenHash string
EmailChangeTokenExpiry *time.Time
Metadata map[string]any
Roles []string
Permissions []string
Tenants []Tenant
CustomClaims map[string]any
CreatedAt time.Time
UpdatedAt time.Time
}
User is the canonical identity model.
type UserAccountStore ¶
type UserAccountStore interface {
UpdateProfile(ctx context.Context, userID, tenantID, firstName, lastName string) (User, error)
DeleteUser(ctx context.Context, userID, tenantID string) error
}
UserAccountStore adds account lifecycle operations.
type UserMetadataStore ¶
type UserMetadataStore interface {
GetMetadata(ctx context.Context, userID string) (map[string]any, error)
UpdateMetadata(ctx context.Context, userID string, metadata map[string]any) error
ClearMetadata(ctx context.Context, userID string) error
}
UserMetadataStore manages arbitrary key/value user metadata.
type UserPasswordStore ¶
type UserPasswordStore interface {
UpdatePassword(ctx context.Context, userID, tenantID, passwordHash string) error
UpdateResetToken(ctx context.Context, userID, tenantID, tokenHash string, expiry time.Time) error
GetUserByResetTokenHash(ctx context.Context, tokenHash string) (User, error)
ClearResetToken(ctx context.Context, userID, tenantID string) error
}
UserPasswordStore adds password lifecycle methods.
type UserPhoneStore ¶ added in v0.2.0
type UserPhoneStore interface {
UpdatePhoneNumber(ctx context.Context, userID, tenantID, phoneNumber string) (User, error)
}
UserPhoneStore adds the phone-number update behind POST /add-phone. It is a capability of its own rather than a method on UserAccountStore so that a store written against the existing interface keeps compiling; a store that does not implement it makes the route answer 501, which is the reference's own behaviour when userStore.updatePhoneNumber is absent (auth.router.ts:700-703).
type UserStore ¶
type UserStore interface {
CreateUser(ctx context.Context, user User) (User, error)
GetUserByEmail(ctx context.Context, email, tenantID string) (User, error)
GetUserByID(ctx context.Context, id, tenantID string) (User, error)
}
UserStore persists and resolves users.
type VerifyEmailInput ¶
type VerifyEmailInput struct {
Token string
}
type WebhookDispatcher ¶
type WebhookDispatcher struct {
// contains filtered or unexported fields
}
WebhookDispatcher delivers signed webhook payloads.
func NewWebhookDispatcher ¶
func NewWebhookDispatcher(endpoints ...WebhookEndpoint) *WebhookDispatcher
NewWebhookDispatcher creates a dispatcher for the given endpoints.
Source Files
¶
- account.go
- api_keys.go
- auth.go
- compatibility.go
- compatibility_markdown.go
- config.go
- csrf.go
- delivery.go
- delivery_password_email.go
- errors.go
- events.go
- feature_stores.go
- idp.go
- login_2fa.go
- mailer.go
- mcp.go
- memory_store.go
- models.go
- oauth.go
- oauth_wire.go
- openapi.go
- passwordless.go
- security.go
- service.go
- sms.go
- sse.go
- store.go
- telemetry.go
- token.go
- totp.go
- ui.go
- webhooks.go
- wire.go
- wire_password_email.go
Directories
¶
| Path | Synopsis |
|---|---|
|
adapter
|
|
|
internal/wiretest
Package wiretest is the shared wire-contract conformance suite for the HTTP adapters.
|
Package wiretest is the shared wire-contract conformance suite for the HTTP adapters. |
|
examples
|
|
|
chi-postgres
command
Chi + PostgreSQL example for awesome-go-auth.
|
Chi + PostgreSQL example for awesome-go-auth. |
|
echo-sqlite
command
Echo + SQLite example for awesome-go-auth.
|
Echo + SQLite example for awesome-go-auth. |
|
gin-mongodb
command
Gin + MongoDB example for awesome-go-auth.
|
Gin + MongoDB example for awesome-go-auth. |