Documentation
¶
Index ¶
- Constants
- Variables
- func APIKeyMiddleware(store APIKeyStore, requiredScopes []string) func(http.Handler) http.Handler
- func AccessTokenFromRequest(r *http.Request) string
- func AdminOpenAPIHandler(info AdminOpenAPIInfo) http.Handler
- func BearerToken(header string) string
- func BuildRS256JWT(signer crypto.Signer, kid string, claims map[string]any) (string, error)
- func CSRFMiddleware(cfg HTTPConfig) func(http.Handler) http.Handler
- func CompileProfileMap(m map[string]string) (func(map[string]any) (OAuthUserInfo, error), error)
- func ContextWithEventContext(ctx context.Context, ec EventContext) context.Context
- func ContextWithUser(ctx context.Context, user User) context.Context
- 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 EventContextMiddleware(cfg HTTPConfig) func(http.Handler) http.Handler
- func EventNames() []string
- func EventTopics(ev Event) []string
- func GenerateAdminOpenAPISpec(info AdminOpenAPIInfo) map[string]any
- func GenerateOpenAPISpec(info OpenAPIInfo) map[string]any
- func GenerateToolsOpenAPISpec(info ToolsOpenAPIInfo) map[string]any
- func IsBearerRequest(r *http.Request) bool
- func IsEventName(name string) bool
- func MagicLinkURL(base, token string) string
- func OAuthNotConfiguredError(provider string) error
- func OpenAPIHandler(info OpenAPIInfo) http.Handler
- func ParseRSAPrivateKeyPEM(pemText string) (*rsa.PrivateKey, error)
- func PasswordResetURL(base, token string) string
- func RateLimitMiddleware(cfg HTTPConfig) func(http.Handler) http.Handler
- func ReadUpstreamUIAsset(name string) ([]byte, error)
- func RefreshTokenFromRequest(r *http.Request) string
- func ResourceServerGatedRoutes() map[string]string
- func ResourceServerMiddleware(a *Auth, cfg ResourceServerConfig) func(http.Handler) http.Handler
- func ResourceServerPrincipal(r *http.Request, a *Auth, client *JWKSClient, issuer string) (User, HTTPError, bool)
- func SMSCodeMessage(code string) string
- func ServeAdminUI() http.Handlerdeprecated
- func ServeAuthJS() http.Handlerdeprecated
- func ServeAuthUI() http.Handlerdeprecated
- func SessionHandleParam(r *http.Request, raw string) string
- func SortUploadedFiles(files []UploadedFile)
- func StreamTopics(userID, tenantID string, requested []string) []string
- func SwaggerUIHandler(specURL string) http.Handler
- func ToolsOpenAPIHandler(info ToolsOpenAPIInfo) http.Handler
- func ToolsProtectMiddleware(cfg HTTPConfig) func(http.Handler) http.Handler
- func ToolsSseTokenMiddleware(next http.Handler) http.Handler
- func TwoFactorMode(mode string) bool
- func UploadContentType(key string) string
- func UploadFS(store UploadStore) fs.FS
- func UploadNameAllowed(name string) bool
- func UpstreamUIAssetFS() fs.FS
- func ValidUploadKey(key string) bool
- func VerifyEmailToken(r *http.Request) string
- func VerifyRS256(ctx context.Context, token string, client *JWKSClient, expectedIssuer string) (map[string]any, error)
- func VerifyWebhookSignature(secret string, body []byte, sigHeader string) error
- func WebhookTimestamp(t time.Time) string
- 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 APIKeyAdminStore
- type APIKeyAuditEntry
- type APIKeyAuditStore
- type APIKeyDeleteStore
- type APIKeyRecord
- type APIKeyService
- type APIKeyServiceIndexStore
- type APIKeyStore
- type AddPhoneInput
- type AdminAccessPolicy
- type AdminGuard
- type AdminOpenAPIInfo
- type AdminOptions
- type AdminPolicyFunc
- type AdminPolicyKind
- type AdminRootUser
- type AdminUserStore
- type Auth
- func (a *Auth) AdminGuard(cfg HTTPConfig) *AdminGuard
- func (a *Auth) AdminHandler(cfg HTTPConfig) http.Handler
- func (a *Auth) AdminOpenAPIInfo(cfg HTTPConfig) AdminOpenAPIInfo
- func (a *Auth) Authenticate(ctx context.Context, accessToken string) (User, error)
- 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) IDP() *IDP
- 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) JWKSHandler() http.Handler
- 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) ResolveSiteURL(r *http.Request) string
- 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) ToolsHandler(cfg HTTPConfig) http.Handler
- func (a *Auth) TwoFactorPolicy(ctx context.Context) (bool, error)
- func (a *Auth) UIConfig(ctx context.Context, r *http.Request, cfg HTTPConfig) UIConfig
- func (a *Auth) UIHandler(cfg HTTPConfig, configRoute http.Handler) http.Handler
- 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 AuthCode
- type AuthCodeStore
- type AuthSettings
- type AuthTokens
- type AuthTools
- type AuthToolsOptions
- type CSRFConfig
- type ChangeEmailRequestInput
- type ChangePasswordInput
- type ClaimsWebhook
- 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 DeliveryWebhook
- func (w *DeliveryWebhook) SendEmailChange(ctx context.Context, delivery EmailChangeDelivery) error
- func (w *DeliveryWebhook) SendEmailVerification(ctx context.Context, delivery EmailVerificationDelivery) error
- func (w *DeliveryWebhook) SendMagicLink(ctx context.Context, delivery MagicLinkDelivery) error
- func (w *DeliveryWebhook) SendPasswordReset(ctx context.Context, delivery PasswordResetDelivery) error
- func (w *DeliveryWebhook) SendSMSCode(ctx context.Context, delivery SMSCodeDelivery) error
- type DeliveryWebhookRequest
- type Deviation
- type DeviationNote
- type DocsOptions
- type EmailChangeDelivery
- type EmailChangeMailer
- type EmailChangeSender
- type EmailChangeStore
- type EmailChangedDelivery
- type EmailChangedMailer
- type EmailChangedSender
- type EmailVerificationDelivery
- type EmailVerificationInput
- type EmailVerificationMailer
- type EmailVerificationSender
- type EmailVerificationStore
- type Event
- type EventBus
- type EventContext
- type ForgotPasswordInput
- type HTTPConfig
- func (c HTTPConfig) AccountConflictLink(siteURL, provider, email string) string
- func (c HTTPConfig) AdminDocsBasePath() string
- func (c HTTPConfig) AdminMounted() bool
- func (c HTTPConfig) AdminPath() string
- func (c HTTPConfig) DocsBasePath() string
- func (c HTTPConfig) LinkBase(siteURL string) string
- func (c HTTPConfig) Prefix() string
- func (c HTTPConfig) ToolsDocsBasePath() string
- func (c HTTPConfig) ToolsMounted() bool
- func (c HTTPConfig) ToolsOpenAPIInfo() ToolsOpenAPIInfo
- func (c HTTPConfig) ToolsPath() string
- func (c HTTPConfig) UILink(siteURL, path string) string
- func (c HTTPConfig) WriteTokens(w http.ResponseWriter, r *http.Request, status int, tokens AuthTokens, ...)
- 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 HTTPMailerTransportdeprecated
- type HTTPSMSTransport
- type HTTPWebhookDeliverer
- type IDP
- func (idp *IDP) IssueIdPTokenPair(ctx context.Context, user User) (AuthTokens, error)
- func (idp *IDP) JWKS() JWKS
- func (idp *IDP) JWKSHandler() http.Handler
- func (idp *IDP) JWKSPath() string
- func (idp *IDP) OIDCMounts() []OIDCMount
- func (idp *IDP) RegisterHandlers(mux *http.ServeMux, basePath string)
- type IDPClient
- type IDPConfig
- type InboundWebhookStore
- type JWK
- type JWKS
- type JWKSClient
- 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 MailRendered
- type MailTemplate
- type MailTemplateData
- type MailTemplatePatch
- type MailTemplater
- func (t *MailTemplater) Register(locale, id, subject, html, text string)
- func (t *MailTemplater) Render(locale, name string, data MailTemplateData) (subject, body string, err error)
- func (t *MailTemplater) RenderMail(ctx context.Context, locale, id string, data MailTemplateData) (MailRendered, error)
- type MailerConfig
- type MailerTransport
- type MemoryAPIKeyStore
- func (s *MemoryAPIKeyStore) Delete(_ context.Context, id string) error
- func (s *MemoryAPIKeyStore) FindByID(_ context.Context, id string) (APIKeyRecord, error)
- func (s *MemoryAPIKeyStore) FindByPrefix(_ context.Context, prefix string) (APIKeyRecord, error)
- func (s *MemoryAPIKeyStore) ListAll(_ context.Context, limit, offset int) ([]APIKeyRecord, error)
- func (s *MemoryAPIKeyStore) ListByServiceID(_ context.Context, serviceID string) ([]APIKeyRecord, error)
- func (s *MemoryAPIKeyStore) LogUsage(_ context.Context, entry APIKeyAuditEntry) error
- func (s *MemoryAPIKeyStore) Revoke(_ context.Context, id string) error
- func (s *MemoryAPIKeyStore) Save(_ context.Context, key APIKeyRecord) error
- func (s *MemoryAPIKeyStore) UpdateLastUsed(_ context.Context, id string, when time.Time) error
- func (s *MemoryAPIKeyStore) Usage() []APIKeyAuditEntry
- type MemoryAuthCodeStore
- 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) GetAllRoles(_ context.Context) ([]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) GetAllSessions(_ context.Context, limit, offset int) ([]Session, 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 MemorySettingsStore
- type MemoryTelemetryStore
- type MemoryTemplateStore
- func (s *MemoryTemplateStore) GetMailTemplate(_ context.Context, id string) (MailTemplate, bool, error)
- func (s *MemoryTemplateStore) GetUITranslations(_ context.Context, page string) (UITranslation, bool, error)
- func (s *MemoryTemplateStore) ListMailTemplates(_ context.Context) ([]MailTemplate, error)
- func (s *MemoryTemplateStore) ListUITranslations(_ context.Context) ([]UITranslation, error)
- func (s *MemoryTemplateStore) UpdateMailTemplate(_ context.Context, id string, patch MailTemplatePatch) (MailTemplate, error)
- func (s *MemoryTemplateStore) UpdateUITranslations(_ context.Context, page string, translations map[string]map[string]string) (UITranslation, error)
- 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 MemoryUploadStore
- func (s *MemoryUploadStore) Delete(_ context.Context, key string) error
- func (s *MemoryUploadStore) List(_ context.Context) ([]UploadedFile, error)
- func (s *MemoryUploadStore) Open(_ context.Context, key string) (io.ReadCloser, UploadedFile, error)
- func (s *MemoryUploadStore) Put(_ context.Context, key string, content io.Reader) (UploadedFile, 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) ListUsers(_ context.Context, tenantID string, limit, offset int) ([]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) UpdateIsAdmin(_ context.Context, userID, tenantID string, isAdmin bool) 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) UpdateRequire2FA(_ context.Context, userID, tenantID string, required bool) 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 MemoryWebhookStore
- func (s *MemoryWebhookStore) AddWebhook(_ context.Context, config WebhookConfig) (WebhookConfig, error)
- func (s *MemoryWebhookStore) FindByEvent(_ context.Context, event, tenantID string) ([]WebhookConfig, error)
- func (s *MemoryWebhookStore) FindByProvider(_ context.Context, provider string) (WebhookConfig, bool, error)
- func (s *MemoryWebhookStore) ListWebhooks(_ context.Context, limit, offset int) ([]WebhookConfig, error)
- func (s *MemoryWebhookStore) RemoveWebhook(_ context.Context, id string) error
- func (s *MemoryWebhookStore) UpdateWebhook(_ context.Context, id string, patch WebhookPatch) error
- type NotifyChannel
- type NotifyOptions
- type OAuthAccountConflictError
- type OAuthBeginInput
- type OAuthBeginResult
- type OAuthCompleteInput
- type OAuthCompleteResult
- type OAuthLinkedAccount
- type OAuthPendingMeta
- type OAuthProvider
- type OAuthProvisioning
- 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) HandleCallbackWithPolicy(ctx context.Context, authSvc *Service, linkedAccounts LinkedAccountStore, ...) (User, AuthTokens, error)
- func (s *OAuthService) Has(name string) bool
- type OAuthUserInfo
- type OAuthWiring
- type OIDCMount
- type OpenAPIInfo
- type Option
- func WithAPIKeyStore(store APIKeyStore) Option
- func WithBcryptCost(cost int) Option
- func WithEmailChangeSender(sender EmailChangeSender) Option
- func WithEmailChangedSender(sender EmailChangedSender) Option
- func WithEmailVerificationSender(sender EmailVerificationSender) Option
- func WithEventBus(bus *EventBus) Option
- func WithIDP(idp *IDP) 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 WithPasswordVerifier(verify PasswordVerifier) 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 WithSettingsStore(store SettingsStore) Option
- func WithSiteURLs(urls ...string) Option
- func WithTemplateStore(store TemplateStore) Option
- func WithTenantProvider(store TenantStore) Option
- func WithTokenClaimsBuilder(fn TokenClaimsBuilder) Option
- func WithTokenTTLs(access, refresh time.Duration) Option
- func WithTwoFactorAppName(name string) Option
- func WithUploadStore(store UploadStore) Option
- func WithUserStore(store UserStore) Option
- func WithWebhookStore(store WebhookStore) Option
- type OutgoingWebhookEvent
- type PasswordResetDelivery
- type PasswordResetMailer
- type PasswordResetSender
- type PasswordVerifier
- type PendingLinkStore
- type PublicLinkedAccount
- type PublicSession
- type PublicTenant
- type PublicUser
- type RegisterInput
- type ResetPasswordInput
- type ResourceServerConfig
- type RoleLister
- 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) Authenticate(ctx context.Context, accessToken string) (User, 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) ListAllRoles(ctx context.Context) ([]string, error)
- func (s *Service) ListAllSessions(ctx context.Context, limit, offset int) ([]Session, error)
- func (s *Service) ListSessions(ctx context.Context, userID, tenantID string) ([]Session, error)
- func (s *Service) ListUsers(ctx context.Context, tenantID string, limit, offset int) ([]User, 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 SessionLister
- type SessionLookupStore
- type SessionStore
- type SettingsStore
- type SseConnectionMeta
- type SseDistributor
- type SseManager
- func (m *SseManager) BridgeEventBus(ctx context.Context, bus *EventBus, topics func(Event) []string) (cancel func())
- func (m *SseManager) Broadcast(ctx context.Context, topic string, ev StreamEvent)
- func (m *SseManager) ConnectionCount() int
- func (m *SseManager) Deduplicate() bool
- func (m *SseManager) Disconnect(connectionID string)
- func (m *SseManager) HeartbeatInterval() time.Duration
- func (m *SseManager) Serve(w http.ResponseWriter, r *http.Request, topics []string, ...) error
- type SseOption
- type StreamEvent
- type TOTPSetup
- type TOTPStore
- type TelemetryEvent
- type TelemetryFilter
- type TelemetryStore
- type TempTokenSubject
- type TemplateStore
- type Tenant
- type TenantStore
- type TokenClaimsBuilder
- type TokenMailer
- type ToolsAccess
- type ToolsFeatures
- type ToolsOpenAPIInfo
- type ToolsOptions
- type TrackOptions
- type TwoFactorChallenge
- type UIBranding
- type UIConfig
- type UIConfigBranding
- type UIFeatures
- type UIOptions
- type UISettings
- type UITranslation
- type UpdateProfileInput
- type UploadStore
- type UploadedFile
- type UpstreamUIAsset
- type User
- type UserAccountStore
- type UserAdminFlagStore
- type UserMetadataStore
- type UserPasswordStore
- type UserPhoneStore
- type UserStore
- type UserTwoFactorPolicyStore
- type VerifyEmailInput
- type WebhookAdminStore
- type WebhookAttempt
- type WebhookConfig
- type WebhookDeliverer
- type WebhookDelivererFunc
- type WebhookEmitter
- type WebhookPatch
- type WebhookSender
- type WebhookStatusError
- type WebhookStore
Constants ¶
const ( // DefaultAdminPath is AdminOptions.Path's default and the reference's // swaggerBasePath default (admin.router.ts:190, openapi.ts:674). DefaultAdminPath = "/admin" AdminLoginPath = "/login" // admin.router.ts:543 AdminLogoutPath = "/logout" // admin.router.ts:618 AdminCSSPath = "/assets/admin.css" // admin.router.ts:690 AdminJSPath = "/assets/admin.js" // admin.router.ts:696 AdminShellPath = "/" // admin.router.ts:738 AdminPingPath = "/api/ping" // admin.router.ts:741 // AdminOpenAPIPath and AdminDocsPath are the console's own documentation // pair (admin.router.ts:1499, :1517). They are the same two names the auth // and tools routers serve, one segment further down — the reference spells // them out there too — and they are the only two routes under /api/* that // the reference registers with no guard. See AdminOptions.Docs. AdminOpenAPIPath = "/api" + DocsSpecPath AdminDocsPath = "/api" + DocsUIPath )
The admin router's own paths, relative to its mount. These six are the whole of U12; the rest of /api/* arrives with the PRs that own those routes.
const ( AdminAPIKeysPath = "/api/api-keys" // admin.router.ts:1253 AdminWebhooksPath = "/api/webhooks" // admin.router.ts:1363 )
The credential routes, relative to the admin mount. Six of the eight are a parameter below one of these two.
const ( // AdminPromoteUsersPath is the collection this route hangs off, and the one // thing to notice about it is the missing /api. See this file's header. AdminPromoteUsersPath = "/users" // AdminPromoteSuffix is the last segment of the pattern: // <admin>/users/{id}/promote. AdminPromoteSuffix = "/promote" )
const ( AdminUsersPath = "/api/users" // admin.router.ts:748 AdminActionsPath = "/api/actions" // admin.router.ts:946 AdminSettingsPath = "/api/settings" // admin.router.ts:951 AdminSessionsPath = "/api/sessions" // admin.router.ts:1086 AdminRolesPath = "/api/roles" // admin.router.ts:1129 AdminTenantsPath = "/api/tenants" // admin.router.ts:1176 AdminMailTemplatesPath = "/api/templates/mail" // admin.router.ts:1448 AdminUITemplatesPath = "/api/templates/ui" // admin.router.ts:1470 )
The read routes, relative to the admin mount. Eight are fixed paths; the six that carry a path parameter are matched by matchAdminRead below.
const ( // AdminUploadLogoPath and AdminUploadBGImagePath are two registrations of // one handler (admin.router.ts:1024, :1035). The reference's two bodies are // identical, down to the comment on the first and not the second, and // neither reads which of the two it is: the SPA tells them apart only by // which input it puts the answer into (admin.js uploadAsset). AdminUploadLogoPath = "/api/upload/logo" AdminUploadBGImagePath = "/api/upload/bg-image" // AdminUploadFilesPath is the listing (admin.router.ts:1047). AdminUploadFilesPath = "/api/upload/files" // AdminUploadPath is the prefix DELETE /api/upload/:filename captures below // (admin.router.ts:1066). It is not a route of its own. AdminUploadPath = "/api/upload" )
The upload routes, relative to the admin mount.
const ( // AdminTwoFAPolicyPath is the bulk require-2FA switch (admin.router.ts:820). AdminTwoFAPolicyPath = "/api/2fa-policy" // AdminSettingsUIPath is the branding patch (admin.router.ts:974). There is // no GET here: the UI block is read through GET /api/settings. AdminSettingsUIPath = "/api/settings/ui" )
The write routes' own paths, relative to the admin mount. The other fourteen reuse a read path with a different method, or add a parameter to one.
const ( SseTopicGlobal = "global" SseTopicTenantPrefix = "tenant:" SseTopicUserPrefix = "user:" SseTopicSessionPrefix = "session:" )
SseTopicGlobal and the three prefixes are the topic vocabulary both sides of the stream agree on: the reference's documented scheme, of which these four are the part either tree actually produces (sse-manager.ts:88, and the convention quoted in full on SseManager).
They are constants rather than string literals at the two call sites because the two sites must not drift: EventTopics builds what a publisher sends to and StreamTopics builds what a connection is allowed to hold, and a typo in either one is silence rather than an error.
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 ( DeliveryKindMagicLink = "magic-link" DeliveryKindPasswordReset = "password-reset" DeliveryKindEmailVerification = "email-verification" DeliveryKindEmailChange = "email-change" DeliveryKindSMSCode = "sms-code" )
Delivery kinds: the value of the request's "kind" field and, prefixed with "delivery.", of the X-Webhook-Event header.
const ( // DocsSpecPath and DocsUIPath are where the two routes sit below the mount // prefix (auth.router.ts:1658, :1674). They are constants and not // configuration: the reference has no option for either, and the family's // clients that read a spec read it from the first. DocsSpecPath = "/openapi.json" DocsUIPath = "/docs" )
const ( // EventUserCreated is raised once an account exists, by the register route // (node-auth auth.router.ts:813). Its data carries the email and whether the // host's onRegister hook or the default handler created the account. EventUserCreated = "identity.user.created" // EventUserDeleted is raised by the self-service account deletion route // after the cookies are cleared (node-auth auth.router.ts:1776). EventUserDeleted = "identity.user.deleted" // EventUserEmailChanged is the one name in this list that does not exist at // ReferenceRevision; see the file comment. The dev line raises it from // POST /change-email/confirm with both addresses in its data (node-auth // auth-event-names.ts:9, raised at node-auth auth.router.ts:1175). EventUserEmailChanged = "identity.user.email.changed" // EventUserEmailVerified is raised by the verification route once the flag // is written and the token cleared (node-auth auth.router.ts:1096). EventUserEmailVerified = "identity.user.email.verified" // EventUserPasswordChanged is raised by the authenticated change-password // route (node-auth auth.router.ts:1030). Note that the reset-password route // raises nothing: see EventNames for what that asymmetry means for U18. EventUserPasswordChanged = "identity.user.password.changed" // EventUser2FAEnabled is raised once a TOTP secret is confirmed and stored // (node-auth auth.router.ts:943). The digits in the Go identifiers here // follow Config.Require2FA, which is already the package's spelling of the // same concept. EventUser2FAEnabled = "identity.user.2fa.enabled" // EventUser2FADisabled is raised once the secret is removed (node-auth // auth.router.ts:997). EventUser2FADisabled = "identity.user.2fa.disabled" // EventUserLinked and EventUserUnlinked are declared by both trees and // raised by neither. The account-linking routes this port already serves — // POST <prefix>/link-verify and DELETE <prefix>/linked-accounts/... — are // the obvious sites, and U18 may not add them: a port does not publish where // the source does not. They are exported because a host that wires its own // linking flow needs the spelling, and because the webhook admin UI offers // the declared vocabulary, not the raised subset. EventUserLinked = "identity.user.linked" EventUserUnlinked = "identity.user.unlinked" // EventSessionCreated, EventSessionRevoked and EventSessionExpired are // declared and never raised, in either tree. Every place a session is // created the dev line raises a login or oauth success instead, with the // session id on the payload. EventSessionCreated = "identity.session.created" EventSessionRevoked = "identity.session.revoked" EventSessionExpired = "identity.session.expired" // EventSessionRotated is raised by the refresh route, and it is the only // session.* name with a publisher: its data carries the previous session id // (node-auth auth.router.ts:733). EventSessionRotated = "identity.session.rotated" // EventAuthLoginSuccess is the busiest name in the vocabulary: five of the // dev line's twenty-six publication points raise it, one per credential // (node-auth auth.router.ts:649 local, :968 totp, :1267 and :1282 // magic-link, :1408 sms). Each distinguishes itself by a `method` key in // data rather than by a name of its own, which is why U18 must not invent // per-credential names. EventAuthLoginSuccess = "identity.auth.login.success" // EventAuthLoginFailed is raised only from the local login route, and only // for a 401 — the dev line's catch tests the status before publishing // (node-auth auth.router.ts:656), so a 500 from a broken store raises // nothing. It carries the attempted email and no user id, because at that // point there may be no user. EventAuthLoginFailed = "identity.auth.login.failed" // EventAuthLogout is raised after the cookies are cleared (node-auth // auth.router.ts:692), and only on the success path: the dev line's catch // clears the cookies a second time and publishes nothing. EventAuthLogout = "identity.auth.logout" // EventAuthOAuthSuccess is raised once per completed OAuth callback, from // handleOAuthLogin — the shared helper all three callback routes reach, and // therefore one publication point serving three routes (node-auth // auth.router.ts:1444). It is a separate name from EventAuthLoginSuccess // even though an OAuth callback is a login, and it is raised only on the // path that issues tokens: a callback that diverts to the 2FA page returns // before it (:1440). EventAuthOAuthSuccess = "identity.auth.oauth.success" // EventAuthOAuthConflict is raised three times, once per conflict site // (node-auth auth.router.ts:1480, :1530, :1580), and is the one name raised // on a path that ends in a redirect rather than a JSON body. It carries no // user id — there is no user yet — and spreads the error's own data under // the provider. EventAuthOAuthConflict = "identity.auth.oauth.conflict" // The four tenant names are declared by both trees and raised by neither, // although Service.CreateTenant and Service.AddUserToTenant exist here and // the equivalents exist there. U18 does not add them for the same reason it // does not add the linking names. EventTenantCreated = "identity.tenant.created" EventTenantDeleted = "identity.tenant.deleted" EventTenantUserAdded = "identity.tenant.user.added" EventTenantUserRemoved = "identity.tenant.user.removed" // EventRoleAssigned and EventRoleRevoked are the two names raised from // outside the auth router. Five sites raise the first — three in the admin // router (node-auth admin.router.ts:1002, :1041, :1055) and two in the // configurator's promoteToAdmin (node-auth auth-configurator.ts:92, :104) — // and two raise the second (node-auth admin.router.ts:1020, // auth-configurator.ts:133). The four admin-router sites are this port's as // of M8: POST <admin>/api/users/{id}/roles, DELETE // <admin>/api/users/{id}/roles/{role} and both branches of POST // <admin>/users/{id}/promote. The three configurator sites have no // counterpart here and are the remainder of U18's twenty-six that U18 could // not reach — AuthConfigurator is the dev line's imperative facade over its // routers, and this port's facade is Auth, whose surface is the routes. // // node-auth admin.router.ts:1002 is also the single publication point in the // whole of the dev line that passes a tenant id. Every other one leaves // Event.TenantID empty. EventRoleAssigned = "identity.role.assigned" EventRoleRevoked = "identity.role.revoked" // EventPermissionGranted and EventPermissionRevoked are declared and never // raised, in either tree. EventPermissionGranted = "identity.permission.granted" EventPermissionRevoked = "identity.permission.revoked" )
The identity.* event vocabulary, in the reference's declaration order and under the reference's grouping (auth-event-names.ts:5-40, node-auth auth-event-names.ts:5-41).
These are untyped string constants rather than values of a named string type, and that is a decision rather than an oversight. A named type — the Go analogue of the reference's `AuthEventName` union (auth-event-names.ts:42) — would buy exhaustiveness nowhere, because Go has no exhaustive switch, and would cost a conversion at every boundary that already speaks `string`: Event.Name, EventBus.Subscribe's first parameter, WebhookConfig.Events, OutgoingWebhookEvent.Event, TelemetryEvent.EventName, TelemetryFilter.EventName and StreamEvent.Type are all `string` today, and every one of them is a place a caller will want to write a constant from this list. Untyped constants are assignable to all of them with no conversion and no API change, and they still fail the compile on a misspelling at the call site, which is the whole of what the union buys a TypeScript caller.
The wire strings are what a subscriber matches on and what a webhook subscription stores, so they are the load-bearing half; the Go identifiers are this port's own and may be read as a convenience. Nothing may change a string here without changing it in the reference first.
const ( OIDCDiscoveryPath = "/.well-known/openid-configuration" OIDCAuthorizePath = "/authorize" OIDCTokenPath = "/token" OIDCUserInfoPath = "/userinfo" )
The four OIDC endpoints, below the mount prefix. They are constants because four places have to spell them identically: RegisterHandlers, the four adapters' MountWithConfig, the JWKSPath collision check, and the generated OpenAPI document.
Unlike the JWKS route these paths are not configurable. They have no counterpart in the reference, which ships no OIDC authorization server, so there is nothing to track: the discovery document is the well-known path RFC 8414 fixes, and the other three are the names the discovery document this package has always served advertises (handleDiscovery below derives them from Issuer, so a second spelling would advertise one path and serve another).
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 ( TemplatePasswordReset = "password-reset" TemplateMagicLink = "magic-link" TemplateWelcome = "welcome" TemplateVerifyEmail = "verify-email" TemplateEmailChanged = "email-changed" TemplateInvitation = "invitation" )
The six template ids the reference renders, as its MailerService names them (mailer.service.ts:186, 191, 196, 201, 206, 211). They are the ids a TemplateStore is asked for and the ids Register, RenderMail and Render take.
const ( // OAuthEmailMatchLink links the provider account to the account that // already holds the address and signs that account in. It is the default // and what this port did before the policy existed. OAuthEmailMatchLink = "link" // OAuthEmailMatchConflict refuses to link and raises the reference's // OAUTH_ACCOUNT_CONFLICT: the callback stashes (email, provider, // providerAccountId) and redirects the browser to /account-conflict, where // the front-end drives /link-request and /link-verify so the link is made // only after the address has been proven by mail. OAuthEmailMatchConflict = "conflict" // OAuthEmailMatchReject refuses outright, with no linking flow offered: // 403 OAUTH_USER_NOT_PROVISIONED, the same answer AutoCreate false gives, // because both mean the deployment will not provision this identity. OAuthEmailMatchReject = "reject" )
The three OnEmailMatch modes.
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" // CodeOAuthAccountConflict is the reference's own literal, and the only one // of these four that reaches a client the reference also sends it to: it // travels in the query of the /account-conflict redirect rather than in an // error body (auth.router.ts:1346-1355). CodeOAuthAccountConflict = "OAUTH_ACCOUNT_CONFLICT" // The three provisioning refusals. They have no reference counterpart — // the reference has no provisioning at all — and are registered in // compatibility.go; none of them can be emitted by a deployment that // configures no OAuthProvisioning. CodeOAuthEmailNotVerified = "OAUTH_EMAIL_NOT_VERIFIED" CodeOAuthEmailDomainNotAllowed = "OAUTH_EMAIL_DOMAIN_NOT_ALLOWED" CodeOAuthUserNotProvisioned = "OAUTH_USER_NOT_PROVISIONED" )
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 ( DefaultJWKSCacheTTL = time.Hour DefaultJWKSFetchTimeout = 5 * time.Second )
Defaults for ResourceServerConfig, from the reference JwksClient constructor (jwks.service.ts:40-41): cacheTtl 3_600_000 ms and fetchTimeout 5000 ms.
const ( // DefaultToolsPath is ToolsOptions.Path's default and the reference's // swaggerBasePath default (tools.router.ts:127). DefaultToolsPath = "/tools" // The five feature routes, each the parent path of a group U23 through U25 // mounts. The three that take a path parameter are spelled without it — // the reference's are '/track/:eventName', '/notify/:target' and // '/webhook/:provider' — because what the switch in ToolsHandler matches on // is the segment above the parameter. ToolsTrackPath = "/track" // tools.router.ts:141, U23 ToolsNotifyPath = "/notify" // tools.router.ts:166, U23 ToolsStreamPath = "/stream" // tools.router.ts:192, U24 ToolsTelemetryPath = "/telemetry" // tools.router.ts:227, U25 ToolsWebhookPath = "/webhook" // tools.router.ts:251, U25 )
The tools router's own paths, relative to its mount. The two documentation routes reuse DocsSpecPath and DocsUIPath: the reference registers them at the same two literals on both routers (tools.router.ts:333, :348 against auth.router.ts:1658, :1674), and spelling them once is what keeps that true.
const ( // ToolsStreamTokenParam is extractSseToken's parameter (tools.router.ts:186). // Read the head of this file before building a client that sets it. ToolsStreamTokenParam = "token" // ToolsStreamTopicsParam is the requested-topic list (tools.router.ts:203). ToolsStreamTopicsParam = "topics" )
The two query parameters of GET <tools>/stream, named because two files read them — this one and the OpenAPI document that describes them.
const ( // TOTPAlgorithm is the HMAC hash, spelled as the otpauth:// `algorithm` // parameter spells it. generateTOTPCode's hmac.New(sha1.New, …) is the // implementation of this value. TOTPAlgorithm = "SHA1" // TOTPDigits is the length of a code. TOTPDigits = 6 // TOTPPeriod is the length of one time step. TOTPPeriod = 30 * time.Second // TOTPSkew is the number of steps either side of now a code is accepted for. TOTPSkew = 1 )
The TOTP parameters. They are stated once, here, and read by both the provisioning URI (totpProvisioningURI) and the verifier (validateTOTPCode), so that what an authenticator app is told at enrolment cannot drift from what the server later checks a code against.
TOTPAlgorithm, TOTPDigits and TOTPPeriod are RFC 6238's defaults and the ones the reference runs with: it constructs otplib's TOTP with nothing but the crypto and base32 plugins (totp.strategy.ts:5-8), so `totp.verify` (:23) takes the library's own algorithm "sha1", digits 6 and period 30 — not knobs there (auth-config.model.ts:272-274 has only `twoFactor.appName`), and not knobs here. They are exported so that a host rendering its own URI, or a test generating codes, reads the same values rather than restating them.
TOTPSkew is the number of whole steps either side of the current one a code is accepted for, and is this port's tolerance, not the reference's: otplib's `epochTolerance` option defaults to 0 seconds (@otplib/totp 13.4.0, the version the reference's package-lock.json pins; the reference pins otplib ^13.3.0 and never sets it), so the reference accepts the current step only. One step of skew is the allowance RFC 6238 §5.2 recommends for clock drift and network delay.
const ( UIDefaultPrimaryColor = "#4a90d9" UIDefaultSecondaryColor = "#6c757d" UIDefaultSiteName = "Awesome Node Auth" // UIDefaultLang is the language the document falls back to when the request // asks for none and none is configured, and the language whose translations // stand in for a language the template store does not hold // (ui.router.ts:103, :110). UIDefaultLang = "en" )
The branding defaults. They are the reference's literals (ui.router.ts:126-129, repeated in its error fallback at :149-151) and are wire values rather than a claim about who wrote the server: the family's clients render siteName as the page heading, so a port that substituted a default of its own would show a different login page for the same configuration.
const ( DefaultWebhookMaxRetries = 3 DefaultWebhookRetryDelay = time.Second )
The defaults the reference documents on WebhookConfig and resolves in the sender rather than in the store (webhook-sender.ts:18-19, webhook-store.interface.ts:27, :32). They are exported because WebhookSender applies exactly these numbers and a host reproducing its retry policy on the far side of a WebhookDeliverer needs the same two, and because a store round-tripping a config has to be able to tell an unset field from a field set to the same value.
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" CodeInvalidInput = "INVALID_INPUT" )
Codes with no counterpart in the published 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.
CodeInvalidInput is the one with a counterpart, but not in the published reference: it comes from the private dev line node-auth (DevLineRevision), which mounts POST /register by default and gives that default handler a presence check on the two fields, refusing the pair with 400 {"error": "Email and password are required", "code": "INVALID_INPUT"} before it hashes or stores anything (node-auth auth.router.ts:515-525). The message and the code here are that handler's, spelled exactly, so a client written against either sees one answer.
The published reference at ReferenceRevision has neither: it mounts /register only when the host supplies options.onRegister (auth.router.ts:713) and has no default handler, so the string INVALID_INPUT does not occur in it. Copying an answer from an unreleased tree is a deliberate bet, recorded in DevLineRevision's own doc comment; nothing else in this package does it.
const ClaimsWebhookEvent = "claims.build"
ClaimsWebhookEvent is the X-Webhook-Event value of every request a ClaimsWebhook sends.
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 CorrelationIDHeader = "X-Correlation-Id"
CorrelationIDHeader is the request header the correlation id is read from: the reference's 'x-correlation-id' (node-auth auth.router.ts:407).
It is spelled in canonical MIME header form here because that is the form http.Header.Get and http.Header.Values want. Header matching is case-insensitive on both sides, so a client sending X-Correlation-ID or x-correlation-id is read identically; Express lowercases its header keys and the reference indexes with the lowercase form, which is the same lookup.
const DefaultAPIPrefix = "/auth"
DefaultAPIPrefix is where the auth routes mount unless told otherwise.
const DefaultIDPKeyID = "provisioner-key-1"
DefaultIDPKeyID is the kid the IdP signs with and publishes when IDPConfig.KeyID is empty: the constant the reference hardcodes for every IdP-issued token and for the JWKS document (token.service.ts:78, jwks.service.ts:184).
const DefaultJWKSMinRefreshInterval = 30 * time.Second
DefaultJWKSMinRefreshInterval is how long a freshly fetched document is trusted to be complete: inside it, a token naming a kid the document does not carry is refused without a refetch.
It has no counterpart in the reference, which invalidates and refetches on every unknown kid (token.service.ts:116-125). That is an unauthenticated request amplifier — anyone who can reach the resource server can make it call the issuer once per request by sending a random kid, and because Invalidate drops the in-flight handle too, concurrent junk does not even coalesce. See the jwks-unknown-kid-refetch-is-rate-limited deviation.
Thirty seconds is picked the way node-jwks-rsa picks its rateLimit default: long enough that a flood costs one fetch, short enough that a real rotation is picked up in the same breath as it happens.
const DefaultJWKSPath = "/.well-known/jwks.json"
DefaultJWKSPath is where the adapters serve the JWKS document, below the mount prefix, when IDPConfig.JWKSPath is empty: the reference's `idProvider.jwksPath ?? '/.well-known/jwks.json'` (auth.router.ts:475).
const DefaultSseHeartbeatInterval = 30 * time.Second
DefaultSseHeartbeatInterval is the reference's default of 30 000 ms (sse-manager.ts:105).
const DefaultWebhookTimeout = 10 * time.Second
DefaultWebhookTimeout bounds one HTTPWebhookDeliverer request whose Timeout is unset.
The reference sets none: its fetch is bounded only by whatever its runtime does. Go's zero value is "wait forever", which for a delivery on a detached goroutine means a leaked goroutine per hung endpoint rather than a slow webhook, so this package picks a bound and makes it a field. Ten seconds is what the removed WebhookDispatcher used, and a receiver that cannot acknowledge in ten seconds should acknowledge and do the work afterwards.
const DevLineRevision = "nik2208/node-auth@e8af923 (unreleased, package.json 1.9.0)"
DevLineRevision names the second tree this package cites: nik2208/node-auth, the private line the published reference is cut from.
Nothing in the deviation register resolves against it. Deviation.Citations is ReferenceRevision only, and compatibility_test.go enforces that. This constant exists because a few wire facts outside the register do come from the dev line, and a reader has to be able to tell which tree a file:line in a doc comment means: a citation written as "node-auth auth.router.ts:N" is this revision, a bare "auth.router.ts:N" is ReferenceRevision.
One such fact exists today, and it is deliberate rather than accidental: the INVALID_INPUT refusal on POST /register (CodeInvalidInput in wire.go, ErrInvalidInput in errors.go, the check in Service.Register). The dev line mounts that route by default and gives its default handler a presence check on the two fields; the published reference mounts /register only when the host supplies options.onRegister (auth.router.ts:713), has no default handler at all, and therefore has no message and no code of its own there. Copying the dev line's answer is a bet that the published tree will grow the same one. When the dev line ships, ReferenceRevision moves forward and those citations fold back into it.
const EventBusWildcard = "*"
EventBusWildcard is the subscription name that receives every event: the channel the reference's publish emits on a second time (auth-event-bus.ts:63, documented at :69).
It is spelled the same as WebhookEventWildcard and means something adjacent but not identical — that one selects webhook subscriptions from a store, this one selects bus handlers in process — so the two are separate constants rather than one shared with two meanings.
const LoginProviderLocal = "local"
LoginProviderLocal is the loginProvider a user without a recorded provider is reported under, on tokens and on the profile alike: the reference's `user.loginProvider ?? 'local'` (auth.router.ts:379).
const MagicLinkVerifyPath = "/magic-link/verify"
MagicLinkVerifyPath is the route a magic link points at, relative to the mount prefix.
const MaxUploadKeyLen = 128
MaxUploadKeyLen bounds a key. The keys the admin routes generate are at most 59 bytes — forty of sanitised name, an underscore, thirteen of millisecond timestamp, a dot and four of extension — and the rest of the budget is for a host that puts objects in the store itself.
It is a byte count and the grammar is ASCII, so it is also a character count and a safe column width.
const OutgoingWebhookVersion = "1"
OutgoingWebhookVersion is the default OutgoingWebhookEvent.Version, the reference's AuthToolsOptions.webhookVersion ?? "1" (auth-tools.ts:175).
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.
const UIConfigRoute = UIRoute + "/config"
UIConfigRoute is where the document is served below the mount prefix. The reference mounts its whole ui router at <prefix>/ui (auth.router.ts:1640) and the config endpoint at /config within it (ui.router.ts:165).
It is exported because openapi.go describes the route and the conformance suite replays it, and one constant is what keeps the description and the mount from parting company — the way ResourceServerGatedRoutes is shared already.
Since v0.9.0 no adapter registers this path on its own router. UIHandler serves the whole of <prefix>/ui, this route included, and delegates it to the same handler the adapters used to mount here — which is where the reference has it too (ui.router.ts:165 is a route inside the router auth.router.ts:1640 mounts at /ui). The constant is built from UIRoute so that the two cannot drift.
const UIRoute = "/ui"
UIRoute is where the reference mounts its whole ui router below the API prefix (auth.router.ts:1640). Everything in this file is relative to it.
const UploadMaxBytes = 5 << 20
UploadMaxBytes is the largest file the admin upload routes accept, and it is the reference's multer `limits: { fileSize: 5 * 1024 * 1024 }` (admin.router.ts:1016).
It bounds the route rather than the store: Put is handed a reader the route has already held to this, so an implementation may stream its argument without a limit of its own. A host calling Put directly is responsible for its own bound, because nothing else is.
const UpstreamUIAssetCommit = "cc01e9975fe9e425dc6d938a9c5d0738b59c79d8"
UpstreamUIAssetCommit is the full object name of the awesome-node-auth commit the files under ui/upstream/assets were taken from.
ReferenceRevision in compatibility.go abbreviates the same commit and is what the deviation register's citations resolve against; this constant spells it out because the re-vendoring recipe in the README needs a full object name, and because an abbreviation cannot be pasted into `git cat-file` with any guarantee that it still resolves uniquely in a tree that has grown since. TestVendoredUIAssetsAgreeWithReferenceRevision holds the two together.
const UpstreamUIAssetDir = "src/ui/assets"
UpstreamUIAssetDir is the directory in the reference tree the assets were copied from, so that a reader can find the originals without already knowing the reference's layout.
const WebhookEventWildcard = "*"
WebhookEventWildcard is the events entry that subscribes a configuration to every event (webhook-store.interface.ts:11). It is the only pattern the reference understands: matching is literal otherwise, so "identity.auth.*" is an event name nothing emits rather than a prefix subscription — the example query matches with whereJsonContains (:106).
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") // ErrInvalidInput is a registration missing one of the two fields the route // cannot proceed without. It is deliberately not ErrWeakPassword: an absent // password is not a rejected one, and the default register handler on the // private dev line node-auth refuses the pair together, before it hashes or // stores anything (node-auth auth.router.ts:518-519, resolved against // DevLineRevision). The published reference at ReferenceRevision mounts // /register only when the host supplies options.onRegister and has no // default handler at all, so it has no code of its own here. See // Service.Register. ErrInvalidInput = errors.New("auth: email and password are required") // 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 provisioning refusals, all on the callback and all JSON rather than a // redirect — the reference's own choice for everything on that route that is // not the account conflict (wire-contract.md §4, gap 5). 403 rather than 401: // the provider did authenticate somebody, and the deployment will not admit // them, so repeating the flow cannot help. HTTPErrOAuthEmailNotVerified = HTTPError{Status: http.StatusForbidden, Message: "OAuth email address is not verified", Code: CodeOAuthEmailNotVerified} HTTPErrOAuthEmailDomainNotAllowed = HTTPError{Status: http.StatusForbidden, Message: "OAuth email domain is not allowed", Code: CodeOAuthEmailDomainNotAllowed} HTTPErrOAuthUserNotProvisioned = HTTPError{Status: http.StatusForbidden, Message: "No account is provisioned for this OAuth identity", Code: CodeOAuthUserNotProvisioned} )
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} // HTTPErrInvalidInput carries the dev line's literal message: a body that // names neither field is not a weak password, and answering WEAK_PASSWORD // there tells the user to pick a better one when they typed none at all. HTTPErrInvalidInput = HTTPError{Status: http.StatusBadRequest, Message: "Email and password are required", Code: CodeInvalidInput} )
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 ErrAPIKeyNotFound = errors.New("auth: api key not found")
ErrAPIKeyNotFound is what an API key store answers when no record matches. It lives here rather than in errors.go for the reason ErrDeliveryFailed lives beside the senders: it is one feature's store contract and means nothing outside it.
The reference's two finders return `ApiKey | null` (api-key-store.interface.ts:48, :54), which a two-value Go return has no room for, so absence is this error rather than a zero record with a nil error — the same shape the rest of this package uses. Verify collapses it, and every other store error, into ErrInvalidCredentials, so the distinction never reaches the wire.
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 ErrOAuthAccountConflict = errors.New("auth: oauth account conflict")
ErrOAuthAccountConflict is the sentinel every account conflict unwraps to, so a caller can test for the outcome with errors.Is without naming the type. The details — which address, which provider account — are on OAuthAccountConflictError, which errors.As recovers.
var ErrSseStreamingUnsupported = errors.New("auth: response writer does not support streaming")
ErrSseStreamingUnsupported is returned by SseManager.Serve when the http.ResponseWriter it was handed cannot be flushed, so nothing it wrote would reach the client before the handler returned. Nothing has been written to the response when it is returned, so a caller may still answer the request itself.
var ErrUploadNotFound = errors.New("auth: uploaded file not found")
ErrUploadNotFound is what Open and Delete report for a key the store does not hold. It is a sentinel rather than a bare error so that DELETE <admin>/api/upload/:filename can answer the reference's 404 for an absent object and its 500 for a store that failed, which is a distinction the reference draws with fs.existsSync before it calls fs.unlinkSync (admin.router.ts:1074-1080).
Drawing it with one store call instead of two is deliberate: the reference's exists-then-unlink pair races with a second administrator deleting the same file, and answering 500 for that race would be worse than answering 404 for it.
var HTTPErrInvalidTokenRS = HTTPError{ Status: http.StatusUnauthorized, Message: "Invalid or expired access token", Code: CodeInvalidToken, }
HTTPErrInvalidTokenRS is what the resource-server middleware answers for any verification failure: 401 with code INVALID_TOKEN (jwks-auth.middleware.ts:77).
It shares its message with HTTPErrInvalidAccessToken and differs in status and code, which is the reference difference too: the session middleware answers 403 and no code (auth.middleware.ts), and this one answers 401 with a code. Both messages are the same string, so a client that only surfaces the message reads the same sentence either way.
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.
var HTTPErrSseNotEnabled = HTTPError{Status: http.StatusServiceUnavailable, Message: "SSE not enabled"}
HTTPErrSseNotEnabled is the answer when the facade was built without SSE: `res.status(503).json({ error: 'SSE not enabled' })` (tools.router.ts:193-196). No code field, because the reference's literal has one key.
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 AdminOpenAPIHandler ¶ added in v0.10.0
func AdminOpenAPIHandler(info AdminOpenAPIInfo) http.Handler
AdminOpenAPIHandler serves GenerateAdminOpenAPISpec(info) as application/json, which is what the reference's route does with the document its own generator returns (admin.router.ts:1499-1515).
The document is built once, here, and not per request: it is a pure function of info. The handler carries no guard, as the reference's route carries none; AdminOptions.Docs is where that is decided and says what it costs.
func BearerToken ¶ added in v0.2.0
BearerToken extracts the token from an Authorization header.
func BuildRS256JWT ¶ added in v0.5.0
BuildRS256JWT creates a signed RS256 JWT using only standard library packages: header {"alg":"RS256","typ":"JWT","kid":kid}, the claims as given, both unpadded base64url, and the PKCS#1 v1.5 signature over the SHA-256 of base64url(header) + "." + base64url(claims), as RFC 7515 section 5.1 and RFC 7518 section 3.3 require.
The signer is asked for exactly one thing: Sign(rand.Reader, digest, crypto.SHA256) with the 32-byte SHA-256 digest of the signing input. An *rsa.PrivateKey answers with PKCS#1 v1.5, which is what RS256 is; a signer backed by a KMS, an HSM or another process must do the same when it sees crypto.SHA256 as the option (AWS KMS: RSASSA_PKCS1_V1_5_SHA_256 on a MessageType DIGEST request), and its Public must return the matching *rsa.PublicKey for the key to be published.
A signer whose Public is not an *rsa.PublicKey — an ECDSA or Ed25519 key — is refused with an error before Sign is asked: the alternative is a token whose header says RS256 over a signature no RS256 verifier accepts, and no error anywhere. NewIDP makes the same check once at construction; this one is for a host calling BuildRS256JWT on its own, and it consults Public on every call.
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 CompileProfileMap ¶ added in v0.4.0
CompileProfileMap compiles a declarative profile map into the function OAuthProvider.MapProfile takes. It is what NewOAuthService runs over every OAuthProvider.ProfileMap, exported so a deployment can validate a map it loaded from configuration before it wires anything.
The keys are id, email, emailVerified, name and picture — the fields of the reference profile a mapProfile hook returns (generic-oauth.strategy.ts:71-77). Any other key is an error, and so is a map without id, since the profile it would produce can never identify a user. The values are expressions in this grammar, which is this port's own (the reference takes a function):
expr = alt ("??" alt)*
alt = path | literal
path = "$" ("." key | "[" index "]")+
key = one or more letters, digits, "_" or "-"
index = one or more decimal digits
literal = a double-quoted string, allowed only as the last alternative
Whitespace around an alternative is ignored. Anything else — "$" alone, a single-quoted literal, a wildcard, a filter, a slice, a function call, a quoted key — is a compile error naming the expression.
Evaluation follows JavaScript's ?? operator: the alternatives are tried in order and the first path that resolves to a value that is neither missing nor null wins, so an empty string, false or 0 is a value and stops the chain; a literal always resolves. A string is taken as is, a number is rendered as JavaScript's String() renders it (an integer-valued number has no decimal point and no exponent below 1e21) and a boolean becomes "true" or "false"; a value that is an object or an array is an evaluation error. emailVerified is the one non-string field: its value must be a boolean or the strings "true" and "false", and a literal that is neither is refused at compile time. id is required — an id that resolves to nothing, or to the empty string, is an evaluation error; the other four are optional and stay empty (emailVerified nil) when no alternative resolves.
The returned function fills ProviderID, Email, Name, AvatarURL, EmailVerified and Raw; Provider is left for the service to set.
func ContextWithEventContext ¶ added in v0.9.0
func ContextWithEventContext(ctx context.Context, ec EventContext) context.Context
ContextWithEventContext returns ctx carrying ec as the request provenance.
It is exported for the same reason ContextWithUser is: a host that mounts this library behind something other than the four adapters — a gRPC gateway, a Lambda handler translating an API Gateway event, a queue consumer replaying a recorded request — has the three values and no *http.Request to hand EventContextFromRequest, and must still be able to install them.
func ContextWithUser ¶ added in v0.6.0
ContextWithUser returns ctx carrying user as the authenticated principal.
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 EventContextMiddleware ¶ added in v0.9.0
func EventContextMiddleware(cfg HTTPConfig) func(http.Handler) http.Handler
EventContextMiddleware installs the request provenance on the request context so that anything publishing an event downstream can reach it.
It is the counterpart of RateLimitMiddleware and CSRFMiddleware, takes the same HTTPConfig and returns the same shape, and the four adapters compose it outermost — ahead of the limiter, which is ahead of the CSRF middleware. That position is chosen and worth stating:
- It cannot refuse a request, so nothing is gained by running it later and one thing is lost. The limiter and the CSRF middleware are the two places a request is turned away, and a host that has replaced either with its own code — HTTPConfig.RateLimiter is a slot for exactly that — may want to publish or log from inside it. Outermost is the only position where the carrier is already there when that happens.
- The reference's position is "at publish time", which is outside every middleware in the sense that matters: getRequestEventContext reads the raw headers off the request and no middleware in the chain can have changed them. Installing first is the closest a split carrier gets to that.
It costs one context.WithValue and two header lookups per request, on a chain that already allocates a context value for the authenticated user.
A host mounting this library some other way can call it directly, and a host that mounts nothing of this package's middleware at all still works — an event published without a carrier simply has the three fields empty. See EventBus.PublishContext.
func EventNames ¶ added in v0.9.0
func EventNames() []string
EventNames returns the whole identity.* vocabulary, in the reference's declaration order.
The order is the reference's grouping — user, session, authentication, tenant, authorization — and not alphabetical, because the two consumers that show the list to a person show it in this order: a webhook admin screen offers the names as checkboxes, and the OpenAPI document describes the `events` field of a webhook subscription by enumerating them. Sorting would scatter `identity.user.2fa.enabled` away from the rest of the user block for no gain.
Three things need the full set rather than the fifteen names anything actually raises, and each needs it for a different reason:
- The webhook admin UI (U14 and the admin routes behind it) offers a subscriber every name the library might ever raise, not the subset it raises today. A subscription created against a name nothing raises yet is valid and simply never fires; one that could not be created at all would have to be created again after every release that adds a publisher.
- The OpenAPI document describes the format of WebhookConfig.Events. The wildcard aside (WebhookEventWildcard), this list is the domain of that field.
- The conformance test that U18 ends with asserts that this port raises an event at each of the twenty-six places the dev line raises one. That test counts publication points against the dev line, not names against this list — the two numbers are both twenty-six by coincidence, since fifteen distinct names cover twenty-six sites and twenty-six names are declared. It needs this function to assert that every name it expects is a name the package actually declares, so that a typo in the test fails the test rather than silently passing against a name nobody publishes.
The returned slice is freshly allocated and shares nothing with the package.
func EventTopics ¶ added in v0.10.0
EventTopics resolves the SSE topics one event is broadcast to: the reference's private resolveTopics (auth-tools.ts:353-359).
Always `global`; then `tenant:<id>`, `user:<id>` and `session:<id>` for each identifier the event carries. It is exported because a host wiring SseManager.BridgeEventBus directly needs the same rule Track applies, and a second copy written at the call site is the thing that drifts:
stop := sse.BridgeEventBus(ctx, bus, auth.EventTopics)
It does not agree with the subscribe side, and that is the source ¶
StreamTopics authorises three kinds of topic and this produces four. The reference has the same gap: its stream route builds `global`, `tenant:<id>` and `user:<id>` and nothing else (tools.router.ts:207-210), so the `session:<id>` topic this publishes to is one no client connected through that route can ever hold. Every event with a session id is therefore broadcast to a topic with no subscribers.
It is reproduced rather than corrected in either direction. Dropping the session topic here would remove a channel a host serving its own stream can use — connections are registered with whatever topics the server passes, and nothing but that route restricts them. Adding it to StreamTopics would widen what a client can subscribe to beyond what the reference allows, on a surface whose entire security property is that the server picks the topics. The broadcast to nobody costs a map lookup that matches nothing.
In this port the tenant topic is unreachable too, for now and for a different reason: Event.TenantID is empty on everything the library currently publishes, so until M8 gives the service layer a tenant to put there, the library's own events resolve to `global`, `user:<id>` and `session:<id>`.
func GenerateAdminOpenAPISpec ¶ added in v0.10.0
func GenerateAdminOpenAPISpec(info AdminOpenAPIInfo) map[string]any
GenerateAdminOpenAPISpec builds the admin console's OpenAPI 3.0 document: the reference's buildAdminOpenApiSpec (openapi.ts:672-1362).
func GenerateOpenAPISpec ¶
func GenerateOpenAPISpec(info OpenAPIInfo) map[string]any
GenerateOpenAPISpec returns an OpenAPI 3.0 spec for the mounted auth endpoints.
func GenerateToolsOpenAPISpec ¶ added in v0.10.0
func GenerateToolsOpenAPISpec(info ToolsOpenAPIInfo) map[string]any
GenerateToolsOpenAPISpec builds the tools router's OpenAPI 3.0 document: the reference's buildOpenApiSpec (openapi.ts:1366-1637).
func IsBearerRequest ¶ added in v0.2.0
IsBearerRequest reports whether the caller asked for token delivery in the response body instead of cookies.
func IsEventName ¶ added in v0.9.0
IsEventName reports whether name is one of the declared identity.* names.
It is a membership test over the vocabulary and nothing more: it does not reject a name this library never raises, and it does not accept WebhookEventWildcard, which is a subscription term rather than an event name — WebhookConfig.Matches handles that one, and a store or an admin route that validates a subscription has to allow both this and the wildcard.
The linear scan is deliberate. Twenty-six comparisons of short strings that share a seven-byte prefix cost less than the map lookup that would replace them, and a map would have to be built at init time for a function that a validating admin route calls at most once per request.
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), and so do the adapters here: MagicLinkDelivery.LinkBase is that value (Auth.ResolveSiteURL, HTTPConfig.LinkBase), and a custom sender passes it straight in. 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 OpenAPIHandler ¶ added in v0.7.0
func OpenAPIHandler(info OpenAPIInfo) http.Handler
OpenAPIHandler serves GenerateOpenAPISpec(info) as application/json, which is what the reference's route does with the document its own generator returns (auth.router.ts:1658-1671).
The document is built once, here, and not per request: it is a pure function of info, and nothing the handler sees can change it. The bytes are stable too — encoding/json writes map keys in sorted order — so a caching client sees one body per configuration.
The handler carries no middleware of its own, as the reference's route carries no guard: no auth, no session, nothing to present a credential to. The adapters put CSRFMiddleware in front of it, which on a GET only distributes the cookie the reference's router-level auto-init distributes (auth.router.ts:529-538) and rejects nothing. A host mounting it by hand should leave it as public as that, or keep it off: it describes routes, not data, but it is still a description of the surface an attacker would otherwise have to guess.
func ParseRSAPrivateKeyPEM ¶ added in v0.5.0
func ParseRSAPrivateKeyPEM(pemText string) (*rsa.PrivateKey, error)
ParseRSAPrivateKeyPEM decodes a PEM-encoded RSA private key, accepting both the PKCS#8 form the reference generates ("PRIVATE KEY", jwks.service.ts:152) and the PKCS#1 form openssl genrsa writes ("RSA PRIVATE KEY"). Anything else — no PEM block, another block type, a PKCS#8 key of a different algorithm — is an error. The result is what IDPConfig.Signer takes: an *rsa.PrivateKey is a crypto.Signer.
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) and so do the adapters here — each delivery's LinkBase is that value, see MagicLinkURL — so a custom sender passes delivery.LinkBase in, falling back to a base of its own when it is empty.
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 RateLimitMiddleware ¶ added in v0.7.0
func RateLimitMiddleware(cfg HTTPConfig) func(http.Handler) http.Handler
RateLimitMiddleware returns the configured rate limiter, or a pass-through when HTTPConfig.RateLimiter is nil.
It is the counterpart of CSRFMiddleware and the adapters compose the two in that order — this one outermost — so that a refused request never reaches either the CSRF comparison or the auth middleware. See HTTPConfig.RateLimiter for the reference citations.
func ReadUpstreamUIAsset ¶ added in v0.9.0
ReadUpstreamUIAsset returns the contents of one vendored asset by base name, for example "auth.js" or "login.html".
The returned slice is a copy. embed.FS hands out the backing array of the embedded data itself, so a caller that wrote through an uncopied slice would corrupt the asset for every later reader in the process — including the drift check, which would then report a difference nobody made on disk.
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 ResourceServerGatedRoutes ¶ added in v0.6.0
ResourceServerGatedRoutes is the credential surface HTTPConfig.ResourceServer unmounts, as method-keyed paths relative to the mount prefix.
Nineteen routes, and the rule behind the list is one sentence: a route is here when it mints, delivers or consumes a credential. Nothing more is claimed for it — in particular, this is not the set of routes that need a local user database; see below.
POST /register create an account and a password POST /login exchange a password for a session POST /refresh rotate this instance own session POST /logout revoke this instance own session POST /forgot-password mint and mail a reset token POST /reset-password spend a reset token POST /change-password replace a password POST /send-verification-email mint and mail a verification token GET /verify-email spend a verification token POST /change-email/request mint and mail an email-change token POST /change-email/confirm spend an email-change token POST /magic-link/send mint and mail a login credential POST /magic-link/verify spend it for a session POST /sms/send mint and text a login credential POST /sms/verify spend it for a session POST /2fa/setup enrol a TOTP secret POST /2fa/verify-setup confirm the enrolment POST /2fa/verify spend a second factor for a session POST /2fa/disable drop the enrolment
What stays mounted is GET /me, the session routes, PATCH /profile, POST /add-phone, DELETE /account, and the whole OAuth and account-linking group. On an Auth built WithIDP, the JWKS route, <prefix>/userinfo and <prefix>/.well-known/openid-configuration stay too — and <prefix>/authorize and <prefix>/token do not.
Those four are not in this map, because this map is the base surface's and they are routes only under auth.WithIDP; the two gated ones are named by OIDCMount.ResourceServerGated, which every adapter reads from the same walk that mounts them, and by OpenAPIInfo.OIDC, which drops them from the spec in the same breath. The reason they are gated is the definition at the top of this file: POST <prefix>/authorize reads an email, a password and a tenant off the form and hands them to Service.Login, and POST <prefix>/token spends the resulting code for this instance's own HS256 session pair, so leaving them mounted would hand a deployment that set this flag to unmount /login and /refresh a password-accepting endpoint and a session-minting one in two hops. Discovery is metadata, like the JWKS document beside it, and userinfo reads a bearer token and returns a profile, which is GET /me's shape and stays for GET /me's reason.
Those still need a local user store, and saying otherwise would be a trap: /me goes through Service.Authenticate to users.GetUserByID (service.go:236,249-255); /profile, /add-phone and /account read and write the user store; and GET /oauth/{provider}/callback provisions a user and mints a local session, which is a credential this list otherwise gates. They stay because the reference mounts them and because a deployment that does have a store — a full instance that also accepts another issuer's bearer tokens — uses them. Resource-server mode is about credentials, not about store independence: the store-less deployment is the one for ResourceServerMiddleware, whose two paths read no store at all, and such a deployment should not be mounting the auth router's account routes in the first place. Gating them here instead would take routes away from the hybrid deployment, which is the commoner one, to protect a deployment that has no reason to mount them; that is the decision, and it is why the list is what it is rather than "every route that touches the user store".
The reference gates six of these — /login, /logout, /refresh, /register, /forgot-password and /reset-password (auth.router.ts:541, :590, :622, :713, :777, :802) — and leaves the other thirteen mounted, where they fail at runtime against a database that is not there. Gating all nineteen is the resource-server-gates-all-credential-routes deviation; see CompatibilityNotes.
The returned map is freshly built, so a caller may edit it.
func ResourceServerMiddleware ¶ added in v0.6.0
ResourceServerMiddleware guards a host route in resource-server mode (jwks-auth.middleware.ts:37-80).
A bearer token is verified against the remote JWKS; an access-token cookie is verified against this instance own HS256 secret. Neither path reads a store: the principal is built from the verified claims in both cases, which is what makes the middleware usable on the deployment it is named after — an instance with no user database. The reference keeps the cookie path for the dashboard that hosts the resource server and may itself be logged in, and verifies it with tokenService.verifyAccessToken (jwks-auth.middleware.ts:70-73), a bare jwt.verify against the HS256 secret with no store access of any kind (token.service.ts:143-150).
The two failures are the reference failures, and they differ:
- no token at all — neither header nor cookie — is 403 {"error":"No access token provided"}, code-less, the same body the session middleware writes (jwks-auth.middleware.ts:56-59);
- anything else is 401 {"error":"Invalid or expired access token", "code":"INVALID_TOKEN"} (:76-78).
The cookie path here is deliberately not Auth.Authenticate, which the adapters' session Middleware uses. Authenticate reads the user store and checks the session for revocation; verifyAccessToken does neither, so this middleware does neither. Two consequences follow, both the reference's: a cookie whose subject is not in this instance store is still accepted, and a revoked session is accepted until its access token expires — SESSION_REVOKED is a code of the session middleware, and this middleware cannot emit it. A deployment that wants revocation checked on every call mounts the session Middleware instead; it is the one with a database behind it.
The user reaches the handler through UserFromContext on the request context, which is what every adapter UserFromContext reads.
One JWKSClient is built per call, so the key cache is shared by every request through the returned middleware and not beyond it. The reference caches one client per URL for the whole process (jwks-auth.middleware.ts:10-23); a host that mounts two middlewares against one issuer and wants one cache builds the client once with NewJWKSClient and shares it. Nothing on the wire depends on which it does.
func ResourceServerPrincipal ¶ added in v0.6.0
func ResourceServerPrincipal(r *http.Request, a *Auth, client *JWKSClient, issuer string) (User, HTTPError, bool)
ResourceServerPrincipal verifies the credential on r the way ResourceServerMiddleware does and returns the principal, the error envelope to write when there is none, and whether verification succeeded.
It is exported for the framework adapters, which have to put the user into their own context rather than into the request context, and for a host writing a middleware for a framework this repository does not ship.
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
deprecated
ServeAdminUI returns an http.Handler serving the embedded admin dashboard HTML.
The page is this port's own, not the reference's. The reference's admin SPA is vendored as admin.js and admin.css and is served by UIHandler along with everything else under <prefix>/ui — but it calls an admin API that no adapter mounts until M8, so it is bytes on disk rather than a working dashboard, and this hand-written page is neither replaced nor removed in this release.
Deprecated: this page is not maintained and nothing in this module serves it. It will be removed in v1.0.0; there is no drop-in replacement until the admin router lands in M8.
func ServeAuthJS
deprecated
ServeAuthJS returns an http.Handler serving the auth.js browser SDK.
What it serves changed in v0.9.0. It now returns the reference's auth.js, vendored byte for byte under ui/upstream/assets and pinned by its sha256 (ui_upstream.go), rather than this port's hand-written ui/auth.js. The hand-written file is still in the tree and still embedded, because ui_test.go's contract tests are written against it and because removing it is a breaking change this release does not make — but nothing serves it any more, and it is not maintained.
One behaviour changed with the bytes. The hand-written SDK sent X-Auth-Strategy: bearer and exposed the tokens it got back; the reference's SDK is a cookie-only client and never sends that header. The server is unaffected and still honours X-Auth-Strategy from any caller, so a page that wants bearer delivery can still ask for it — but it can no longer ask for it through this SDK. See the v0.9.0 CHANGELOG entry.
Deprecated: set HTTPConfig.UI.Enabled and let the adapter mount <prefix>/ui, where UIHandler serves this same file — and the rest of the vendored set, with the SSR config injection the reference's pages expect — at the path those pages actually load it from. Or read it directly: UpstreamUIAssetFS(), or ReadUpstreamUIAsset("auth.js"). This function is a thin wrapper kept so that existing importers keep compiling, and it will be removed in v1.0.0.
func ServeAuthUI
deprecated
ServeAuthUI returns an http.Handler serving the embedded auth UI HTML (login/register).
It serves this port's hand-written page, and it serves it raw: no branding, no site name, no logo, no injected config. The page fetches <prefix>/ui/config for itself, so it works, but it flashes unstyled and it is not the page the family's deployments show.
Deprecated: mount the UI instead — set HTTPConfig.UI.Enabled and the adapter registers <prefix>/ui, where UIHandler serves the reference's own login page with the SSR config injection. This function will be removed in v1.0.0.
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 SortUploadedFiles ¶ added in v0.10.0
func SortUploadedFiles(files []UploadedFile)
SortUploadedFiles applies the listing order UploadStore.List documents and GET <admin>/api/upload/files answers with: newest first, ties broken by key ascending.
The first half is the reference's `sort((a, b) => b.mtime.localeCompare(a.mtime))` over ISO-8601 strings (admin.router.ts:1057), which orders by time because that encoding sorts lexicographically. The second half the reference does not have: its comparator returns 0 for two files stamped the same second and Array.prototype.sort is stable, so equal timestamps come back in readdir order — the filesystem's, and not a contract. A total order is, and it is what lets a test and a paginating client see the same page twice.
It is exported because a store implementing List has to produce this order and should not have to re-derive it.
func StreamTopics ¶ added in v0.10.0
StreamTopics resolves the topics one connection may hold: the reference's stream route, which builds the authorised list from the authenticated principal and filters the client's request against it (tools.router.ts:207-216).
`global`, plus `tenant:<id>` and `user:<id>` for whichever the principal has. requested is what the client asked for — the `topics` query parameter, split and trimmed by whatever serves the route. Nothing requested means the whole authorised list; anything requested is intersected with it, so a client that asks for a topic it is not entitled to is silently given the rest, and one that asks only for such topics gets an empty list and a stream that never fires. That silence is the reference's, and it is the correct shape for a filter whose job is that a client cannot self-declare a channel.
It lives here, beside EventTopics, because the two are one decision: what a publisher sends to has to be what a subscriber is allowed to hold, and the one place that is true is the place both are written. Mounting the route that calls this is U22's.
func SwaggerUIHandler ¶ added in v0.7.0
SwaggerUIHandler serves the reference's Swagger UI page for the document at specURL, as text/html (auth.router.ts:1674-1677). An empty specURL means the reference's own default, "./openapi.json" (openapi.ts:1646).
The page is rendered once, here: it is a pure function of specURL.
func ToolsOpenAPIHandler ¶ added in v0.10.0
func ToolsOpenAPIHandler(info ToolsOpenAPIInfo) http.Handler
ToolsOpenAPIHandler serves GenerateToolsOpenAPISpec(info) as application/json, which is what the reference's route does (tools.router.ts:333-346).
The document is built once, here, and not per request: it is a pure function of info. The handler carries no guard, as the reference's route carries none; ToolsHandler is where that is decided and says why.
func ToolsProtectMiddleware ¶ added in v0.10.0
func ToolsProtectMiddleware(cfg HTTPConfig) func(http.Handler) http.Handler
ToolsProtectMiddleware is the reference's `...protect` spread (tools.router.ts:135) as one wrapper: the configured guard, or a pass-through when the host asked for ToolsPublic.
It is the counterpart of RateLimitMiddleware and CSRFMiddleware and is written the same way, so that every route U23 through U25 adds behind a guard is one call — ToolsProtectMiddleware(cfg)(handler) — and none of them has to re-derive which configurations mean "no guard". A configuration with no Access at all never reaches here: ToolsMounted is false for it and the adapters register no route to wrap.
func ToolsSseTokenMiddleware ¶ added in v0.10.0
ToolsSseTokenMiddleware is the port of extractSseToken (tools.router.ts:185-190): it copies a `?token=` query parameter into an `Authorization: Bearer …` header so that the guard behind it — which reads headers, as every guard does — can authenticate a browser EventSource, which cannot set one.
It is applied to GET <tools>/stream ahead of ToolsProtectMiddleware, in that order, because the reference registers it ahead of ...protect (:192). It is exported for a host serving its own stream route off SseManager.Serve: the posture is the route's, not the manager's, and a host that wants the same one should not have to rewrite it.
Read the head of this file before mounting it anywhere else. A token in a URL is a token in access logs, in Referer headers and in browser history, and this middleware is the thing that makes that trade.
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 UploadContentType ¶ added in v0.10.0
UploadContentType is the media type for a key's extension, or "" for an extension that is not one of the seven.
Nothing in this package needs it: the UI serves an object through http.ServeContent, which types it from the same extension. It is exported for the store implementations a host writes, because an object store records a content type at write time and serves it forever after — an S3 PutObject without ContentType serves binary/octet-stream, and a browser will not paint a logo it is handed under that type.
The table is written out rather than taken from mime.TypeByExtension because that function reads the system's mime.types on unix: ".ico" is absent from Go's built-in table, so the answer for it would depend on which files the host image happens to ship.
func UploadFS ¶ added in v0.10.0
func UploadFS(store UploadStore) fs.FS
UploadFS presents an UploadStore as the read-only fs.FS UIOptions.Uploads is, so that one configured store serves both halves of the reference's arrangement: the admin routes write through the store, and the UI's <prefix>/ui/assets/logo/ and <prefix>/ui/assets/uploads/ mounts read the same objects back (ui.router.ts:185-191).
A host does not normally call this. UIHandler falls back to it when UIOptions.Uploads is nil and an UploadStore is configured, which is what makes "configure an UploadStore" enough — see UIOptions.Uploads for the precedence. It is exported for the host that wants the read side somewhere else: its own static mount, an http.FileServer, an fs.WalkDir over what has accumulated.
What it does with names ¶
A name that is not a valid key does not reach the store: Open answers fs.ErrNotExist for it. So the UI's fall-through behaviour is unchanged — a request for "../../secrets" under the upload prefix misses here exactly as it missed against an os.DirFS, and ends at the same 404 — and the store is never asked to defend a name this package could have refused.
What it cannot do ¶
fs.FS.Open takes no context, so the store is called with context.Background(): there is no request deadline and no cancellation on a client that hung up. A store whose reads can block indefinitely should carry its own timeout, and a host that needs per-request cancellation on the read path sets UIOptions.Uploads to its own fs.FS instead of relying on this.
The returned file implements io.Seeker only when the store's reader does. Without one the UI reads the object into memory before serving it, which is bounded by UploadMaxBytes for anything these routes wrote.
func UploadNameAllowed ¶ added in v0.10.0
UploadNameAllowed is the reference's multer fileFilter (admin.router.ts:1018-1022): the client-supplied filename must end in one of seven image extensions, compared case-insensitively.
It tests the name and nothing else — no magic bytes, no declared Content-Type — which is the reference's rule and is reproduced rather than improved on. The consequences are worth stating where a host will meet them, because both are served back from the auth origin:
- A file named "logo.png" holding HTML is stored and served as image/png. A browser will not render it as a document at that type, but a host that serves the upload prefix without X-Content-Type-Options: nosniff is relying on the type header alone.
- An SVG is an XML document that may carry script, and it is on this list. Served same-origin it runs with the auth cookies. The upload routes are behind the admin guard, so the author is an administrator — but a host that would rather not take that trade serves the upload prefix from a separate origin, or behind a Content-Security-Policy, or drops "svg" by wrapping the store. Dropping it here would be a different product from the reference and would break a console that offers it.
func UpstreamUIAssetFS ¶ added in v0.9.0
UpstreamUIAssetFS returns the vendored assets as a filesystem rooted at the asset directory, so that a caller opens "login.html" rather than the whole embedded path.
This is the seam U11 mounts. Serving these files is deliberately not part of the change that vendors them, so that the bytes and their provenance can be reviewed on their own before anything depends on them being served.
func ValidUploadKey ¶ added in v0.10.0
ValidUploadKey reports whether key names an object: non-empty, at most MaxUploadKeyLen bytes, made only of ASCII letters, digits, '.', '_' and '-', and not beginning with '.'.
Why this shape, and not a traversal check ¶
The reference's delete route defends by inspection: it refuses a filename containing '/' or '\' or beginning with '.', then calls path.join(uploadDir, filename) (admin.router.ts:1067-1073). That check happens to be sufficient for its own three cases, and it is the wrong shape of defence anyway, because it enumerates what a caller must not send. This grammar enumerates what a key may be, and the difference is that traversal stops being something to catch:
- No key contains '/' or '\', so a key is exactly one path element under any root, on every operating system. There is no separator to build a second element out of.
- No key begins with '.', so no key is "." or "..", the only two names that move a path upwards without a separator.
- No key contains ':', so a key cannot name a Windows drive or an NTFS alternate data stream, and no key is a URL scheme or an S3 endpoint.
- Every byte is ASCII, so no key survives a Unicode normalisation or an overlong-encoding round trip as a different key.
filepath.Join(root, key) is therefore root plus one element, and strings.TrimSuffix(prefix, "/") + "/" + key is one object below the prefix, by construction rather than by check. A future implementation that resolves a key against something else — a database row, a URL — inherits the same property, which is the reason the rule lives on the seam and not in the one route that reads a name off the wire.
It implies fs.ValidPath, so a key is always a legal fs.FS name too.
What it does not settle ¶
Two things an implementation still owns. A filesystem-backed store on Windows must refuse the reserved device names — "NUL.png" satisfies this grammar and opens the null device — and any store must decide whether its namespace is case-sensitive, because "Logo.png" and "logo.png" are two keys here and one file on a case-insensitive volume.
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 VerifyRS256 ¶ added in v0.6.0
func VerifyRS256(ctx context.Context, token string, client *JWKSClient, expectedIssuer string) (map[string]any, error)
VerifyRS256 verifies an RS256 token against the keys client publishes and returns its claims — the reference verifyWithJwks (token.service.ts:102-141).
The shape is the reference shape, step for step:
- the header is decoded without verifying, only to read kid;
- a token with no kid is refused;
- the header alg must be RS256. This check is the reference's algorithms: ['RS256'] (token.service.ts:136) hoisted above the key lookup: the reference reaches the network first and only then refuses an alg:none or alg:HS256 token, which means a junk token can cost an outbound fetch. Nothing on the wire changes — both refusals are the same 401 INVALID_TOKEN — only which of the five messages is logged;
- the key is looked up; if the cache does not carry it, the cache is invalidated and the lookup retried exactly once, which is how a rotation is picked up mid-TTL. A second miss is refused, and so is the first when the cached document is younger than MinRefreshInterval (see invalidateForRotation and the jwks-unknown-kid-refetch-is-rate-limited deviation);
- the signature is checked with RS256 pinned, and exp and nbf are honoured;
- when expectedIssuer is set, iss must equal it — a plain string comparison, as the reference does it (token.service.ts:137).
Every refusal of the token itself wraps ErrInvalidToken, so HTTPErrorFor maps it to 401 INVALID_TOKEN. A failure to read the JWKS at all does not: a transport error, a non-200 status and an unparseable document surface from GetKey unchanged and unwrapped, and HTTPErrorFor sends them to HTTPErrInternal, because an issuer outage is this server's 5xx and not the caller's bad credential. ResourceServerMiddleware answers 401 either way, as the reference does (its catch does not distinguish them either, jwks-auth.middleware.ts:76-78); a host calling VerifyRS256 directly sees the difference and should.
The claims are returned as the decoded JSON object, unchanged: this verifier does not know what an issuer puts in its tokens, and a resource server is entitled to read all of it.
func VerifyWebhookSignature ¶
VerifyWebhookSignature validates an X-Webhook-Signature header against the raw body it should cover: the reference's WebhookSender.verify (webhook-sender.ts:65-74), which a receiver calls rather than a sender.
sigHeader is the header value whole, "sha256=" prefix included. A nil error means the body was signed with secret and has not been altered since.
It answers an error where the reference answers false, because a Go caller gating on a webhook wants to know which of the three refusals it hit while it is writing the check, and because false is the one return value that is easy to ignore. The comparison is constant-time either way — the reference hand-rolls one over char codes (:68-73) and this uses hmac.Equal — and the length check that precedes it leaks nothing on either side: the length of a hex-encoded SHA-256 is not a secret.
func WebhookTimestamp ¶ added in v0.10.0
WebhookTimestamp formats t the way the reference's toISOString() does, which is the only spelling OutgoingWebhookEvent.Timestamp and X-Webhook-Timestamp may carry: UTC, exactly three fractional digits, a literal Z.
It is exported because a host building an OutgoingWebhookEvent by hand — a replay tool, a test fixture, the far side of a queueing WebhookDeliverer — has to produce the same bytes, and time.RFC3339Nano does not.
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 APIKeyAdminStore ¶ added in v0.8.0
type APIKeyAdminStore interface {
ListAll(ctx context.Context, limit, offset int) ([]APIKeyRecord, error)
}
APIKeyAdminStore is the reference's optional `listAll?` (api-key-store.interface.ts:74-78): the page of records an admin listing walks. A configured store that does not implement it is the case GET /admin/api/api-keys answers `501 {"error": "IApiKeyStore.listAll is not implemented", "keys": [], "total": 0}` for (admin.router.ts:1259-1262), which is the whole reason it is an interface of its own and not a method on APIKeyStore.
Ordering. ListAll returns records newest first: CreatedAt descending, ties broken by ID ascending. That sentence is normative for this package. The reference ships the interface and no implementation of it, so there is nothing to reproduce and the choice has to be made somewhere; it is made here, MemoryAPIKeyStore is its executable statement, and a store that cannot hold to it — a DynamoDB table with no suitable index, say — registers its own order as a deviation against this paragraph rather than inventing one in silence.
Newest first because that is what a key listing is read for: the key just minted is the one the operator came to look at. The ID tiebreak is what makes it a *total* order, and that is the load-bearing half. CreatedAt alone is not one — two keys can share an instant, and a backing column with second resolution will make them share it often — and limit/offset paging over a partial order silently repeats some rows and drops others. Records with a zero CreatedAt sort last, after every record that has one.
The order is stable for a fixed set of records. It is not stable across a concurrent Save: a key created between two pages shifts every older record one place, so the second page repeats a row. The reference pages the same way (:1263-1265) and its `total` is openly a guess (:1288), so the property is reproduced rather than fixed.
limit is the maximum number of records to return and offset how many to skip first. A limit of zero returns nothing, not everything; a negative limit or offset is read as zero. Returning fewer than limit records is how a caller learns the page is the last one — it is exactly what the reference's `total` heuristic reads (:1288) — so a store must not return a short page for any other reason.
type APIKeyAuditEntry ¶ added in v0.8.0
type APIKeyAuditEntry struct {
// KeyID is the record the attempt was made against. The reference writes the
// literal `<unknown>` when the key did not resolve, so that an attempt
// against a prefix nobody owns is still audited
// (api-key.strategy.ts:215-217); a caller in this package must write the
// same string rather than an empty one.
KeyID string
// Timestamp is when the attempt was made, from the caller's clock.
Timestamp time.Time
// IP is the remote address the attempt came from, empty when unknown.
IP string
// UserAgent is the request's User-Agent, empty when absent.
UserAgent string
// Path and Method describe the request. See the type comment: the reference
// declares them and never fills them in.
Path string
Method string
// Success says whether the attempt authenticated. Both outcomes are logged.
Success bool
// FailureReason is the reference's own vocabulary on a failure —
// INVALID_KEY, KEY_REVOKED, KEY_EXPIRED, IP_NOT_ALLOWED,
// INSUFFICIENT_SCOPE (api-key.strategy.ts:91, :97, :103, :111, :122) — and
// empty on success.
FailureReason string
}
APIKeyAuditEntry is one usage record for an API key, the reference's ApiKeyAuditEntry (api-key-store.interface.ts:93-103).
The reference's optional fields are plain strings here rather than pointers: every one of them is a header value or a request component, where absent and empty are the same fact.
Two of them the reference never populates. Path and Method are declared on the type (:99-100) and its only producer, the strategy's audit hook, writes keyId, timestamp, ip, userAgent, success and failureReason and nothing else (api-key.strategy.ts:214-223). They are carried anyway, because a store written against the reference will have columns for them.
type APIKeyAuditStore ¶ added in v0.8.0
type APIKeyAuditStore interface {
LogUsage(ctx context.Context, entry APIKeyAuditEntry) error
}
APIKeyAuditStore is the reference's optional `logUsage?` (api-key-store.interface.ts:86-90): a per-key access log.
This is a seam. Nothing in this package calls LogUsage today, and a store that implements it will not be asked for anything until something does. In the reference the caller is the API key strategy, and it is off by default: the hook returns immediately unless `ApiKeyStrategyOptions.auditLog` is set *and* the store implements the method (api-key.strategy.ts:31, :213), and it swallows whatever LogUsage throws, because "Audit log failures must never block the request" (:224-228). The caller arrives with the admin API key surface in M8 (v0.10.0), which is where APIKeyMiddleware grows the reference's auditLog option and where the entries this collects are read. It is defined now so that a host writing a store for M8 writes the table once.
The seam is deliberately not wired early. Logging every authentication attempt — the failures above all, which is the point of it — turns an unauthenticated request into a write, and that is a decision for the milestone that also ships the switch to turn it off.
type APIKeyDeleteStore ¶ added in v0.8.0
APIKeyDeleteStore is the reference's optional `delete?` (api-key-store.interface.ts:80-84): a hard delete, audit trail and all. The reference's own advice is not to implement it — "prefer `revoke` for audit-trail preservation" (:83) — and DELETE /admin/api/api-keys/:id is written so that declining costs nothing: with no delete it revokes instead and says so in the body, `200 {"success": true, "note": "IApiKeyStore.delete not implemented; key was revoked instead"}` (admin.router.ts:1348-1354).
That fallback is why this is an interface of its own: the route has to be able to observe the absence of this one method, on a store whose other four are present, to choose between the two answers.
An id that matches nothing is a no-op returning nil, as it is on Revoke and for the same reason: the route awaits the call and answers 200 without a lookup of its own (:1349-1350).
type APIKeyRecord ¶
type APIKeyRecord struct {
ID string
Prefix string
Name string
ServiceID string
KeyHash string
Scopes []string
AllowedIPs []string
IsActive bool
ExpiresAt *time.Time
CreatedAt time.Time
LastUsedAt *time.Time
}
APIKeyRecord stores hashed key material and policy constraints.
CreatedAt is the minting instant, and it is not decoration. The admin listing projects it (`createdAt`, admin.router.ts:1275) and the create route echoes it back in the record beside the once-only raw key (:1325), so a store that drops it serves nulls to a screen built to show them. It is also the only field that can order that listing: newID mints `key_` plus random hex (security.go:20-26), so the IDs carry no time component and sorting by one is sorting by noise. APIKeyService.Create stamps it; a store must round-trip it unchanged.
It is a value and not a pointer because the reference declares it non-optional — `createdAt: Date` (api-key.model.ts:59-60), where `expiresAt` and `lastUsedAt` are both nullable — and this type keeps that split. A zero CreatedAt therefore means a record that predates the field or a store that lost it, never "this key has no creation time".
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)
Create mints a key, hashes it and hands the raw value back exactly once.
CreatedAt is stamped here, from time.Now(), which is where the reference stamps it too — the record literal in createKey carries `createdAt: new Date()` (api-key.service.ts:64) — rather than being left to the store. A store that filled it in itself would give two records created in one request different notions of "now", and would make the record this returns disagree with the row it just wrote; the admin create route echoes the returned record straight back to the caller (admin.router.ts:1314-1327), so the two have to be the same value.
LastUsedAt stays nil, as the reference leaves it null (:65): a key that has never been presented has no last use, and zero would read as 1 January year 1.
func (*APIKeyService) Verify ¶
func (s *APIKeyService) Verify(ctx context.Context, store APIKeyStore, rawKey string, ip string, requiredScopes []string) (APIKeyRecord, error)
type APIKeyServiceIndexStore ¶ added in v0.8.0
type APIKeyServiceIndexStore interface {
ListByServiceID(ctx context.Context, serviceID string) ([]APIKeyRecord, error)
}
APIKeyServiceIndexStore is the reference's optional `listByServiceId?` (api-key-store.interface.ts:68-72): every key issued to one service identity, active or not.
Nothing in the reference calls it. It is declared on IApiKeyStore, described as needed "only if you expose a key-listing UI" (:70-71), and no route, no strategy and no service reaches for it — the admin UI lists with listAll instead. It is ported because the interface is the contract a host implements against, and a host that has already written the query should not discover later that this port dropped it; it is not ported because anything here needs it.
Ordering is APIKeyAdminStore's: CreatedAt descending, ID ascending. There is no limit or offset because the reference declares none — the whole set comes back — so a service identity with a pathological number of keys is the caller's problem, as it is there.
type APIKeyStore ¶
type APIKeyStore interface {
// Save persists a newly created record. APIKeyService.Create is its only
// caller in this package and calls it once, with a record whose ID and
// Prefix were minted a few lines earlier; the reference's own example is a
// bare insert (api-key-store.interface.ts:16-18), with no collision rule,
// and none is imposed here.
Save(ctx context.Context, key APIKeyRecord) error
// FindByPrefix returns the *active* candidate for a prefix. The reference is
// explicit — "Only return records where `isActive = true`"
// (api-key-store.interface.ts:46) — and its example implementation puts the
// flag in the WHERE clause (:20). The prefix is a lookup index and not a
// credential: the caller still verifies the raw key against KeyHash.
//
// Verify does not depend on the filter. It re-checks IsActive on whatever
// comes back, exactly as the reference's strategy does
// (api-key.strategy.ts:96), so a store that leaks revoked rows here is wrong
// but not exploitable. Implement the filter anyway: it is the contract, and
// a revoked key stays reachable for the admin screens through FindByID.
//
// Answer ErrAPIKeyNotFound when no active record has this prefix.
FindByPrefix(ctx context.Context, prefix string) (APIKeyRecord, error)
// FindByID returns the record with this id whatever its state — active,
// revoked or expired. It is the deliberate opposite of FindByPrefix, and the
// reference says why: it is "Used for revocation, rotation, and admin
// management" (api-key-store.interface.ts:51-52), and a management screen
// that could not open a revoked key could not show an operator the thing
// they just revoked. Nothing on the authentication path calls it, so the
// widening costs no strictness there.
//
// Answer ErrAPIKeyNotFound for an id that is absent.
FindByID(ctx context.Context, id string) (APIKeyRecord, error)
// Revoke marks the key inactive. It is a soft delete, and it is the one the
// reference prefers — "prefer `revoke` for audit-trail preservation"
// (api-key-store.interface.ts:83) — so the record stays readable through
// FindByID afterwards.
//
// An id that matches nothing is not an error. The reference's example is an
// unconditional update behind a `where({ id })`
// (api-key-store.interface.ts:25-27), which no-ops on a miss, and the admin
// route awaits it and answers `200 {"success": true}` with no lookup of its
// own (admin.router.ts:1337-1338). A store that answered ErrAPIKeyNotFound
// here would turn that 200 into a 500.
Revoke(ctx context.Context, id string) error
// UpdateLastUsed stamps the record after a successful authentication. Verify
// calls it and discards the error, so a store may treat it as best-effort.
//
// The reference's signature is `updateLastUsed(id, at?)`
// (api-key-store.interface.ts:66) with the instant optional and its example
// defaulting to the store's own clock (:28-30); its one caller passes the
// instant anyway (api-key.strategy.ts:132). Here it is required, so the
// timestamp is always the caller's clock and a test can pin it.
//
// An unknown id is a no-op, for the reason it is on Revoke.
UpdateLastUsed(ctx context.Context, id string, when time.Time) error
}
APIKeyStore persists and resolves API keys.
This is the mandatory half of the reference's IApiKeyStore, and the reference draws the line itself: "All methods are optional except for `findByPrefix` and `findById`" (api-key-store.interface.ts:7-9). The four it marks optional are not methods here at all. Each is a one-method companion interface below, which a caller reaches by type-asserting the configured store — the way SessionAdminStore and UserMetadataStore are reached — so a store that implements none of them still satisfies APIKeyStore and still authenticates.
The optional four are split one to an interface, rather than gathered into a single admin interface, because the reference distinguishes them one at a time and answers differently for each. GET /admin/api/api-keys answers `404 {"error": "API key store not configured"}` when no store is configured (admin.router.ts:1254) and `501 {"error": "IApiKeyStore.listAll is not implemented", "keys": [], "total": 0}` when the configured store has no listAll (:1259-1262): two statuses for two different absences, and the 501 names the single method. A combined interface would fail its assertion for a store that implements ListByServiceID and not ListAll, and answer 501 naming a method that is present. DELETE /admin/api/api-keys/:id makes the same distinction the other way round — with no delete it falls back to revoke and answers `200 {"success": true, "note": "IApiKeyStore.delete not implemented; key was revoked instead"}` (:1348-1354) — which it can only do if the absence of Delete alone is observable.
FindByID sits on this interface and not among the companions because the reference makes it mandatory. Adding it breaks every existing implementor of the previous four-method interface, and that is taken deliberately: the admin surface addresses keys by id throughout, Revoke and UpdateLastUsed already take an id so any store that satisfies them already has an id index and the new method is one query it can already run, and leaving it optional would put an entry in the deviation register whose only argument is that this port shipped the interface incomplete first. v0.8.0 is the release the roadmap names as the one that completes APIKeyStore, so it is the release that pays for it.
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 AdminAccessPolicy ¶ added in v0.10.0
type AdminAccessPolicy struct {
Kind AdminPolicyKind
Predicate AdminPolicyFunc
}
AdminAccessPolicy is the reference's AdminAccessPolicy (admin.router.ts:38-42): one of three named policies, or a host-supplied predicate.
It is a struct behind a pointer rather than a bare string because the reference distinguishes three states and not two — a policy, a legacy secret, and *neither* (:516-537) — and `accessPolicy !== undefined` is the test that picks the first. A nil *AdminAccessPolicy is "not set".
Kind and Predicate are exclusive: a non-nil Predicate is the function arm and Kind is ignored. A zero value — no Kind, no Predicate — is the reference's "policy is a string nobody matched": the evaluation falls through every branch with granted still false, and the request is refused 403.
func AdminFirstUser ¶ added in v0.10.0
func AdminFirstUser() *AdminAccessPolicy
func AdminIsAdminFlag ¶ added in v0.10.0
func AdminIsAdminFlag() *AdminAccessPolicy
func AdminOpen ¶ added in v0.10.0
func AdminOpen() *AdminAccessPolicy
AdminOpen, AdminFirstUser, AdminIsAdminFlag and AdminPredicate build the four policies. They exist so that a configuration reads as one call — the shape the reference's string union has — rather than as a struct literal whose fields a reader has to check against each other.
func AdminPredicate ¶ added in v0.10.0
func AdminPredicate(fn AdminPolicyFunc) *AdminAccessPolicy
type AdminGuard ¶ added in v0.10.0
type AdminGuard struct {
// contains filtered or unexported fields
}
AdminGuard is buildPolicyGuard (admin.router.ts:250-395) plus the selection above it (:516-537), resolved once at mount time.
One value serves every admin route, and the two wrappers below are the only way through it. U13 onwards mount their routes with Protect and read the principal with AdminUserFromContext; nothing else about this type is theirs to re-derive.
func (*AdminGuard) LoginRoutesMounted ¶ added in v0.10.0
func (g *AdminGuard) LoginRoutesMounted() bool
LoginRoutesMounted reports whether POST <admin>/login and POST <admin>/logout exist: the reference registers both only under `sessionBased && secret` (:541).
func (*AdminGuard) Protect ¶ added in v0.10.0
func (g *AdminGuard) Protect(next http.Handler) http.Handler
Protect is the guard every admin route other than the shell wraps itself in.
It differs from the reference in exactly one branch, and it is the branch the reference gets wrong. There, an unauthenticated GET carrying `Accept: text/html` is let through to *every* guarded route when no loginPath is configured (:318-325) — so `curl -H 'Accept: text/html' /admin/api/users` reads the user table with no credential at all, because the handler behind the guard never looks at the marker. Here the marker never reaches a handler through Protect: it is 401, like every other unauthenticated request. See the admin-unauthenticated-get-serves-only-the-login-form deviation.
func (*AdminGuard) ProtectShell ¶ added in v0.10.0
func (g *AdminGuard) ProtectShell(next http.Handler) http.Handler
ProtectShell is Protect for the one route that is allowed to answer an unauthenticated request: the HTML shell, whose whole job in that state is to render the built-in login form (:318-325, :709-724).
The marker reaches the handler as adminNeedsAuth, and the handler serves a page with showLogin set and every feature flag off. Nothing else is behind this wrapper, and nothing else may be.
func (*AdminGuard) SessionBased ¶ added in v0.10.0
func (g *AdminGuard) SessionBased() bool
SessionBased reports whether the console authenticates through a session rather than through the legacy bearer secret. It is what makes the shell conditional (:707) and what the SPA reads to choose its login form.
type AdminOpenAPIInfo ¶ added in v0.10.0
type AdminOpenAPIInfo struct {
// Title, Description and Version default to the reference's literals with
// the package name changed (openapi.ts:1344-1348).
Title string
Description string
Version string
// BasePath is where the admin router is mounted as a reader can reach it,
// and the prefix every documented path carries: the reference's basePath
// parameter, whose default is '/admin' (openapi.ts:674). Empty means
// DefaultAdminPath.
BasePath string
// The eight flags createAdminRouter passes (admin.router.ts:1501-1510), in
// the reference's polarity: each includes the path items of its group. They
// must match the stores the Auth was built with, or the document describes
// routes the console answers 404 for — (*Auth).AdminOpenAPIInfo is the way
// not to have to remember that.
Sessions bool
Roles bool
Tenants bool
Metadata bool
Settings bool
LinkedAccounts bool
APIKeys bool
Webhooks bool
// UI is the reference's hasUi, and it describes two paths the admin router
// has never served. Nothing sets it. See this file's header.
UI bool
// Docs mirrors AdminOptions.Docs.Enabled: the two documentation routes the
// console then mounts are described by the document too. Set the two
// together, or the document omits two routes the server serves — or
// describes two it does not.
//
// The reference's generator describes neither, under any option. That half
// of the docs-routes-are-opt-in deviation is registered in compatibility.go
// and now covers all three documents.
Docs bool
}
AdminOpenAPIInfo is what the admin document is generated from: the feature flags that select its optional path items, and the base path they are written under.
It is the reference's AdminOpenApiOptions plus basePath (openapi.ts:645-664, :672-675). (*Auth).AdminOpenAPIInfo fills one in from a mounted configuration, which is how the adapters get it; a caller building one by hand is describing a mount it is responsible for matching.
type AdminOptions ¶ added in v0.10.0
type AdminOptions struct {
// Enabled mounts the admin surface, the way Docs.Enabled mounts the
// documentation routes. Unset — the default — nothing under Path is
// registered at all and every admin route answers 404.
//
// Enabled on its own is not sufficient: with neither AccessPolicy nor
// Secret configured the adapters still mount nothing. That is the
// admin-console-requires-an-explicit-policy deviation, and the shortest way
// to the reference's own default is AccessPolicy: AdminOpen().
Enabled bool
// Path is where the admin router mounts, on the same router the adapter was
// given and beside HTTPConfig.APIPrefix rather than under it. Empty means
// DefaultAdminPath, which is the reference's own default (:190).
Path string
// AccessPolicy is the reference's accessPolicy: the session-based guard,
// and the first arm of the selection at :516-537. Non-nil wins over Secret.
//
// Every policy other than AdminPolicyOpen needs JWTSecret to verify
// anything, exactly as there (:76-79) — with no secret the guard reads no
// token, every request is unauthenticated, and the surface is a login form
// and a string of 401s.
//
// Nil, with Secret also empty, is where this port parts company with the
// reference. There the router is built anyway, a warning is written to
// stderr and every route is served unguarded (:530-536); here nothing is
// mounted. A deployment that wants the open console asks for it by name
// with AdminOpen(), which is the reference's own vocabulary for exactly
// that deployment and which puts the decision in the host's configuration
// where a reviewer can see it.
AccessPolicy *AdminAccessPolicy
// Secret is the reference's deprecated adminSecret (:44-56): a bearer token
// compared against the Authorization header, and the second arm of the
// selection. It is also a login credential — POST <admin>/login accepts it
// as a password for the empty email or the literal "admin" and answers with
// a token carrying isRoot (:557-562).
//
// Under Secret alone the shell is served unguarded and the SPA does the
// login client-side, holding the secret in sessionStorage (:707-737,
// admin.js). Prefer AccessPolicy.
//
// Deprecated: use AccessPolicy. Kept because the reference keeps it.
Secret string
// JWTSecret verifies the tokens the guard reads and signs the one
// <admin>/login mints — the reference's jwtSecret (:68-79). Empty means
// Config.Secret, which is what the reference's "must match
// AuthConfig.accessTokenSecret" asks a host to arrange by hand, and which
// is the arrangement that makes an ordinary access token a valid admin
// credential for the policy to judge.
//
// Setting it to something else is a real choice and not a hardening: it
// severs that integration, so the only credential the guard can then accept
// is one <admin>/login minted, and AccessPolicy still decides who that is.
JWTSecret string
// LoginPath redirects an unauthenticated browser instead of showing the
// built-in login form (:107-113, :311-315). Set, the guard answers 302 to
// <LoginPath>?redirect=<the admin URL>; unset, a GET that accepts HTML is
// let through to the shell, which renders the form.
//
// The value is emitted as given. A host that points it at another origin is
// choosing to, and the redirect parameter it carries is this router's own
// path, never anything read off the request.
LoginPath string
// CookiePrefix overrides the prefix on the cookie the guard reads and
// <admin>/login writes — the reference's cookiePrefix (:96-103).
//
// It is a *string because the reference tests `cookiePrefix !== undefined`
// (:221, :291): an explicitly empty prefix means the bare name
// "accessToken" and is a different instruction from leaving it unset, which
// tries "__Host-accessToken", "__Secure-accessToken" and "accessToken" in
// that order on the way in and derives the name from CookieOptions on the
// way out.
CookiePrefix *string
// RootUser is the bootstrap credential; see AdminRootUser.
RootUser *AdminRootUser
// AuthAPIPrefix is the auth router's mount as the SPA should call it — the
// reference's apiPrefix (:152-157), injected as authApiPrefix. Empty means
// HTTPConfig.Prefix(), which this port knows and the reference's separate
// router does not.
AuthAPIPrefix string
// UploadBaseURL is the public base the SPA and the two upload routes build
// uploaded-asset URLs from — the reference's uploadBaseUrl (:136-150).
//
// Empty is not "no URL": with an UploadStore configured it is derived from
// the mount, as the reference derives it from apiPrefix, and the derived
// value is where UIHandler serves those objects back. See
// (*Auth).adminUploadBaseURL, whose comment carries the one place the
// derivation differs from the reference's.
//
// Set it when the assets are served from somewhere this deployment cannot
// know: a CDN in front of the UI mount, or a bucket served directly. The
// value is emitted as given, into the injected config and into the `url`
// member of both upload answers.
UploadBaseURL string
// Docs mounts the console's own documentation pair, GET <admin>/api/openapi.json
// and GET <admin>/api/docs, and says under which base path the document
// describes itself (admin.router.ts:1493-1521).
//
// It is DocsOptions, the same type HTTPConfig.Docs and ToolsOptions.Docs
// are, because the reference's three routers carry the same swagger option
// and the same swaggerBasePath beside it. BasePath defaults to
// HTTPConfig.AdminPath() here, which is the reference's own
// `options.swaggerBasePath ?? '/admin'` (:1498) read against a mount this
// port knows.
//
// # Enabling it publishes the admin API description to anonymous callers
//
// This is the one thing to weigh, and it is deliberate on both sides. Every
// other route under /api/* is registered with the guard; these two are not
// (:1499, :1517). So with this enabled, a caller holding no credential of
// any kind reads the whole documented admin surface — every path, every
// method, every request body — and, because the document's path items follow
// the configured stores, learns which optional features this deployment
// wired. The Swagger page beside it additionally loads swagger-ui-dist@5
// from the unpkg CDN onto the auth origin, with no subresource integrity;
// see DocsOptions.Enabled, which says the same of the auth router's pair.
//
// That asymmetry is reproduced rather than tidied away, because a port that
// quietly guarded these two would answer 401 where the reference answers
// 200 and would break any tooling that reads the document — and because the
// place to decide it is a host's configuration, which is why it is off by
// default here where the reference's is on outside production. That default
// is the docs-routes-are-opt-in deviation, which this pair now shares.
Docs DocsOptions
// RateLimiter is the console's own limiter slot, separate from
// HTTPConfig.RateLimiter and applied to **exactly one route**: POST
// <admin>/users/{id}/promote. nil — the default — means no limiter, which is
// the reference's own default: it collapses the option to an empty
// middleware list when it is absent (node-auth admin.router.ts:577) and
// ships no algorithm of its own.
//
// One route is not an oversight to improve on. The dev line spreads
// `...rateLimiter` onto the promote route and onto nothing else (node-auth
// admin.router.ts:1030, the option at :205-211), and the route it does not
// cover is the one a reader will assume it covers: the admin login
// (node-auth admin.router.ts:614) is deliberately not rate limited on either
// line, and neither is any other route on this surface. A field documented
// as "the admin limiter" would say the opposite of that while being true of
// one route, which is why this field is worded the way it is and why U12
// left the slot out until the route it applies to existed.
//
// It runs before the guard, reproducing the spread's order at :1030 —
// `...rateLimiter, guard`. So a caller over the limit is refused without a
// token being verified, a store being read or a policy being evaluated, and
// is refused whether or not it holds an admin credential, which is what
// makes the slot worth anything against an unauthenticated flood. It is the
// same ordering rule HTTPConfig.RateLimiter states for the auth router.
//
// Like that field it is a constructor called once at mount time and not a
// handler, so whatever the limiter counts with must be created outside the
// function and captured by it. Here that matters less — one route, one call
// — but a host that passes the same limiter to this slot and to
// HTTPConfig.RateLimiter gets one shared budget, which is what passing one
// Express handler instance to both options gives there.
//
// This is a development-line option: at ReferenceRevision AdminOptions has
// no rateLimiter at all (admin.router.ts:44-186). See the
// admin-promote-route-comes-from-the-development-line deviation, which
// covers the route and this slot together.
RateLimiter func(http.Handler) http.Handler
}
AdminOptions is HTTPConfig.Admin: whether the adapters mount the admin surface, where, and who gets in.
It is the reference's AdminOptions (admin.router.ts:44-186) less the store slots. The reference passes each optional store to createAdminRouter and turns the SPA's tabs on from what it was handed; this port already holds those stores on the Auth, so the feature flags are derived from what is actually configured (adminFeatures) and there is nothing to pass twice.
type AdminPolicyFunc ¶ added in v0.10.0
AdminPolicyFunc is the reference's custom predicate arm: handed the resolved user and the configured RBAC store, returning whether to grant (admin.router.ts:41, :376).
A non-nil error is a denial and not a 500, because the reference wraps the whole policy evaluation in a try/catch whose catch sets granted = false (:378-380). A predicate that cannot reach its store therefore refuses the request rather than failing it, on both sides.
type AdminPolicyKind ¶ added in v0.10.0
type AdminPolicyKind string
AdminPolicyKind is the string half of the reference's AdminAccessPolicy union (admin.router.ts:38-42).
const ( // AdminPolicyOpen grants every request, with no token read and no store // consulted (admin.router.ts:269). It is the reference's "use only behind a // VPN or IP allow-list" (:29) and it is also the only way to ask this port // for an unguarded console — see AdminOptions.AccessPolicy. AdminPolicyOpen AdminPolicyKind = "open" // AdminPolicyFirstUser grants the user whose id matches ListUsers(1, 0)[0] // (admin.router.ts:372-374). AdminUserStore's doc comment carries why that // ordering is a contract here and a promise the reference's own stores do // not keep. AdminPolicyFirstUser AdminPolicyKind = "first-user" // AdminPolicyIsAdminFlag grants a user whose IsAdmin is set, and reads // nothing else — not Role, not RBAC (admin.router.ts:370). See User.IsAdmin. AdminPolicyIsAdminFlag AdminPolicyKind = "is-admin-flag" )
type AdminRootUser ¶ added in v0.10.0
AdminRootUser is the reference's AdminOptions.rootUser (admin.router.ts:110-115): a bootstrap credential that lives in configuration rather than in the user store, for "environments without local users".
PasswordHash is a bcrypt hash and never a password. A login against it mints a token carrying isRoot, which is the one claim that skips the store lookup and the policy alike — so this pair is the most privileged credential the console has, and it is as strong as the hash and the JWT secret together.
type AdminUserStore ¶ added in v0.8.0
type AdminUserStore interface {
ListUsers(ctx context.Context, tenantID string, limit, offset int) ([]User, error)
}
AdminUserStore lists users for the admin surface. It is the reference's IUserStore.listUsers (user-store.interface.ts:130) with one parameter added.
What tenantID means, and what it does not ¶
tenantID filters on User.TenantID: the column every user record already carries, and which userEmailKey already makes part of a user's identity. It is *not* a membership query. TenantStore.AssociateUserWithTenant and GetUsersForTenant (tenant-store.interface.ts:112) answer "which users belong to this tenant", a many-to-many relation kept in its own table, and this interface deliberately never consults it: a user whose TenantID is "acme" and who is additionally associated with "globex" is listed under "acme" and nowhere else. Keeping the two apart is the point of the parameter — the membership join is precisely the reachability dead end this interface exists to route around, and answering listing out of it would rebuild the dead end inside the fix.
An empty tenantID is the one place in this package where an empty tenant is a wildcard rather than a literal: it means no filter at all — every user, in every tenant, including users whose own TenantID is empty. Everywhere else in store.go, "" is an ordinary tenant value matching only records stored under it. The exception is not cosmetic. GET /admin/api/users takes no tenant parameter (admin.router.ts:748 onwards) and neither does the 'first-user' policy's `listUsers(1, 0)` (:373), so "every user" has to be expressible, and the reference's signature — which carries no tenant at all — says that is the default reading. The ambiguity this introduces is unobservable in the deployment that would suffer from it: where every user's TenantID is empty, filtering on the empty tenant and not filtering at all return the same set — which is the single-tenant deployment, the common one. It becomes observable only in a tree that mixes tenanted users with untenanted ones, which is migration residue rather than a configuration.
Why the parameter exists ¶
The reference has no tenancy on this route, so the faithful signature is ListUsers(ctx, limit, offset) and a multi-tenant console would filter the page after the fact. That is wrong in the direction that matters: filtering after the page has been cut means a tenant with ten users is invisible behind a hundred thousand belonging to someone else, and every route above it inherits the problem. Pushing the predicate into the store instead costs one parameter, is what every other user-facing method in store.go already does, and — see below — is the form a keyed store can actually satisfy. U13 passes "" and gets the reference's behaviour exactly.
Implementing it on a key-value store ¶
The first implementor is a DynamoDB single-table design, and the requirement there is that this be served without a table Scan. It can be: a secondary index with a *constant* partition key (GSI1PK = "USER") and a sort key of `<tenantID>#<id>` answers both forms as a Query — unscoped with no sort-key condition, scoped with begins_with(GSI1SK, tenantID+"#"). Nothing here is keyed on the tenant itself, which is deliberate: a tenant-partitioned index serves the scoped form and forces a cross-partition Scan for the unscoped one, which is the form the reference's only consumers use.
Two honest limits on that. First, the constant partition key is a hot key under write load; sharding it (USER#0..USER#n) is the usual answer and costs an n-way merge to keep the order. Second, offset is positional, and no key-value store can seek to the Nth item: offset N costs reading and discarding N index entries. That is bounded rather than unbounded — the route clamps limit to 100 and the filter path asks for 500 rows at offset 0 — but it is a real cost, and the only signature that removes it is a cursor, which cannot express the reference's `total` arithmetic and would change the wire. Offset stays.
The `<tenantID>#<id>` sort key above orders the unscoped listing by (TenantID, ID) rather than by ID alone. That is a deviation from the normative order documented above, and the store that makes it registers it.
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) AdminGuard ¶ added in v0.10.0
func (a *Auth) AdminGuard(cfg HTTPConfig) *AdminGuard
AdminGuard resolves the access decision for cfg. It is safe to call once per mount and share; the value holds no per-request state.
func (*Auth) AdminHandler ¶ added in v0.10.0
func (a *Auth) AdminHandler(cfg HTTPConfig) http.Handler
AdminHandler serves the whole admin router: the six routes of U12, dispatched on the path below the mount.
It is one handler rather than six mounts for the reason (*Auth).UIHandler is: the reference mounts one router, the later PRs fill it with paths carrying parameters, and a catch-all beside fixed siblings is not a shape every Go router will register — gin's tree refuses it outright. The four adapters therefore mount this at <AdminPath> and <AdminPath>/, and U13 onwards add their routes to the switch below rather than to four mount functions.
Anything the switch does not recognise is a 404, which is what an Express router with no matching layer ends at.
func (*Auth) AdminOpenAPIInfo ¶ added in v0.10.0
func (a *Auth) AdminOpenAPIInfo(cfg HTTPConfig) AdminOpenAPIInfo
AdminOpenAPIInfo is the document this mount describes, read off the HTTPConfig the adapter was mounted with and the stores this Auth holds.
The eight flags come from adminFeatures rather than from the stores directly, so the document and the console's own tabs cannot come to disagree: a deployment whose SPA hides the Tenants tab does not publish tenant endpoints, and a PR that turns a feature on turns it on in both places at once.
The one place the mapping is not one word for one: hasSettings is the reference's `!!options.settingsStore` and this port's adminFeatures calls that flag Control, which is the SPA's name for the same tab.
func (*Auth) Authenticate ¶ added in v0.5.0
Authenticate delegates to Service.Authenticate: Me without the custom-claim enrichment, which is what the adapters' Middleware calls.
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) IDP ¶ added in v0.6.0
IDP reports the Identity Provider WithIDP configured, or nil. The adapters consult it to decide whether to mount the JWKS route and where, and to walk OIDCMounts for the four OIDC endpoints.
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) JWKSHandler ¶ added in v0.6.0
JWKSHandler returns the handler serving the IdP's JWKS document with the reference's headers, or nil when no WithIDP was given: the same handler the adapters mount at <prefix><IDPConfig.JWKSPath>. See (*IDP).JWKSHandler.
A host mounting it itself must keep it public and ahead of any middleware, as the reference does (auth.router.ts:473-474): a relying party fetches the document with no credential of any kind.
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) ResolveSiteURL ¶ added in v0.4.0
ResolveSiteURL is the reference's resolveSiteUrl (auth.router.ts:233-246) for one request: the Origin header when it is allowlisted, else the origin of the Referer when that is, else the default site URL. The result is always either an allowlist entry or the default, which is what keeps a caller from steering a link — or an OAuth redirect — at an origin of its choosing.
The allowlist is buildAllowedOrigins (auth.router.ts:213-219): Config.SiteURLs followed by OAuthWiring.AllowedOrigins — the port's spelling of the reference's cors.origins for this purpose — deduplicated with the first occurrence's position kept. With an empty allowlist the headers are not consulted at all and the default is returned.
The default is the first Config.SiteURLs entry (getDefaultSiteUrl, auth.router.ts:202-206), unless OAuthWiring.SiteURL is set: that field predates SiteURLs, is documented as the same first siteUrl entry, and stays the override so that a deployment configured through it keeps its links and redirects. With neither the default is "".
It shares resolveSiteURL with OAuthBegin and LinkRequest, so an emailed link and an OAuth redirect resolve identically for the same request.
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) ToolsHandler ¶ added in v0.10.0
func (a *Auth) ToolsHandler(cfg HTTPConfig) http.Handler
ToolsHandler serves the whole tools router, dispatched on the path below the mount.
It is one handler rather than a mount per route for the reason AdminHandler and UIHandler are: the reference mounts one router, three of its six route groups carry a path parameter, and a catch-all beside fixed siblings is not a shape every Go router will register — gin's tree refuses it outright. The four adapters mount this at <ToolsPath> and <ToolsPath>/, and U23 through U25 add their routes to the switch below rather than to four mount functions.
The shape a later route takes is the one the three routes below share: the handler built once outside the closure — the guard chain and anything it captures must not be rebuilt per request — behind ToolsProtectMiddleware where the reference spreads ...protect, left nil when its feature flag is off, and matched on the nil in the case itself, so that the flag is read once and the switch carries no second copy of it. The flag belongs in that nil and not inside the case body: a switched-off group has to fall through to the default, because the reference does not register its route at all. Anything the switch does not recognise is that 404, which is what an Express router with no matching layer ends at.
A route carrying a path parameter matches on the parameter rather than on rel. rel is the decoded path, and a parameter carrying %2F is one segment to Express and two to it; toolsPathParam is the matcher, and U25's inbound webhook will want it too.
The receiver is what the stream reads its SseManager through, and U25's inbound webhook will resolve the enabled action set through Config.Settings, which this Auth already holds — the reason ToolsOptions carries no settingsStore of its own.
func (*Auth) TwoFactorPolicy ¶ added in v0.2.0
TwoFactorPolicy reports whether the deployment requires a second factor of every user, which is what POST <prefix>/2fa/disable refuses on with 403 2FA_REQUIRED. It is the OR of two terms:
- Config.Require2FA, the static deployment-time switch. It is also a login term (Service.requiresTwoFactor), and it is this port's own: the reference has no config-level require2FA anywhere. That is a wire difference, and it is registered as config-require2fa-is-a-system-policy-term in compatibility.go rather than stated only here.
- AuthSettings.Require2FA read from Config.Settings, the run-time switch an administrator flips through the admin Control panel. This is the reference's own term and the only setting it reads on this route (auth.router.ts:890-896).
The static term is checked first and short-circuits, so a deployment that has already decided the answer never pays for a store read.
The error is the store failing. It is reported rather than swallowed so that the route can fail closed — a settings store that cannot be reached must not be read as permission to turn the factor off — and the adapters answer the reference generic 500, which is what its handleError does with anything its getSettings throws (auth.router.ts:189-195, the catch at :899-901).
It is also logged through Config.Logger before being returned, because the 500 the adapters write carries no detail at all — the reference logs the error itself before answering the same body (console.error in handleError, :193) — and a policy-store outage that leaves nothing in the operator's log is indistinguishable from any other 500. A caller that handles the error itself gets one log line it did not ask for, which is the cheaper of the two mistakes.
The signature grew the context and the error when the settings store landed. It was TwoFactorPolicy() bool while the answer came from Config alone, when there was nothing to read and nothing to fail; Go has no overloading, so the old spelling could only be kept under a different name that nothing would call, and it was dropped instead. The CHANGELOG records the break.
func (*Auth) UIConfig ¶ added in v0.7.0
UIConfig builds the document GET <prefix>/ui/config serves (ui.router.ts: 95-170). The adapters call it; a host serving its own UI route may call it too.
r supplies the lang query parameter and nothing else, and may be nil. cfg is the wire configuration the adapter was mounted with: its Prefix is the document's apiPrefix and its UI block is the branding, the language default and the headless flag.
There is no error return, because the route has no error path: a settings store or template store that fails is caught and answered with the fallback document (:143-161), exactly as the reference's catch does. A store failure is therefore invisible to the client beyond the shorter features object, which is why it is logged through Config.Logger on the way past.
func (*Auth) UIHandler ¶ added in v0.9.0
UIHandler returns the handler for the built-in UI: the whole of the reference's ui.router.ts, mounted at <prefix>/ui.
cfg is the wire configuration the adapter was mounted with; it supplies the mount prefix the handler strips to find the router-relative path, the branding the injection reads, and UIOptions.Headless and UIOptions.Uploads.
configRoute serves the router-relative /config route — the reference's first route (ui.router.ts:165-170), which this port mounted two releases ago and which this handler therefore delegates to rather than reimplements. The four adapters pass (*nethttp.Adapter).UIConfigHandler(). Passing nil leaves <prefix>/ui/config to the catch-all, which would render login.html at it — so nil is correct only for a host that has registered a more specific route for it on its own router, and never for a host that mounts this handler as the whole UI surface.
Assets come from UIOptions.Assets, or from the vendored copies (UpstreamUIAssetFS) when a host configures none — the reference's own "if not provided, the internal Vanilla JS UI will be served" (ui.router.ts:11-14, :72-93). The reference searches six candidate directories on disk to find those internal assets; there is nothing to search here, because they are compiled in.
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 AuthCode ¶ added in v0.4.0
type AuthCode struct {
CodeHash string
UserID string
TenantID string
ClientID string
Nonce string
RedirectURI string
CodeChallenge string
CodeChallengeMethod string
Scope string
ExpiresAt time.Time
}
AuthCode is one OIDC authorization code as the IDP stores it between /authorize and /token. CodeHash is hashToken of the value the client was given; the clear-text code is never persisted. RedirectURI is the canonical entry from IDPClient.RedirectURIs that the authorization request matched. CodeChallenge, CodeChallengeMethod and Scope are recorded from the authorization request as plain data: this package does not verify them yet, but carrying them now means a store written today needs no schema change when /token starts to.
type AuthCodeStore ¶ added in v0.4.0
type AuthCodeStore interface {
SaveCode(ctx context.Context, code AuthCode) error
ConsumeCode(ctx context.Context, codeHash string) (AuthCode, error)
}
AuthCodeStore persists OIDC authorization codes for the IDP.
Design note. The IDP endpoints are net-new relative to the TypeScript reference, which ships no authorization server, so there is no wire contract to cite; the shape below is chosen for the runtimes this library targets.
- The store is keyed by hashToken(code), never by the code itself. The client carries a 24-byte random token in the redirect; the store sees only its SHA-256, so a dump of the backing table cannot be redeemed at /token. This matches how refresh tokens, reset tokens and magic links are already held.
- ConsumeCode is the only read and it is destructive: it returns a record exactly once and answers ErrInvalidCode for every later call with the same hash, including concurrent ones. That is the single-use guarantee /token relies on. It used to come from a process-local map, which holds only while /authorize and /token run in the same process; a serverless or multi-instance deployment must supply a shared implementation via IDPConfig.Codes, and its ConsumeCode must be atomic (a conditional delete, a row lock, or a DEL-and-check pipeline, not a read followed by a delete).
- An expired record is absent: ConsumeCode returns ErrInvalidCode for it and is free to delete it. Implementations may also sweep expired rows on SaveCode or on a schedule. The handler re-checks ExpiresAt regardless.
- SaveCode overwrites an existing record with the same CodeHash. The codes are random, so in practice this never happens; the rule exists so an implementation need not detect the collision.
- Errors other than ErrInvalidCode are surfaced by /authorize and /token as plain 500s; the store is not expected to map them.
type AuthSettings ¶ added in v0.6.0
type AuthSettings struct {
// RequireEmailVerification is the legacy boolean form of
// EmailVerificationMode (settings-store.interface.ts:47). Stored only: see
// the file comment, and Config.EmailVerificationMode for the knob that does
// decide a login.
RequireEmailVerification *bool `json:"requireEmailVerification,omitempty"`
// EmailVerificationMode is none, lazy or strict
// (settings-store.interface.ts:55), and overrides
// RequireEmailVerification when both are set. Stored only.
EmailVerificationMode *string `json:"emailVerificationMode,omitempty"`
// LazyEmailVerificationGracePeriodDays is the lazy-mode grace period in days
// (settings-store.interface.ts:62), 7 when unset. Stored only; the reference
// admin UI displays it and nothing enforces it. Note the naming drift in the
// reference itself: its admin OpenAPI calls the same key
// emailVerificationGracePeriodDays (openapi.ts:1278). The interface name is
// the one used here, because it is the one the admin UI writes.
LazyEmailVerificationGracePeriodDays *int `json:"lazyEmailVerificationGracePeriodDays,omitempty"`
// Require2FA is the one setting this library acts on: when true,
// POST <prefix>/2fa/disable answers 403 2FA_REQUIRED
// (auth.router.ts:890-896). Auth.TwoFactorPolicy is where it is read.
Require2FA *bool `json:"require2FA,omitempty"`
// EnabledWebhookActions is the global allowlist of inbound-webhook action
// ids (settings-store.interface.ts:74), the circuit breaker above each
// webhook own allowedActions list. Stored only until the tools router lands.
//
// nil and empty are different values here, as they are everywhere else in
// this type: nil is absent and keeps what is stored, an empty non-nil slice
// is the administrator switching every action off. MarshalJSON is what keeps
// that difference alive through an encoder.
EnabledWebhookActions []string `json:"enabledWebhookActions,omitempty"`
// UI is the branding block. It is one field of the patch, so a patch that
// carries it replaces the stored block whole rather than merging into it:
// see MergeSettings.
UI *UISettings `json:"ui,omitempty"`
}
AuthSettings is the reference's AuthSettings (settings-store.interface.ts:45-92), in both roles it plays there: the whole stored value that GetSettings returns, and the Partial<AuthSettings> patch that UpdateSettings applies. Every field is optional in the reference, so every field here is a pointer or a slice, and a nil one means absent — which on the way in means keep what is stored, and on the way out means the store never held it.
The JSON names are the reference's, so a stored document round-trips between this port and a node-auth deployment unchanged. Round-tripping the *cleared* state of enabledWebhookActions needs one correction that struct tags cannot express on their own: see MarshalJSON.
func MergeSettings ¶ added in v0.6.0
func MergeSettings(current, patch AuthSettings) AuthSettings
MergeSettings applies patch over current and returns the result, the shallow spread the reference's ISettingsStore.updateSettings contract requires: merged = {...current, ...settings} (settings-store.interface.ts:20-24 is the JSDoc example, :36-38 the interface comment, "Settings not present in the input are left untouched").
The contract is all the reference's src/ carries: it ships no ISettingsStore implementation there (src/stores/ holds only memory-template.store.ts). The implementations live beside it and all spread the same way — the closest to this one is examples/in-memory-user-store.ts:266-268, this.settings = { ...this.settings, ...updates }.
Shallow is the operative word, and the one place it bites is UI. A patch whose UI is nil keeps the stored branding; a patch that carries a UI replaces the stored branding whole, so the seven fields it does not name are dropped, not preserved. That is what the spread does, and the reference depends on it: its admin PATCH route reads the current settings, merges the UI sub-object itself and sends the merged block back down (admin.router.ts:979-981), which it would have no reason to do if the store merged it. A caller that wants to change one colour does the same — read, merge, write.
EnabledWebhookActions follows the same rule as a whole value: nil keeps, and a non-nil slice replaces, an empty one included. Everything the result points at is freshly allocated, so neither input can be mutated through it.
func (AuthSettings) MarshalJSON ¶ added in v0.6.0
func (s AuthSettings) MarshalJSON() ([]byte, error)
MarshalJSON encodes the settings under the reference's names, with one correction encoding/json cannot make from a struct tag.
omitempty on a []string drops an empty slice as well as a nil one, so a plain tag would encode AuthSettings{EnabledWebhookActions: []string{}} as {} — and the cleared list, which is how an administrator switches every inbound-webhook action off, would come back from a store that persists JSON as absent, which MergeSettings reads as keep. The list would quietly come back on. The reference has no such hole: JSON.stringify({enabledWebhookActions: []}) emits [].
So the field is encoded through a pointer, where omitempty means nil and nothing else: nil stays absent, and a non-nil empty slice is written as []. Decoding needs no counterpart, because encoding/json already decodes [] into a non-nil empty slice.
type AuthTokens ¶
AuthTokens are the issued access/refresh pair.
type AuthTools ¶ added in v0.10.0
type AuthTools struct {
// Events is the bus, the reference's readonly eventBus (auth-tools.ts:161).
// Never nil: NewAuthTools refuses one.
Events *EventBus
// SSE is the stream manager, the reference's readonly sseManager
// (auth-tools.ts:162). Nil unless AuthToolsOptions.SSE was set, and every
// read of it is guarded, because nil is the reference's default and the
// common case.
SSE *SseManager
// contains filtered or unexported fields
}
AuthTools is the facade the tools router is built on: the reference's AuthTools (auth-tools.ts:160-360).
It is the one place four sinks are fed from one call. Track takes an event name, a payload and six identifiers and turns them into four different shapes — a TelemetryEvent for the store, an Event for the bus, a StreamEvent for the stream, an OutgoingWebhookEvent for the subscriptions — in an order that is observable and is therefore the source's, not this port's. Notify is the other half: one payload, one topic, and up to three transports.
What a deployment gets by default ¶
Silence, for the library's own events.
This is the decision worth reading before anything else here, because it is the one place where porting the reference faithfully is not obviously the same thing as porting it usefully. There, AuthTools is fed by a host calling track and by nothing else: the routers publish onto the bus and stop, and no part of the package subscribes the bus back into the telemetry store, the stream or the webhooks (auth-tools.ts:199-269 against node-auth auth.router.ts:418-433). Here the service layer publishes nineteen identity.* events of its own that no track call produced, so a host who builds an AuthTools with a webhook store and turns SSE on has, by the reference's own wiring, configured four sinks that the library's own events never reach. Bridge is the one call that changes that, and nothing calls it for the host.
Four things decided it, in the order they mattered:
- Subscribing by default would deliver every tracked event twice. Track publishes on the bus at step 2 and then broadcasts and fires webhooks at steps 3 and 4. A facade that also subscribed to the bus would hear its own step 2 and run steps 3 and 4 a second time, with a second event id — so the per-connection deduplication, which compares ids, could not collapse the pair and every subscription would receive two POSTs. The only way to break that loop inside the library is to mark the events Track publishes so the subscription can skip them, which means a hidden field on Event that exists to let one type ignore another type's messages. The hazard is not removed by making the subscription automatic; it is only made invisible.
- A default that sends is a default that sends to third parties. An outgoing webhook leaves the deployment. Between a default that stays quiet until asked and a default that starts POSTing account events to whatever URLs a store happens to hold, the recoverable mistake is the first one: silence is fixed by adding a line, delivery is not undone by removing one.
- U19 drew this line first and said why. WebhookEmitter.Subscribe is a call a host makes rather than something a constructor does, so that a host who wants this facade's ordering later has nothing to switch off. Redrawing that here would strand the deployments that took it at its word.
- It is what the reference does. An AuthTools that subscribed would be a behavioural difference to register, and the register is for differences that buy something the reference's behaviour cannot.
The cost is the monitoring gap, and it is real: a deployment can believe it is receiving login failures and not be. That is paid for with one line and a cancel, and Bridge is written so the line is obvious.
The loop, for a host that calls both ¶
Bridge and Track fan out through the same code. A host that calls Bridge and also calls Track on the same bus therefore gets the doubled delivery described above for the events it tracks, because both paths handle them. The two are alternatives per event name, not layers: Bridge is for the nineteen the library raises, Track is for a host's own. In practice they do not overlap — the library's are the identity.* set in event_names.go and a host's are its own — but a host that does track an identity.* name should call one or the other for it and not both.
func NewAuthTools ¶ added in v0.10.0
NewAuthTools builds the facade: the reference's constructor (auth-tools.ts:170-189).
ctx bounds the SSE manager's distributor subscription and nothing else, so a deployment that cancels it stops cross-instance delivery and leaves Track and Notify working on this process's own connections. It is a parameter because NewSseManager takes one; the reference's constructor needs no equivalent because its SseManager's subscription is torn down by process exit alone.
A nil bus is refused rather than accepted as "no bus". The reference's first constructor parameter is required and untyped-optional is not available to it, and the reasoning WithEventBus already wrote applies here with more force: every one of the four sinks is silent when it is misconfigured, so a facade that accepted nil would fail in exactly the way that takes longest to notice.
The reference's last constructor act is SseNotifyRegistry.setManager (auth-tools.ts:186-188), a process-global slot that its @sseNotify method decorator reads (sse-notify.decorator.ts:37-54). It has no counterpart here and needs none: Go has no method decorators, the registry exists only to give one a manager to reach, and a package-level mutable pointer to the most recently constructed AuthTools is a thing this package should not grow. A host that wants the decorator's effect holds its AuthTools and calls Notify.
func (*AuthTools) Bridge ¶ added in v0.10.0
Bridge runs the library's own publications through the same fan-out Track uses, and returns the function that stops doing so.
This is the opt-in described at length on AuthTools: nothing calls it, and until a host does, the nineteen identity.* events the service layer raises reach the bus and stop. Calling it is the whole of the wiring —
tools, err := auth.NewAuthTools(ctx, bus, auth.AuthToolsOptions{
Telemetry: store, Webhooks: hooks, SSE: true,
})
stop := tools.Bridge(ctx)
defer stop()
— and it replaces the two lower-level calls a host would otherwise write, WebhookEmitter.Subscribe and SseManager.BridgeEventBus, with one that also persists the telemetry record, applies EventTopics rather than a topic rule invented at the call site, and gives a bridged event the same id, the same frame shape and the same envelope a tracked one gets. Those two remain for a host that wants one sink and not the others.
The bus step is skipped, and only that one: the event being handled arrived *from* the bus, and republishing it would feed this subscription its own output forever. The other three run in the order Track runs them.
ctx is the bridge's lifetime and is handed to each sink. It carries no request, so nothing is filled from it — a bus event already has the provenance PublishContext put there when the service published it.
func (*AuthTools) Notify ¶ added in v0.10.0
Notify delivers one payload to one topic and, optionally, to the user's inbox and handset: the reference's notify (auth-tools.ts:292-342).
The SSE channel broadcasts to target and to target alone — this is the one place in the package that does not fan out across EventTopics, because a notification is addressed to a channel the caller names rather than derived from an event's identifiers. The frame carries no id and no timestamp of its own, so Broadcast fills both (auth-tools.ts:297-303).
The email and SMS channels are reached only when a user id is given and a user store is configured, and then only for a user the store returns and who has the contact detail that channel needs (auth-tools.ts:307, :321, :333). A lookup that fails is a user that is absent — the reference catches it into null (:310-314) — so neither channel is attempted and the SSE broadcast that already happened stands.
Failure isolation ¶
As with Track, nothing reaches the caller: the reference's notify returns a promise that its own route does not await (tools.router.ts:170) and whose channel failures are caught individually (:329, :338). Each transport send runs on its own goroutine with the caller's values and without its deadline — context.WithoutCancel, for the reason WebhookEmitter.Emit gives: a delivery cancelled when the response was written is a delivery that never happened.
data is any ¶
Unlike Track's, which becomes Event.Data. This payload becomes a frame's Data and an encoded message body, both of which accept anything JSON accepts, so the reference's generic parameter (auth-tools.ts:292) survives the port intact.
func (*AuthTools) Track ¶ added in v0.10.0
func (t *AuthTools) Track(ctx context.Context, eventName string, data map[string]any, opts TrackOptions)
Track persists, publishes, broadcasts and delivers one event: the reference's track (auth-tools.ts:199-269).
The order ¶
Telemetry, bus, SSE, webhooks — the source's order (auth-tools.ts:216, :221, :233, :249), reproduced because it is observable rather than because it is tidy. A bus subscriber that reads the telemetry store sees the record for the event it is handling, because the save happened first; a subscriber that writes to the store races the broadcast rather than preceding it. Reordering these four would be a silent change to what a subscriber can assume.
What each sink receives ¶
One id and one instant are generated here and shared, which is what makes the four shapes describe a single event rather than four events that happened at almost the same time (auth-tools.ts:200-201):
- The telemetry store gets a TelemetryEvent carrying the id, the six identifiers and the payload under Meta. Success and Error are left zero: they are this port's own fields, the reference's telemetry record has no counterpart for either (telemetry-store.interface.ts:4-25), and a tracked event is a thing that happened rather than a thing that succeeded. A host querying telemetry should not read Success on a record Track wrote.
- The bus gets the Event, which is the payload plus the six identifiers and nothing else new.
- The stream gets a StreamEvent whose Data is the *whole telemetry record*, not the payload — see the frame note below.
- Each matching subscription gets the OutgoingWebhookEvent that Event.OutgoingWebhook builds, whose metadata is four identifiers and deliberately not the IP or the user agent.
The frame carries the record, not the payload ¶
U20 left this open and the source settles it: the StreamEvent's data is `telemetryEvent` (auth-tools.ts:240), the same object step 1 persisted, not the caller's `data`. So a client parsing a frame reads the payload at `rawData.data` and not at `rawData`, and gets the correlation id, the session id, the IP and the user agent alongside it. That is a larger frame than the payload alone and it is the right one: the stream is the live view of the telemetry the store is accumulating, and a client that can see an event but not which request caused it cannot line the two up. It also settles what the SSE step does *not* do — there is no projection to invent and keep in step with the record.
The record is spelled on the wire with the reference's key names, which is what trackedRecord exists for. TelemetryEvent's Go field names are not the reference's JSON keys, and a frame carrying `EventName` and `Meta` where the reference carries `event` and `data` would be a different wire shape for no reason.
Failure isolation ¶
Nothing a sink does can fail this call, and nothing a sink does can stop another sink. That is the source's behaviour reproduced sink by sink, and Track returns nothing because after reproducing it there is nothing left to report:
- Telemetry. The save is awaited, so a slow store delays the other three, and its error is discarded — `.catch(() => {})` (auth-tools.ts:218). The event is still published, broadcast and delivered.
- The bus. The reference publishes unguarded (auth-tools.ts:231), so a throwing subscriber there skips the SSE and webhook steps entirely and fails whatever called track. Here it cannot: EventBus.Publish recovers each handler, which is the deviation event-handler-panic-does-not-fail-the-publisher, already registered by U18. This step inherits that containment rather than adding one.
- SSE. Broadcast returns nothing in both trees and falls back to local delivery when a distributor fails, so a stream that reaches nobody is not an error anyone hears about (auth-tools.ts:245).
- Webhooks. A store lookup failure is no webhooks rather than a failure — `.catch(() => [])` (auth-tools.ts:251) — and each delivery is fire-and-forget with its own error discarded (:266). WebhookEmitter.Emit already reproduces both.
AuthToolsOptions.OnError hears about every one of these that produced an error value. It changes nothing about the flow.
data is map[string]any ¶
The reference's is `unknown` (auth-tools.ts:199). This narrowing is the one Event.Data already carries, for the reasons written there, and Track cannot widen it: the payload it is handed becomes Event.Data at step 2. A caller with a scalar wraps it in a map and names it, which is what every publication point in both trees does anyway.
ctx fills what the caller left empty. The reference's track never reads a request — its route reads the two headers and passes them as options (tools.router.ts:144-145, :154-155) — and a route here may do exactly that. One that does not gets the provenance an adapter captured, because Event.WithRequestContext fills only empty fields and so an explicit option always wins. Both spellings produce the same event.
type AuthToolsOptions ¶ added in v0.10.0
type AuthToolsOptions struct {
// Telemetry persists every tracked event. Nil is "do not persist": the
// reference's optional telemetryStore (auth-tools.ts:20).
Telemetry TelemetryStore
// Webhooks holds the outgoing subscriptions. Nil is "no outgoing
// webhooks": the reference's optional webhookStore (auth-tools.ts:26).
Webhooks WebhookStore
// WebhookVersion is OutgoingWebhookEvent.Version. Empty means
// OutgoingWebhookVersion, which is the reference's `?? '1'`
// (auth-tools.ts:175).
WebhookVersion string
// WebhookSender delivers. Nil means a zero WebhookSender, which POSTs over
// HTTP in process — the reference's `new WebhookSender()`
// (auth-tools.ts:174), which likewise takes no arguments.
//
// It is here because U19 made WebhookDeliverer the transport seam, and a
// facade that constructed the sender privately would put that seam out of
// reach of every host that uses the facade — which, from U22 on, is the
// ordinary way to use this package. Setting it is how a deployment queues
// deliveries rather than sending them inline.
WebhookSender *WebhookSender
// SSE enables the stream. False is the reference's default
// (auth-tools.ts:33) and leaves AuthTools.SSE nil, which makes both the SSE
// step of Track and the `sse` channel of Notify do nothing at all.
SSE bool
// SSEOptions are forwarded to NewSseManager. Ignored when SSE is false, as
// the reference's sseOptions are (auth-tools.ts:176).
SSEOptions []SseOption
// Users resolves the recipient of an email or SMS notification. Required
// for those two channels of Notify and unused by everything else: the
// reference's userStore (auth-tools.ts:60).
Users UserStore
// Mail is the transport the `email` channel of Notify sends on. Nil is
// the reference's absent notificationService.hasEmail (auth-tools.ts:321).
Mail MailerTransport
// SMS is the transport the `sms` channel of Notify sends on. Nil is the
// reference's absent notificationService.hasSms (auth-tools.ts:333).
SMS SMSTransport
// OnError, when set, is called for every failure this facade swallows: a
// telemetry write that failed, a notification that could not be encoded or
// delivered, and — forwarded from WebhookEmitter.OnError — a webhook store
// lookup or delivery that failed.
//
// Nil is the default and nil is the reference, which swallows all of them
// silently (auth-tools.ts:218, :251, :266, :329, :338). The hook is how a
// deployment buys the observability back without this package inventing a
// log line the reference never writes. It may be called from a delivery
// goroutine and must be safe for concurrent use; it must not block, because
// nothing is waiting on it.
OnError func(err error)
}
AuthToolsOptions configures AuthTools: the reference's AuthToolsOptions (auth-tools.ts:15-82).
Every field is optional and every one of them off is the zero value, which is the reference's "all capabilities are optional and have zero overhead when disabled" (auth-tools.ts:140-141) said in Go.
Three of the reference's fields are not here, and each is absent because this port already has the seam they exist to reach:
- `sseOptions` is SSEOptions below, a slice of the SseOption functions NewSseManager already takes, rather than a struct of three settings that would have to be kept in step with it.
- `emailConfig` and `smsConfig` are transport *configuration* there, from which the constructor builds a NotificationService (notification.service.ts:99-100). Here they are Mail and SMS, the transports themselves. That is the shape this package already uses for every other message it sends — MailerTransport and SMSTransport are what the six ready-made senders are built on — and taking the interface rather than a config struct means a host that already configured a mailer for password resets passes the same value again instead of a second copy of its endpoint and key.
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 ClaimsWebhook ¶ added in v0.5.0
type ClaimsWebhook struct {
// URL is the absolute http(s) address every request goes to.
URL string
// Secret keys the signature. Empty sends no X-Webhook-Signature header at
// all, as the reference's sender does without one (webhook-sender.ts:31-33).
// It is never sent and never appears in an error.
Secret string
// Timeout bounds one request, connect to last byte, through the request
// context — a shorter deadline already on ctx wins. Zero or negative means
// 2 seconds: a login is waiting on the answer.
Timeout time.Duration
// Client issues the requests. Nil means http.DefaultClient. A Client with a
// Timeout of its own is bounded by both.
Client *http.Client
}
ClaimsWebhook is a TokenClaimsBuilder that asks an HTTP endpoint for the claims: the consumer's security.jwt.claimsWebhook {url, timeoutMs} knob (config-schema.md §1.1, §3.1), the escape hatch for claims that are computed rather than mapped. It is this port's own extension, like the helpers in claims.go: the reference's only hook is the in-process config.buildTokenPayload(user) (auth.router.ts:378-384), which never crosses HTTP, so the request and response below have no reference wire to cite. The envelope is the family's outbound-webhook convention — the four X-Webhook-* headers and the "sha256=<hex>" HMAC form of the reference's WebhookSender (src/tools/webhook-sender.ts:24-33, :54-56) — so a receiver written for the tools router's webhooks verifies these the same way.
Every mint — login, refresh, the 2FA step-up token — and every GET /me POSTs one request to URL with Content-Type: application/json, the body
{"user": <the user as GET /me renders it — PublicUser, credentials omitted>}
and the headers
X-Webhook-Event: claims.build X-Webhook-Delivery: <a fresh UUID per request> X-Webhook-Timestamp: <ISO 8601 UTC with milliseconds> X-Webhook-Signature: sha256=<hex HMAC-SHA256 of the body> (only when Secret is set)
and expects a 2xx answer whose JSON body is
{"claims": {<claim name>: <value>, ...}}
The claims object becomes the builder's result and is merged as any hook's is: it may override the six base claims and cannot set the reserved session claims — issueToken writes sid, tid, jti, typ, iss, iat and exp after the merge — so an endpoint that is compromised, or merely confused, cannot retype a token or rebind its session. Anything else is an error: a non-2xx status, a transport failure, the timeout, a body over 64 KiB, a body that is not JSON, a missing or null "claims" member or one that is not an object. An error fails the mint, and the route answers 500 exactly as it does for any failing Config.BuildTokenClaims (token.go). Failing closed is the point: a token minted without the claims the deployment configured would authorise less, or more, than the deployment decided.
A builder runs on every mint and on GET /me, and only there: the adapters' Middleware authenticates through Service.Authenticate, which never calls it. One login is therefore one request here, and a protected route is none.
The zero value is usable once URL is set; NewClaimsWebhook additionally validates the URL and fills in the default timeout. A ClaimsWebhook is safe for concurrent use once its fields stop changing.
func NewClaimsWebhook ¶ added in v0.5.0
func NewClaimsWebhook(rawURL, secret string) (*ClaimsWebhook, error)
NewClaimsWebhook builds a ClaimsWebhook for an absolute http(s) URL with the default 2 second timeout. secret may be empty, in which case requests are unsigned.
func (*ClaimsWebhook) Build ¶ added in v0.5.0
Build posts user to the endpoint and returns the claims it answers with. Its signature is TokenClaimsBuilder's, so a *ClaimsWebhook is wired with WithTokenClaimsBuilder(hook.Build) or as one link of a ChainClaims.
An error carries the status or the transport failure and nothing from the response body, which an endpoint may well fill with an echo of the request.
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
// TwoFactorAppName is the issuer an authenticator app files a TOTP
// enrolment under: the `issuer` of the otpauth:// URI POST <prefix>/2fa/setup
// returns, and the prefix of its label. It is the reference's
// `twoFactor.appName` (auth-config.model.ts:272-274, read at
// auth.router.ts:830).
//
// Empty means unset, and unset means Config.Issuer — which is what the URI
// carried before this field existed, so enrolments made then and enrolments
// made now sit under the same name in users' apps. The reference falls back
// to the literal 'awesome-node-auth' instead; that difference is registered
// as the deviation totp-issuer-defaults-to-config-issuer. WithTwoFactorAppName
// sets it.
TwoFactorAppName string
// BuildTokenClaims adds claims to every token minted — access, refresh and
// the 2FA step-up token — and fills CustomClaims on the profile Me returns.
// Its result is spread over the six base claims (sub, email, role,
// loginProvider, isEmailVerified, isTotpEnabled) and may override them, as
// the reference's buildTokenPayload may (auth.router.ts:378-384). The
// session claims sid, tid, jti, typ, iss, iat and exp are reserved: they
// are written after the merge, so a hook value under one of those names is
// discarded rather than minted (see issueToken for why).
//
// It runs at mint time and on Me, and nowhere else: Authenticate, which the
// adapters' Middleware calls on every protected request, never runs it. A
// hook that cannot answer fails the mint (the route answers 500) and is
// merely logged on Me. StaticClaims, UserFieldClaims, ChainClaims and
// ClaimsWebhook build one from configuration rather than code.
BuildTokenClaims TokenClaimsBuilder
// PasswordVerifier is the migration seam: the hook POST <prefix>/login
// consults for a user whose stored hash did not verify the supplied
// password, so that a deployment moving off another identity provider can
// accept that provider's password on the first login, adopt it as a local
// bcrypt hash and stop calling the old system.
//
// It is an additive port extension with no counterpart in the reference,
// whose login path verifies bcrypt directly and offers no hook
// (local.strategy.ts:19-29). Nil, the default, is the reference's behaviour
// exactly: the verifier is never called, and every login answers as it did
// before this field existed. WithPasswordVerifier sets it. See
// password_verifier.go.
PasswordVerifier PasswordVerifier
// 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
// SendEmailChanged is the reference's config.email.sendEmailChanged
// (auth-config.model.ts:252-257): the notice POST <prefix>/change-email/confirm
// mails to the OLD address once the change is applied (auth.router.ts:
// 1060-1066). Optional; nil is silence, as for the three above. A failure is
// the reference's generic 500 with the change already committed — see
// Service.ConfirmEmailChange.
SendEmailChanged EmailChangedSender
// Templates is the reference's config.templateStore
// (auth-config.model.ts:400, read by mailer.service.ts:146): a TemplateStore whose
// templates override the built-ins for every ready-made mailer the service
// calls, and which will hold the UI translations once the UI router reads
// them. Optional; nil renders the built-ins. WithTemplateStore sets it.
Templates TemplateStore
// Uploads is where the admin console's four upload routes write, and where
// the built-in UI's <prefix>/ui/assets/logo/ and <prefix>/ui/assets/uploads/
// mounts read back from: the seam over the reference's uploadDir
// (admin.router.ts:135, read at :991).
//
// Optional, and nil is the whole feature turned off rather than a degraded
// one — the four routes are not registered, so they answer 404 rather than
// 401, and the console's features.upload is false so the SPA draws no file
// picker. There is deliberately no default: see UploadStore.
//
// It is on Config rather than in HTTPConfig.Admin for the reason Templates
// and Settings are — it is a store, the reference carries it in the same
// options bag as the other stores, and both the admin router and the UI
// router read it. WithUploadStore sets it.
Uploads UploadStore
// Settings is the reference's routerOptions.settingsStore
// (auth.router.ts:92, read at :890-896): the global switches an
// administrator flips at run time. Optional; nil means the settings check is
// skipped altogether, as it is in the reference without a store.
//
// One setting reaches this library: AuthSettings.Require2FA, which
// Auth.TwoFactorPolicy ORs with Require2FA above and
// POST <prefix>/2fa/disable refuses on. The email-verification settings are
// stored and are deliberately not consulted at login, because the reference
// does not consult them either — see settings_store.go and README_DETAILED,
// "Runtime settings". WithSettingsStore sets it.
Settings SettingsStore
// Events is the bus every identity.* event is raised on: the reference's
// routerOptions.eventBus (node-auth auth.router.ts:418-420, where
// publishRouterEvent takes it and returns early when it is undefined).
//
// Optional, and nil is the common case. A deployment that subscribes to
// nothing configures nothing, and Service.publish then does no work at all —
// not even building the payload, which is why the publication sites pass a
// builder rather than a value. See Service.publish.
//
// It sits on Config rather than being a ServiceOption for the reason
// Settings does: the reference carries it in the same options bag as the
// stores, a host that builds its Config by hand must be able to reach it,
// and NewWithConfig is the constructor that takes the struct whole.
// WithEventBus sets it.
Events *EventBus
// SiteURLs is the reference's config.email.siteUrl, which may be one string
// or an array of them (auth-config.model.ts). Two things are read off it:
//
// - The first entry is the canonical base URL for an emailed link
// (getDefaultSiteUrl, auth.router.ts:202-206): what a link points at when
// the request that asked for it carries no allowlisted Origin or Referer.
// - Every entry, together with OAuthWiring.AllowedOrigins, forms the origin
// allowlist Auth.ResolveSiteURL matches a request against
// (buildAllowedOrigins, auth.router.ts:213-219), so a deployment serving
// several front ends mails each one a link back to itself.
//
// Entries are matched exactly, as the reference's `includes` does: no trailing
// slash normalisation, no case folding. Optional; with none configured the
// adapters hand the ready-made mailers no per-request base and their static
// BaseURL applies, as it did before this field existed. WithSiteURLs sets it.
SiteURLs []string
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
// Lang is copied onto EmailChangedDelivery for the notice to the old
// address. The adapters leave it empty: the reference's route reads only
// token from the body and calls sendEmailChanged without a language
// (auth.router.ts:1046, 1062, 1065), so the notice renders in the mailer's
// default. It is here for a caller driving the service directly.
Lang 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 DeliveryWebhook ¶ added in v0.4.0
type DeliveryWebhook struct {
// URL is the absolute http(s) address every request goes to.
URL string
// Secret keys the signature. Empty sends no X-Webhook-Signature header at
// all, as the reference's sender does without one (webhook-sender.ts:31-33).
// It is never sent.
Secret string
// Timeout bounds one request, connect to last byte, through the request
// context — a shorter deadline already on ctx wins. Zero or negative means
// 5 seconds.
Timeout time.Duration
// Client issues the requests. Nil means http.DefaultClient. A Client with a
// Timeout of its own is bounded by both.
Client *http.Client
}
DeliveryWebhook is a sender for every delivery kind at once. Its five Send methods have the five sender signatures, so one value serves every seam:
hook, err := auth.NewDeliveryWebhook("https://hooks.example.com/auth-delivery", secret)
a, err := auth.New(
auth.WithMagicLinkSender(hook.SendMagicLink),
auth.WithPasswordResetSender(hook.SendPasswordReset),
auth.WithEmailVerificationSender(hook.SendEmailVerification),
auth.WithEmailChangeSender(hook.SendEmailChange),
auth.WithSMSCodeSender(hook.SendSMSCode),
)
Each call POSTs one request to URL with Content-Type: application/json, the body {"kind": "<kind>", "delivery": {…}} (DeliveryWebhookRequest), and the headers
X-Webhook-Event: delivery.<kind> X-Webhook-Delivery: <a fresh UUID per request> X-Webhook-Timestamp: <ISO 8601 UTC with milliseconds> X-Webhook-Signature: sha256=<hex HMAC-SHA256 of the body> (only when Secret is set)
Any 2xx answer is delivered. Anything else, a transport failure or the timeout is an error, which the route the sender is wired to handles per its existing contract. The error names the kind, the status and, for mail, the recipient's domain; it never carries the token, the code, the full address or the secret, because an error travels further than the request does — it is logged, and it is host code's to inspect.
The zero value is usable once URL is set; NewDeliveryWebhook additionally validates the URL and fills in the default timeout.
func NewDeliveryWebhook ¶ added in v0.4.0
func NewDeliveryWebhook(rawURL, secret string) (*DeliveryWebhook, error)
NewDeliveryWebhook builds a DeliveryWebhook for an absolute http(s) URL with the default 5 second timeout. secret may be empty, in which case requests are unsigned.
func (*DeliveryWebhook) SendEmailChange ¶ added in v0.4.0
func (w *DeliveryWebhook) SendEmailChange(ctx context.Context, delivery EmailChangeDelivery) error
SendEmailChange posts an email-change delivery, addressed to the new mailbox. Its signature is EmailChangeSender's.
func (*DeliveryWebhook) SendEmailVerification ¶ added in v0.4.0
func (w *DeliveryWebhook) SendEmailVerification(ctx context.Context, delivery EmailVerificationDelivery) error
SendEmailVerification posts an email-verification delivery. Its signature is EmailVerificationSender's.
func (*DeliveryWebhook) SendMagicLink ¶ added in v0.4.0
func (w *DeliveryWebhook) SendMagicLink(ctx context.Context, delivery MagicLinkDelivery) error
SendMagicLink posts a magic-link delivery. Its signature is MagicLinkSender's.
func (*DeliveryWebhook) SendPasswordReset ¶ added in v0.4.0
func (w *DeliveryWebhook) SendPasswordReset(ctx context.Context, delivery PasswordResetDelivery) error
SendPasswordReset posts a password-reset delivery. Its signature is PasswordResetSender's.
func (*DeliveryWebhook) SendSMSCode ¶ added in v0.4.0
func (w *DeliveryWebhook) SendSMSCode(ctx context.Context, delivery SMSCodeDelivery) error
SendSMSCode posts an SMS-code delivery. Its signature is SMSCodeSender's.
type DeliveryWebhookRequest ¶ added in v0.4.0
type DeliveryWebhookRequest struct {
Kind string `json:"kind"`
Delivery json.RawMessage `json:"delivery"`
}
DeliveryWebhookRequest is the body DeliveryWebhook posts. Delivery is the delivery struct that matches Kind — MagicLinkDelivery for DeliveryKindMagicLink, PasswordResetDelivery for DeliveryKindPasswordReset, EmailVerificationDelivery, EmailChangeDelivery, SMSCodeDelivery — encoded with its JSON tags, so a receiver written in Go decodes Kind, switches on it and unmarshals Delivery into the very type this package sent.
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 DocsOptions ¶ added in v0.7.0
type DocsOptions struct {
// Enabled mounts GET <prefix>/openapi.json and GET <prefix>/docs, neither
// of them guarded, as the reference registers them
// (auth.router.ts:1656-1677). Unset — the default — neither route is
// registered at all, and both answer 404.
//
// Enabling the second of the two puts a remote script on the auth origin:
// the page is the reference's, and it loads swagger-ui-dist@5 from the
// unpkg CDN with no subresource integrity (see swaggerUIHTML). Whatever
// unpkg serves then runs same-origin with the auth cookies, where the CSRF
// cookie is readable from JavaScript by design. Keep the routes off in
// production, or serve them behind a Content-Security-Policy that pins that
// CDN — the deviation register's docs-routes-are-opt-in note says the same.
//
// It is a plain bool. The reference's option is boolean or the string
// "auto", "auto" is the default, and it means enabled unless the process
// environment says NODE_ENV=production (auth.router.ts:123-131, :1652-1654).
// This port resolves nothing against an environment: a library whose routes
// appear and disappear with a variable it never sees configured is a
// library nobody can reason about from its own configuration, and Go has no
// NODE_ENV to read anyway. Deciding what the ambient environment means is
// the host's call, and it is one line where the HTTPConfig is built:
//
// cfg.Docs.Enabled = os.Getenv("APP_ENV") != "production"
//
// The consequence — off by default where the reference is on by default
// outside production — is the docs-routes-are-opt-in deviation.
Enabled bool
// BasePath is the prefix the served document writes its paths under, and
// the base of the spec URL the Swagger UI page fetches. Empty means the
// mount prefix, HTTPConfig.Prefix(), and is normalised the way that is, so
// a base path and a mount built from the same string agree.
//
// It is the reference's swaggerBasePath (auth.router.ts:133-139, read at
// :1657), which defaults to the configured apiPrefix there too. The
// reference needs it more than this port does: an Express router cannot
// know where app.use mounted it, while an adapter here is mounted at the
// prefix it was configured with. Set it when the routes are reachable from
// outside under some other path — behind a reverse proxy that adds or
// strips a segment — so that the document and the page point at the URL a
// reader can actually fetch.
//
// It moves the description, never the mount: the two routes are always
// served at HTTPConfig.Prefix(), because that is where the adapter is.
BasePath string
}
DocsOptions is HTTPConfig.Docs: whether the adapters mount the two documentation routes, and under which base path the document describes itself.
type EmailChangeDelivery ¶ added in v0.3.0
type EmailChangeDelivery struct {
UserID string `json:"userId"`
TenantID string `json:"tenantId"`
// NewEmail is the pending address the message goes to.
NewEmail string `json:"newEmail"`
// Token is the plaintext token POST <prefix>/change-email/confirm accepts.
Token string `json:"token"`
ExpiresAt time.Time `json:"expiresAt"`
// LinkBase and Lang are what PasswordResetDelivery's are
// (auth.router.ts:1025-1028).
LinkBase string `json:"linkBase,omitempty"`
Lang string `json:"lang,omitempty"`
}
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 notice the reference mails the *old* address once the change is applied (config.email.sendEmailChanged from /change-email/confirm, auth.router.ts:1060-1066) is a different delivery: EmailChangedDelivery, handed to Config.SendEmailChanged.
type EmailChangeMailer ¶ added in v0.3.0
type EmailChangeMailer struct{ TokenMailer }
EmailChangeMailer is the ready-made EmailChangeSender. It mails the pending address the verify-email template (TemplateVerifyEmail) under the change-email confirmation link, which is exactly what the reference does: /change-email/request has no template of its own and calls sendVerificationEmail with its own link (auth.router.ts:1027-1032, mailer.service.ts:200-203). The 0.3.x email_change template is an alias of the same id now (see templateAliases); a deployment that wants different wording for this mail registers it under TemplateVerifyEmail on a separate templater, or stores an override.
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 EmailChangedDelivery ¶ added in v0.4.0
type EmailChangedDelivery struct {
UserID string `json:"userId"`
TenantID string `json:"tenantId"`
// OldEmail is the address the notice goes to: the one the account had
// until the change was applied (auth.router.ts:1056, 1062).
OldEmail string `json:"oldEmail"`
// NewEmail is the address the account has now (:1057, 1062).
NewEmail string `json:"newEmail"`
// Lang is ConfirmEmailChangeInput.Lang, copied through untouched. The
// routes leave it empty — the reference calls sendEmailChanged with no
// language (:1062, 1065) — so EmailChangedMailer renders in its Locale.
Lang string `json:"lang"`
}
EmailChangedDelivery is what an EmailChangedSender is handed: the notice that POST <prefix>/change-email/confirm sends to the address an account just moved away from (auth.router.ts:1060-1066, "Send notification to old address").
OldEmail is the recipient; NewEmail is what the notice reports. There is no token and no link — this mail confirms nothing, it informs — which is why it carries neither. The JSON tags are the reference's argument names in camelCase, so a sender that forwards the delivery as JSON does so in the family's spelling.
type EmailChangedMailer ¶ added in v0.4.0
type EmailChangedMailer struct {
// Transport delivers the rendered message. Required.
Transport MailerTransport
// Locale is what TokenMailer.Locale is. The routes hand this mailer no
// language at all (EmailChangedDelivery.Lang), so it is usually the rule
// here rather than the default.
Locale string
// Templates renders subject and bodies. NewEmailChangedMailer fills this in.
Templates *MailTemplater
}
EmailChangedMailer is the ready-made EmailChangedSender: it renders the email-changed template (TemplateEmailChanged) with the new address as its data and mails it to the old one, as the reference's sendEmailChanged does (mailer.service.ts:205-208). It has no BaseURL because the notice carries no link.
auth.WithEmailChangedSender(auth.NewEmailChangedMailer(mailer, "Example App").Send)
func NewEmailChangedMailer ¶ added in v0.4.0
func NewEmailChangedMailer(transport MailerTransport, appName string) *EmailChangedMailer
NewEmailChangedMailer builds an EmailChangedMailer with the built-in templates.
func (*EmailChangedMailer) Send ¶ added in v0.4.0
func (m *EmailChangedMailer) Send(ctx context.Context, delivery EmailChangedDelivery) error
Send renders and delivers the notice to the old address. Its signature is EmailChangedSender's.
type EmailChangedSender ¶ added in v0.4.0
type EmailChangedSender func(ctx context.Context, delivery EmailChangedDelivery) error
EmailChangedSender delivers the email-changed notice to the old address.
An error fails POST <prefix>/change-email/confirm with the reference's generic 500 — its sendEmailChanged call sits inside the route's try block, after the change has been written (auth.router.ts:1058-1066, handleError at :1068-1069) — so the address has moved even though the response says 500. The failure contract is EmailVerificationSender's; the difference is what has already happened when it fires.
type EmailVerificationDelivery ¶ added in v0.3.0
type EmailVerificationDelivery struct {
UserID string `json:"userId"`
TenantID string `json:"tenantId"`
Email string `json:"email"`
// Token is the plaintext token GET <prefix>/verify-email accepts.
Token string `json:"token"`
ExpiresAt time.Time `json:"expiresAt"`
// LinkBase and Lang are what PasswordResetDelivery's are
// (auth.router.ts:941, 954-957).
LinkBase string `json:"linkBase,omitempty"`
Lang string `json:"lang,omitempty"`
}
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 verify-email template (TemplateVerifyEmail) as the reference's sendVerificationEmail does (mailer.service.ts:200-203). 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
// SessionID is the session the event concerns, when there is one. Empty at
// the sites where there is not: a failed login has no session, and neither
// does an OAuth conflict.
SessionID string
// CorrelationID, IP and UserAgent are the request provenance. They are
// filled by WithRequestContext from the carrier the adapters install, not
// by the publisher, which in this port has no *http.Request to read. See
// EventContext.
CorrelationID string
IP string
UserAgent string
}
Event is one identity event as it travels the bus: the reference's AuthEventPayload (auth-event-bus.ts:6-25).
The nine fields are the reference's nine, and each of the four this port did not have is justified by something that reads it rather than by symmetry with the source:
- SessionID. The reference's telemetry record carries it (telemetry-store.interface.ts:18) and its outgoing webhook envelope puts it in metadata (auth-tools.ts:260); eight of the dev line's twenty-six publication points pass one, always the session issueTokens just minted. Without it a subscriber cannot tell two concurrent logins by one user apart, which is the one thing a session-management screen needs.
- CorrelationID. Read from the request and nothing else — see EventContext. Telemetry stores it (telemetry-store.interface.ts:20), the webhook envelope carries it (auth-tools.ts:261), and it is the only field that joins an event to the request that caused it across service boundaries.
- IP and UserAgent. Telemetry stores both (telemetry-store.interface.ts:22-24) and the SSE frame carries them indirectly, because the dev line broadcasts the whole telemetry record as the frame's data rather than a projection of it (auth-tools.ts:250-252, against StreamEvent at sse-manager.ts:8-25). The webhook envelope deliberately does not: its metadata is the four identifiers and not these two, which is a privacy line the port keeps — see WebhookMetadata.
Timestamp stays a time.Time although the reference's is an ISO 8601 string (auth-event-bus.ts:11), because this type never crosses the wire. The two things that do — OutgoingWebhookEvent.Timestamp and the SSE frame — hold strings already, and the reason they do is written on OutgoingWebhookEvent.
Data stays map[string]any although the reference's is `unknown` (auth-event-bus.ts:12). Nothing is lost: all twenty-six of the dev line's publication points pass an object literal, never a scalar, an array or null — the widest of them is the OAuth conflict site, which spreads an error's own data under a provider key (node-auth auth.router.ts:1480) and is still an object. Keeping the narrower type means Event.Data is assignable to OutgoingWebhookEvent.Data and to TelemetryEvent.Meta with no conversion and no runtime type check, and a publisher cannot pass a shape a JSON consumer would receive as a bare string. Event.OutgoingWebhook is what relies on that.
The field order is the order this struct already had, with the four new fields appended. The reference's order is different (event, timestamp, data, userId, …) and matching it would reorder fields for no one: nothing in this package or in the reference constructs an Event positionally.
func (Event) OutgoingWebhook ¶ added in v0.10.0
func (e Event) OutgoingWebhook(version string) OutgoingWebhookEvent
OutgoingWebhook returns the envelope an outgoing webhook carries for this event: the object the reference's track builds once per event and sends to every matching subscription (auth-tools.ts:266-273).
version is OutgoingWebhookEvent.Version, the reference's AuthToolsOptions.webhookVersion; empty means OutgoingWebhookVersion.
The four members are the reference's four. Data is the event's data or JSON null — the reference writes `data ?? null` (:269), and a nil Event.Data reaches the wire as null rather than as {} or as an absent key, which is what leaving the `any` nil does. Metadata is Event.WebhookMetadata(), which is where the four-key object and the decision to leave IP and UserAgent out of it already live; building it here a second time is exactly what that method exists to prevent.
A zero Timestamp is filled with the current instant, for the reason EventBus.Publish fills one: an envelope with a zero time would put year 1 on the wire, and every event the reference sends is stamped at the moment track ran.
func (Event) WebhookMetadata ¶ added in v0.9.0
WebhookMetadata returns the `metadata` object of an outgoing webhook envelope for this event: OutgoingWebhookEvent.Metadata, built the way the reference builds it (auth-tools.ts:257-262).
It exists so that the webhook envelope WebhookSender sends is derived from the event rather than assembled a second time beside it — Event.OutgoingWebhook is its one caller. OutgoingWebhookEvent already holds Metadata as a map[string]any, and the alternative — a second struct with the same four identifiers — would be the thing that drifts.
Four keys, spelled as the reference spells them (camelCase, matching OutgoingWebhookEvent's own JSON tags), and an absent one is omitted rather than written empty: the reference builds the object with all four members and lets JSON.stringify drop the undefined ones, so a receiver sees only the keys that had a value. That is the reasoning already written on OutgoingWebhookEvent.Metadata, and this method is its producer.
An event with none of the four returns nil, which that field's `omitempty` renders as no metadata key at all, where the reference would emit `"metadata":{}`. The difference is unreachable rather than resolved: no publication point in either tree produces such an event — every one of the twenty-six passes a user id, or is an OAuth conflict, which carries the correlation id the request supplied. Returning nil for the empty case is cheaper than special-casing a state nothing produces.
IP and UserAgent are deliberately not here. The reference's metadata is these four and not those two (auth-tools.ts:257-262), and the asymmetry is the point: a webhook leaves the deployment, and the client address and browser string of an end user are the two fields on this struct that should not follow it to a third party by default. Telemetry keeps both because it stays inside.
func (Event) WithRequestContext ¶ added in v0.9.0
WithRequestContext returns ev with its request-provenance fields filled from the EventContext an adapter installed on ctx, and returns ev unchanged when ctx carries none.
This is the port of the spread in the dev line's publishRouterEvent, which builds every payload as `{...getRequestEventContext(req), ...payload}` (node-auth auth.router.ts:430-433, and the byte-identical publishAdminEvent at node-auth admin.router.ts:245-249). There the request context is the base and the caller's payload is spread over it, so the caller wins a collision; here a non-empty field on ev wins and an empty one is filled. The two rules agree everywhere the reference can reach, because the payload parameter's type admits only data, userId, tenantId and sessionId (node-auth auth.router.ts:422-427) — it has no correlationId, ip or userAgent key to collide with. "Fill what is empty" is the rule that survives a caller who sets one of the three deliberately, which the spread would silently discard.
Nothing here reads the user from ctx. ContextWithUser is on the same context and carries a User with an ID, and it is tempting to default Event.UserID from it; the reference does not, and the difference is observable — the dev line's logout site publishes the user id it read from the refresh token, not from the authenticated principal, and its failed-login site publishes no user id at all even though an unauthenticated request has none to offer either.
type EventBus ¶
type EventBus struct {
// contains filtered or unexported fields
}
EventBus is the in-process pub/sub every downstream consumer of the event plane sits on: the reference's AuthEventBus (auth-event-bus.ts:48-82), which is a subclass of Node's EventEmitter.
The two were compared axis by axis when the vocabulary landed, because a delivery-semantics mismatch discovered by the SSE or webhook work would be expensive and one discovered here is not. They agree on four of the five axes that matter:
- Publish is synchronous. EventEmitter.emit calls its listeners inline and returns when the last one returns; publish emits twice and returns (auth-event-bus.ts:61-63). Publish below does the same. Neither defers to a queue, so a handler that blocks blocks the publisher — which for U18 means a slow subscriber delays the HTTP response, exactly as it does in the reference.
- Ordering within a name is registration order, in both.
- The named channel is served before the wildcard, in both: the reference emits eventName and then '*' (auth-event-bus.ts:61-63), and Publish below runs the named handlers and then the wildcard ones.
- Both dispatch against a snapshot of the handler list. EventEmitter clones its listener array before iterating; Publish copies under the read lock and releases it before calling. So in both, a handler that subscribes during a dispatch is not called for the event being dispatched, and — for the same reason — a Subscribe from inside a handler cannot deadlock here.
They disagree on the fifth. A handler that throws propagates out of EventEmitter.emit, so in the reference it skips every later handler and surfaces in whatever published — for the dev line's router that is the route handler's own catch, which answers the request 500 for work that already succeeded. Here a panicking handler is recovered, logged and stepped over. That is the deviation registered as event-handler-panic-does-not-fail-the-publisher, and safeCall is where it lives.
One piece of the reference's surface is ported by shape rather than by signature. AuthEventBus exposes offEvent (auth-event-bus.ts:79-81), which takes the same listener reference that was registered and removes it. Go functions are not comparable, so `Unsubscribe(name, handler)` cannot be written at all, and the Go shape is a cancel function returned from Subscribe — the thing the caller already holds, rather than a value it has to keep and hand back. It arrived with the first consumer that needed it, the SSE manager in U20, whose per-connection subscriptions are the only ones in the plan that end before the process does.
func NewEventBus ¶
func NewEventBus() *EventBus
func (*EventBus) Publish ¶
Publish delivers ev to the handlers registered for ev.Name and then to the wildcard handlers, synchronously, in registration order.
A zero Timestamp is filled with the current time, which is the reference's `payload.timestamp ?? new Date().toISOString()` (auth-event-bus.ts:59): a caller that has a better instant than "now" may say so, and one that does not gets the publish instant.
The library itself calls this from nowhere. Closing that — raising an event at each of the twenty-six places the dev line raises one — is U18.
func (*EventBus) PublishContext ¶ added in v0.9.0
PublishContext publishes ev with its request-provenance fields filled from ctx: Publish(ev.WithRequestContext(ctx)).
This is the method a service publishes through, and it is the whole reason the carrier in event_context.go exists. The reference reaches the request where it publishes — publishRouterEvent takes the *Request and reads the three headers at publish time (node-auth auth.router.ts:418-434) — and a Service method here takes a context.Context and nothing else. Splitting the read (an adapter, once per request) from the use (a publisher, once per event) is what bridges that, and this method is the use.
A ctx with no EventContext on it publishes an event with the three fields empty, which is what a publish from outside any request — a background sweep, a host calling a Service method from a cron job — should produce. It is not an error and is not logged: the reference's equivalent is a publish through AuthTools.track with no metadata options (auth-tools.ts:245), which is likewise silent.
func (*EventBus) Subscribe ¶
Subscribe registers handler for one event name, or for EventBusWildcard to receive every event. It is the reference's onEvent (auth-event-bus.ts:72-74).
The returned function cancels this one subscription and is the port of offEvent (auth-event-bus.ts:79-81) — see the comment on EventBus for why the shape differs. It is idempotent: calling it twice, or after the bus has been dropped, is a no-op.
Cancelling does not reach a dispatch already in flight. Publish copies the handler list under the read lock and releases it before calling anything, so a handler cancelled while an event is being delivered may still be called for that event — exactly as EventEmitter, which clones its listener array before emitting, still calls a listener removed by an earlier one. A handler that must not run after its cancel returns has to guard itself.
A caller with nothing to cancel may discard the result; that is why adding it leaves every existing call site compiling unchanged.
type EventContext ¶ added in v0.9.0
type EventContext struct {
// CorrelationID is the caller's own trace handle, read from
// CorrelationIDHeader and never generated. The reference does not mint one
// when the header is absent (node-auth auth.router.ts:407-408, :412) and
// neither does this: an id this library invented would join nothing to
// anything, and a consumer that saw one would have no way to tell it from
// an id the caller's gateway assigned.
CorrelationID string
// IP is the client address. What that means behind a proxy is
// HTTPConfig.ClientIP's decision; see EventContextFromRequest.
IP string
// UserAgent is the User-Agent header value, or empty.
UserAgent string
}
EventContext is the per-request provenance every published Event carries: the return type of the reference's getRequestEventContext (node-auth auth.router.ts:402-406).
Three fields and no more. It is deliberately not a general-purpose request bag: a second thing that wants to ride the request context should get its own carrier and its own key, so that neither can quietly start depending on the other's lifetime.
func EventContextFromContext ¶ added in v0.9.0
func EventContextFromContext(ctx context.Context) (EventContext, bool)
EventContextFromContext reads the request provenance a middleware installed.
The bool distinguishes "no carrier" from "a carrier with three empty fields", which are different facts: the first says nobody installed one — a publish from a background job, or an adapter that was not wired — and the second says a request arrived with no correlation header, no User-Agent and an unresolvable address. Event.WithRequestContext treats them the same, because the fields it would copy are empty either way; a host writing its own diagnostics may not want to.
func EventContextFromRequest ¶ added in v0.9.0
func EventContextFromRequest(r *http.Request, cfg HTTPConfig) EventContext
EventContextFromRequest reads the request provenance off r, resolving the client address through cfg.
The two header reads are the reference's, quirks included. The reference tests each header for an array and takes the first element when it finds one (node-auth auth.router.ts:407-410) — Node hands an array for a header sent more than once, and a bare string otherwise — so a client that sends X-Correlation-Id twice is read as having sent the first one.
Go stores repeated headers as a slice too, and Header.Get already returns the first entry, so Get would produce the identical value. Header.Values with an explicit index is used anyway, because the rule being reproduced is the reference's array-first choice and not Go's documented default: if Get's behaviour is ever what a reader checks this against, they will have checked the wrong thing.
The address is HTTPConfig.ClientIP when the host set one, and the socket peer otherwise; see that field for why the choice is the host's.
type ForgotPasswordInput ¶
type ForgotPasswordInput struct {
Email string
TenantID string
// LinkBase is the per-request base the reset link is built under, copied
// onto PasswordResetDelivery.LinkBase. The adapters fill it from
// HTTPConfig.LinkBase(Auth.ResolveSiteURL(r)); a direct caller may leave it
// empty, in which case a ready-made mailer uses its static BaseURL.
LinkBase string
// Lang is the request's emailLang body field, copied onto
// PasswordResetDelivery.Lang untouched (auth.router.ts:779, 788).
Lang string
}
type HTTPConfig ¶ added in v0.2.0
type HTTPConfig struct {
APIPrefix string
Cookies CookieOptions
CSRF CSRFConfig
// UI is the reference's config.ui block (auth-config.model.ts:321-381): the
// built-in UI's branding, language default and headless flag, and the switch
// that mounts it. UIOptions.Enabled is what makes the adapters register
// GET <prefix>/ui/config, and what points an emailed link at a UI page — see
// UILink.
UI UIOptions
// UIEnabled is the deprecated spelling of UI.Enabled, from when the flag
// decided nothing but the shape of an emailed link: when set, UILink points
// a link at <prefix>/ui/<path> — the static UI's page for it — instead of at
// the bare API route (buildUiLink, auth.router.ts:265-266).
//
// It is an alias, not a second switch. Either field being set enables the
// UI, resolve sets both from either, and a configuration that only ever set
// this one keeps the links it had and now serves the config route as well.
//
// Deprecated: set UI.Enabled instead. This field is kept through the 0.x
// line.
UIEnabled bool
// ResourceServer mounts this instance as a resource server: an API that
// verifies tokens another instance issued and owns no credentials of its
// own. The adapters then register none of the routes in
// ResourceServerGatedRoutes, so a credential route answers with whatever
// the router returns for an unknown path — 404 on all four — instead of
// reaching a handler that has no user database behind it.
//
// It changes nothing else: the routes that stay are unaffected, and the
// bearer verification itself is ResourceServerMiddleware, which a host
// mounts on its own routes. The reference gates six of the nineteen
// (auth.router.ts:510, :541); gating all of them is the
// resource-server-gates-all-credential-routes deviation.
ResourceServer bool
// RateLimiter is the reference's RouterOptions.rateLimiter
// (auth.router.ts:46): the slot a host drops its own limiter into. nil —
// the default — means no limiter, which is the reference's own default:
// it collapses the slot to an empty middleware list when the option is
// absent (rl, auth.router.ts:468) and ships no algorithm of its own.
//
// The adapters apply it to every route of the auth router, the routes the
// reference spreads ...rl onto (auth.router.ts:541 onwards), and not to the
// JWKS document, which is registered ahead of the whole chain and carries
// no rl (:490).
//
// One pair of routes falls inside the slot here that falls outside it
// there. When an OAuth provider is not configured the reference registers
// bare 404 stubs in place of the real handlers, with no rl on them
// (:1361-1362 for Google, :1407-1408 for GitHub); this port mounts one
// always-guarded handler per OAuth route and decides on the configuration
// inside it. Behind a limiter at its limit those two requests are therefore
// answered 429 here and 404 there, and they consume budget here that they
// do not consume there.
//
// Position in the chain is the load-bearing part. The reference puts rl
// first on each route, ahead of the auth middleware (GET /me,
// auth.router.ts:656) and therefore ahead of the double-submit check,
// which the auth middleware performs and which the one route without that
// middleware repeats by hand inside its handler (:1489-1495). A refusal
// consequently costs nothing downstream: no token is verified, no store is
// read, no CSRF comparison is made. The adapters reproduce that order —
// RateLimiter outermost, then CSRFMiddleware, then the auth middleware.
//
// The field is a constructor, not a handler, and each adapter calls it once
// per route at mount time — some thirty calls for one mount. Whatever the
// limiter counts with, a bucket or a map or a store handle, therefore has
// to be created outside this function and captured by it, the way
// httprate.LimitByIP and the tollbooth wrappers are written. Allocating the
// counter inside the function gives every route a private budget instead of
// the single shared one the reference gets from passing one Express handler
// instance to every route.
//
// The admin console has a slot of its own and this is not it. The family's
// private development line carries a second, separate option on its admin
// router (AdminOptions.rateLimiter, node-auth admin.router.ts:205-211,
// collapsed to an empty list the same way at :577) and spreads it onto
// exactly one route, POST /users/:id/promote (:1030); its admin login route
// (:614) carries none. That path is relative to the admin router's own
// mount, which the host app chooses and which defaults to /admin
// (admin.router.ts:190, openapi.ts:674) — not to APIPrefix. Those line
// numbers are in that private tree and do not resolve against
// ReferenceRevision, which carries no admin rateLimiter at all. U16 ported
// the route and the slot together: see AdminOptions.RateLimiter. This field
// is untouched by it and covers no admin route.
RateLimiter func(http.Handler) http.Handler
// ClientIP resolves the address an event's IP field records, and it is this
// port's answer to a question the reference does not answer in source.
//
// The reference reads `req.ip || req.socket.remoteAddress` (node-auth
// auth.router.ts:413). The second half is unambiguous — the socket peer —
// and the first is not: Express's req.ip is the socket peer when `trust
// proxy` is unset, and the left-most address of X-Forwarded-For that the
// trust setting does not vouch for when it is. `trust proxy` is set by the
// host application, in code this port cannot see and a deployment can
// change without touching either library. So there is no single value that
// is "what the reference does": there are two, and which one a given
// deployment gets is a fact about the deployment.
//
// Rather than pick one silently, the port makes the seam explicit and
// defaults it to the half that is unambiguous:
//
// - nil, the default, is the socket peer address with its port stripped
// (see HTTPConfig.clientIP). That is `req.socket.remoteAddress`, and it
// is also what `req.ip` evaluates to under Express's own default of
// `trust proxy` unset — so a host that configures nothing here gets
// what a host that configures nothing there gets.
// - A non-nil ClientIP is consulted instead, for every request, and its
// answer is used as given. A deployment behind a load balancer supplies
// the function that reads whichever header its balancer actually sets
// and trusts only the hops it actually operates.
//
// The port ships no X-Forwarded-For parser, and that is deliberate rather
// than unfinished. A parser is only as good as the trust configuration
// behind it, an untrusted one is a spoofable client address in a security
// log, and the trust configuration is the host's knowledge — the same
// reasoning that makes `trust proxy` an application setting in Express.
// Handing the host the function pointer says so; shipping a default parser
// would say the opposite while being wrong on most deployments.
//
// The value reaches an Event through EventContextMiddleware, which the
// adapters install outermost on every auth route. It is read once per
// request, so a ClientIP that consults a store or a CIDR list is called
// once per request and not once per event.
ClientIP func(*http.Request) string
// Docs mounts the two documentation routes the reference's swagger option
// registers: GET <prefix>/openapi.json and GET <prefix>/docs, neither with
// a guard of its own (auth.router.ts:1656-1677), both behind the CSRF
// middleware as every reference route registered after its router-level
// auto-init is (:529-538) — which on a GET distributes the csrf-token
// cookie and rejects nothing. Nothing is mounted unless Docs.Enabled is
// set; see DocsOptions in docs.go for that field and for why it is a bool
// where the reference's option is also the string "auto".
Docs DocsOptions
// Admin mounts the admin console: the reference's createAdminRouter, which
// a host mounts beside the auth router rather than under it
// (admin.router.ts:496 onwards), so these routes sit at Admin.Path and not
// below APIPrefix. Nothing is registered unless HTTPConfig.AdminMounted()
// — Admin.Enabled *and* an access decision — and every admin path answers
// 404 otherwise.
//
// See AdminOptions in admin.go for the four access policies, for why the
// mount is a sibling of the API prefix, for why the surface carries none of
// the auth router's middleware, and for the one configuration this port
// refuses to serve.
Admin AdminOptions
// Tools mounts the tools router: the reference's createToolsRouter, which
// like the admin router is a second router the host mounts beside the auth
// router rather than under it (tools.router.ts:114, :127), so these routes
// sit at Tools.Path and not below APIPrefix. Nothing is registered unless
// HTTPConfig.ToolsMounted() — Tools.Enabled, an AuthTools to serve, *and*
// an access decision — and every tools path answers 404 otherwise.
//
// See ToolsOptions in tools.go for the four feature flags, for the guard
// slot and why this port will not mount the router without one, and for the
// shape each route of U23 through U25 is added into.
Tools ToolsOptions
}
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) AccountConflictLink ¶ added in v0.6.0
func (c HTTPConfig) AccountConflictLink(siteURL, provider, email string) string
AccountConflictLink is the Location the callback answers an account conflict with: the reference's buildUiLink(siteUrl, "/account-conflict?…") (auth.router.ts:261-271, :1352-1354). siteURL is the origin the state resolved to — OAuthCompleteResult.RedirectTo, which is the same resolveOAuthRedirect value the reference passes — and the query is its literal one, provider first, then code, then the address when there is one.
The escaping is Go's url.QueryEscape where the reference calls encodeURIComponent. The two agree on every character an address or a provider name can hold in practice; they differ on a space (+ against %20) and on !~*'(), which QueryEscape percent-encodes and encodeURIComponent leaves alone. Both forms decode to the same string.
func (HTTPConfig) AdminDocsBasePath ¶ added in v0.10.0
func (c HTTPConfig) AdminDocsBasePath() string
AdminDocsBasePath is AdminOptions.Docs.BasePath resolved: the configured value, normalised the way Prefix normalises the API prefix, or the admin mount when none is configured.
It is DocsBasePath's counterpart for this router and the reference's `options.swaggerBasePath ?? '/admin'` (admin.router.ts:1498), which is the same literal AdminOptions.Path defaults to — so the two agree by default and a deployment that moves the mount without moving this one is saying the console is reachable from outside under some other path.
It moves the description, never the mount: the pair is always served below AdminPath(), because that is where the adapter put the handler.
func (HTTPConfig) AdminMounted ¶ added in v0.10.0
func (c HTTPConfig) AdminMounted() bool
AdminMounted reports whether the adapters register the admin surface: the block is enabled *and* an access decision has been configured for it.
The second half is the admin-console-requires-an-explicit-policy deviation. It is a method rather than a field so that the four adapters cannot come to disagree about the condition, and so that a host can assert on it at startup — `if cfg.Admin.Enabled && !cfg.AdminMounted() { log.Fatal(...) }` — which is the one thing that turns an unexpected 404 into a message.
func (HTTPConfig) AdminPath ¶ added in v0.10.0
func (c HTTPConfig) AdminPath() string
AdminPath is AdminOptions.Path resolved: the configured value normalised the way Prefix normalises the API prefix, or DefaultAdminPath when none is set.
func (HTTPConfig) DocsBasePath ¶ added in v0.7.0
func (c HTTPConfig) DocsBasePath() string
DocsBasePath is DocsOptions.BasePath resolved: the configured value, or the mount prefix when none is configured.
func (HTTPConfig) LinkBase ¶ added in v0.4.0
func (c HTTPConfig) LinkBase(siteURL string) string
LinkBase is the base MagicLinkURL, PasswordResetURL, EmailVerificationURL and EmailChangeConfirmURL take, resolved for one site URL: UILink(siteURL, "") with the trailing slash removed, which is what the reference does with that value on the one route that passes an empty path (magic-link.strategy.ts:25-27). "https://app.example.com" becomes "https://app.example.com/auth", or "https://app.example.com/auth/ui" under UIEnabled. The adapters put it on each delivery as LinkBase.
An empty siteURL yields "" rather than the bare prefix: no site URL is known for the request, and a delivery with no LinkBase lets a ready-made mailer fall back to its static BaseURL — which a deployment configured before Config.SiteURLs existed relies on. The reference has no such static base and builds a relative link in that case; the port keeps the link that used to be sent.
func (HTTPConfig) Prefix ¶ added in v0.2.0
func (c HTTPConfig) Prefix() string
Prefix returns the normalised mount prefix.
func (HTTPConfig) ToolsDocsBasePath ¶ added in v0.10.0
func (c HTTPConfig) ToolsDocsBasePath() string
ToolsDocsBasePath is ToolsOptions.Docs.BasePath resolved: the configured value, or the tools mount when none is configured. It is the counterpart of DocsBasePath, and the reference's swaggerBasePath (tools.router.ts:127, read at :342 and :350).
func (HTTPConfig) ToolsMounted ¶ added in v0.10.0
func (c HTTPConfig) ToolsMounted() bool
ToolsMounted reports whether the adapters register the tools router: the block is enabled, a facade was supplied, and an access decision has been made for it.
The third condition is the tools-router-requires-an-explicit-guard-decision deviation. It is a method rather than a field so that the four adapters cannot come to disagree about the condition, and so that a host can assert on it at startup — see the head of this file.
func (HTTPConfig) ToolsOpenAPIInfo ¶ added in v0.10.0
func (c HTTPConfig) ToolsOpenAPIInfo() ToolsOpenAPIInfo
ToolsOpenAPIInfo is the document this mount describes, read off the configuration it was mounted with. It is exported for the same reason (*Adapter).OpenAPIInfo is: a host that serves the document from its own route or writes it to a file at build time needs exactly this value.
func (HTTPConfig) ToolsPath ¶ added in v0.10.0
func (c HTTPConfig) ToolsPath() string
ToolsPath is ToolsOptions.Path resolved: the configured value normalised the way Prefix normalises the API prefix, or DefaultToolsPath when none is set.
func (HTTPConfig) UILink ¶ added in v0.4.0
func (c HTTPConfig) UILink(siteURL, path string) string
UILink is the reference's buildUiLink (auth.router.ts:261-271): the link to path under siteURL, through the static UI when the UI is enabled and straight at the API route otherwise.
<siteURL><prefix>/ui/<path> when UI.Enabled <siteURL><prefix>/<path> otherwise
Either UI.Enabled or the deprecated UIEnabled switches it, so a config built before UIOptions existed keeps the links it had.
prefix is Prefix() — the reference strips one trailing slash from its apiPrefix (:263), which Prefix() already does along with the rest of this port's normalisation — and one leading slash is dropped from path (:264), so UILink("https://app.example.com", "/reset-password?token=t") is "https://app.example.com/auth/reset-password?token=t". siteURL is used as given: an empty one yields a relative link, exactly as the reference does with no siteUrl configured.
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.
*OAuthAccountConflictError is deliberately absent: the reference answers that one with a 302 to /account-conflict, not with an error body (auth.router.ts:1346-1355), so the callback handles it before it gets here and anything else that reaches this function with it has lost the redirect.
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
deprecated
type HTTPMailerTransport struct {
EndpointURL string
Secret string
// contains filtered or unexported fields
}
HTTPMailerTransport sends mail via any HTTP endpoint (no SMTP needed).
Deprecated: it POSTs MailMessage as PascalCase JSON ({"To", "Subject", "Body", "IsHTML"}) under an X-Mailer-Secret header — a request of this port's own that no gateway built for the reference accepts. NewGatewayMailerTransport sends the reference's contract instead. This type keeps its 0.3.0 behaviour unchanged for the gateways built against it and is scheduled for removal in v1.0.0.
func NewHTTPMailerTransport
deprecated
func NewHTTPMailerTransport(endpointURL, secret string) *HTTPMailerTransport
NewHTTPMailerTransport creates a transport that POSTs to a webhook-style HTTP endpoint.
Deprecated: use NewGatewayMailerTransport; see HTTPMailerTransport for why.
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 HTTPWebhookDeliverer ¶ added in v0.10.0
type HTTPWebhookDeliverer struct {
// Client issues the request. Nil means http.DefaultClient. A Client with a
// Timeout of its own is bounded by both.
Client *http.Client
// Timeout bounds one request, connect to last byte, through the request
// context — a shorter deadline already on ctx wins. Zero or negative means
// DefaultWebhookTimeout.
Timeout time.Duration
}
HTTPWebhookDeliverer POSTs the attempt over HTTP, in process, on the calling goroutine. It is the default WebhookDeliverer and it is the reference's own behaviour — the fetch at webhook-sender.ts:36, with a timeout Go needs and the reference does without.
The zero value is usable.
func (*HTTPWebhookDeliverer) DeliverWebhook ¶ added in v0.10.0
func (d *HTTPWebhookDeliverer) DeliverWebhook(ctx context.Context, attempt WebhookAttempt) error
DeliverWebhook posts the attempt. A 2xx is nil; anything else is a *WebhookStatusError, and a transport failure or a timeout is whatever the client returned.
type IDP ¶
type IDP struct {
// contains filtered or unexported fields
}
IDP is an OIDC Identity Provider.
func NewIDP ¶
NewIDP creates a new OIDC IDP backed by the given auth service.
A nil cfg.Signer falls back to an ephemeral RSA-2048 key and logs idpEphemeralKeyWarning once; a signer that is not RSA is refused. A nil cfg.Codes falls back to an in-process MemoryAuthCodeStore; see IDPConfig.Codes for when that is not enough.
func (*IDP) IssueIdPTokenPair ¶ added in v0.5.0
IssueIdPTokenPair mints the reference's RS256 token pair for user: the transcription of generateIdProviderTokenPair (token.service.ts:73-95).
The payload is the reference's buildPayload (auth.router.ts:378-384): the six base claims sub, email, role, loginProvider, isEmailVerified and isTotpEnabled with Config.BuildTokenClaims spread over them, the same object the session tokens start from. iat, exp and kid are then dropped from it (:73) — the first two are set below, kid is a header parameter — and iss is set from IDPConfig.Issuer when that is configured, else left as the hook left it (:74-77). Both tokens carry that payload plus iat and exp, both are RS256 under KeyID (:81-93); they differ only in lifetime — AccessTokenTTL, 30 days by default, and RefreshTokenTTL, 90 days by default. ExpiresIn is the access lifetime.
No session is created and no sid, jti or typ claim is written: the reference pair has none either, so the refresh token is a longer-lived copy of the access token that nothing here can revoke. This is a host-level API exactly as in the reference, where nothing in src/ calls generateIdProviderTokenPair (reference-issues N35): /login, /refresh and the OIDC /token keep issuing the HS256 session pair, and enabling the IDP changes nothing about them.
It lives on IDP rather than Service because everything it needs beyond the user is the IDP's — the signer, the kid, the two lifetimes, the issuer — and the IDP already holds the Service for the claims hook, while a Service knows nothing of any IDP. In the reference all of that is the config.idProvider block, which is IDPConfig here.
func (*IDP) JWKS ¶ added in v0.5.0
JWKS is the document the JWKS endpoint serves: the signer's public key under KeyID first, then IDPConfig.PublicKeys in the order given. The reference builds the same document from its one key (jwks.service.ts:184-186); the extra entries are what a rotation publishes so that tokens signed by the previous key stay verifiable until they expire.
func (*IDP) JWKSHandler ¶ added in v0.6.0
JWKSHandler serves JWKS() with the headers the reference sets on that route: Access-Control-Allow-Origin per JWKSCORSOrigins and "Cache-Control: public, max-age=3600" (auth.router.ts:490-503).
The document is built once, here, and not per request: the reference derives it at router-creation time and closes over it (auth.router.ts:487), and nothing an IDP holds can change it afterwards — the signer and IDPConfig.PublicKeys are both fixed at construction.
The handler is public by construction: no auth, no CSRF, no session. Mounting it is what an adapter does for an Auth built WithIDP; a host that mounts it by hand must keep it ahead of any middleware, as the reference does (auth.router.ts:473-474).
func (*IDP) JWKSPath ¶ added in v0.6.0
JWKSPath reports where the JWKS document is served relative to the mount prefix: IDPConfig.JWKSPath, or DefaultJWKSPath when that is empty. It always starts with "/", because NewIDP refuses a path that does not. The adapters call it to build the route they mount.
func (*IDP) OIDCMounts ¶ added in v0.7.0
OIDCMounts returns the four OIDC endpoints — discovery, authorize, token and userinfo — as the adapters and RegisterHandlers mount them.
It is the one list: every adapter's MountWithConfig walks it, and so does RegisterHandlers, so an endpoint cannot appear on one mount and not another. The JWKS route is not in it — that one is the reference's, configurable through IDPConfig.JWKSPath and GET-only, and the adapters mount it from JWKSPath() and JWKSHandler() for that reason.
Two of the four carry ResourceServerGated, which is the adapters' cue to skip them under HTTPConfig.ResourceServer; the flag travels on the mount rather than splitting the walk in four adapters, so this stays the one list.
These four have no counterpart in the reference, which ships no OIDC authorization server; their shapes are described where the handlers are, and nothing here changes them.
func (*IDP) RegisterHandlers ¶
RegisterHandlers mounts OIDC endpoints on the given mux.
An Auth built with WithIDP makes all four adapters mount the same four endpoints under the adapter's own prefix, from the same OIDCMounts list, so a host that mounts an adapter no longer needs this at all. It stays for the host that serves the OIDC endpoints on a mux of its own — under a different base path, or behind its own middleware — and for the one that uses the IdP without mounting an adapter.
All four are mounted here whatever OIDCMount.ResourceServerGated says. That flag is the adapters' reading of HTTPConfig.ResourceServer, and this method is handed a mux with no HTTPConfig behind it: a host mounting the endpoints itself is choosing the surface itself.
Calling it with the adapter's own prefix, on the mux the adapter was mounted on, is a mistake rather than a way to mount the endpoints twice, and it is a loud one: both registrations use the same method-less pattern, so the second panics at mount time — ServeMux reports the two patterns as conflicting and names both registration sites — the same way the JWKS route already panics when a host registers it twice. The panic is the point. A pattern that did not collide, "GET <path>" against this method-less one, would be accepted as the more specific of the two and would leave the endpoint split between two handlers, GET answered by the adapter's mount and every other method by this one; that silent split is the failure this shape rules out.
The JWKS document is served at basePath + JWKSPath(), the canonical location the adapters mount and the one the discovery document points at. <basePath>/jwks is kept as a deprecated alias of it, serving the identical bytes and headers, for the relying parties configured against the path this package published before v0.6.0; the alias is kept through the 0.x line and removed in v1.0.0. The deprecated thing is that URL path, not this method: the other four endpoints, and the canonical JWKS path, are current.
Both JWKS patterns are registered for GET alone, as the reference registers the route (router.get, auth.router.ts:490) and as the four adapters mount it. net/http serves HEAD from a "GET " pattern itself, which is Express's own fallback from HEAD to the GET handler; any other method misses the route.
type IDPConfig ¶
type IDPConfig struct {
// Issuer is the iss claim of the ID token and the base of the discovery
// document. For IssueIdPTokenPair it is optional, as the reference's
// idProvider.issuer is: left empty, the pair carries no iss claim
// (token.service.ts:74-77).
Issuer string
// AccessTokenTTL is the lifetime of the access token IssueIdPTokenPair
// mints; zero resolves to 30 days, the reference's tokenExpiry default
// (token.service.ts:80). It governs only that pair: the OIDC /token
// endpoint returns the HS256 session access token and reports that
// token's lifetime, Config.AccessTokenTTL, as expires_in.
AccessTokenTTL time.Duration
// IDTokenTTL is the lifetime of the ID token; zero resolves to one hour.
IDTokenTTL time.Duration
// Signer signs every RS256 token the IDP issues and is the key the JWKS
// document publishes first. It must be an RSA key — RS256 is RSASSA-PKCS1
// v1.5 over SHA-256 (RFC 7518 section 3.3) — and NewIDP refuses a signer
// whose Public is not an *rsa.PublicKey. An *rsa.PrivateKey qualifies, so
// the reference's PEM configuration is ParseRSAPrivateKeyPEM(pem) here
// (idProvider.privateKey, token.service.ts:47); the public key the
// reference derives when none is given (:67-69) is Signer.Public. A signer
// that keeps the private key elsewhere — a KMS, an HSM — plugs in the same
// way; BuildRS256JWT says what it is asked to do.
//
// Nil generates an ephemeral RSA-2048 key in NewIDP and logs the
// reference's warning once (token.service.ts:50-66, auth.router.ts:480-483):
// every token becomes unverifiable on restart, and two processes never
// share a key. Development only.
Signer crypto.Signer
// KeyID is the kid written into the header of every signed token and
// published with Signer's key in the JWKS document. Empty resolves to
// DefaultIDPKeyID, the reference's constant.
KeyID string
// PublicKeys are further keys the JWKS document publishes after Signer's,
// unchanged: the previous key during a rotation, so that the tokens it
// signed stay verifiable until they expire, or a sibling issuer's. Nothing
// in the IDP signs with them or checks them; NewRSAJWK builds one from an
// *rsa.PublicKey. The reference publishes exactly one key
// (jwks.service.ts:184-186); this is the structural room a rotation needs.
PublicKeys []JWK
// RefreshTokenTTL is the lifetime of the refresh token IssueIdPTokenPair
// mints; zero resolves to 90 days (token.service.ts:91). The reference
// consults its session refresh lifetime in between (idProvider.refreshTokenExpiry,
// then refreshTokenExpiresIn, then 90d); that middle step is not
// reproduced, because Config.RefreshTokenTTL is never unset here —
// DefaultConfig fills it and validate requires it — so honouring it would
// make the 90-day default unreachable and tie the IdP pair silently to the
// session lifetime.
RefreshTokenTTL time.Duration
// Logger receives the IDP's diagnostics, today only the ephemeral-key
// warning. Nil falls back to the Config.Logger of the Service NewIDP was
// given, and to silence when there is none.
Logger func(format string, args ...any)
// Codes holds authorization codes between /authorize and /token. Nil
// selects NewMemoryAuthCodeStore, which is correct only while both
// endpoints are served by the same process: a serverless runtime or any
// deployment with more than one instance must supply a shared
// implementation, or /token will answer invalid_grant for every code
// minted by a sibling process. See AuthCodeStore for the contract.
Codes AuthCodeStore
// CodeTTL bounds how long a code issued by /authorize can be redeemed at
// /token. Zero (or negative) resolves to defaultAuthCodeTTL, five minutes.
CodeTTL time.Duration
// JWKSPath is where the JWKS document is served, relative to the adapter's
// mount prefix: the reference's idProvider.jwksPath (auth.router.ts:475).
// Empty resolves to DefaultJWKSPath. NewIDP refuses a path that the four
// adapters and RegisterHandlers cannot all route as the same literal path:
// it must start with "/" and must not end with one, must not contain "{",
// "}", "?", "#" or "//", and must not collide with one of the four OIDC
// endpoints, which the adapters and RegisterHandlers both mount
// (OIDCAuthorizePath, OIDCTokenPath, OIDCUserInfoPath, OIDCDiscoveryPath).
//
// An Auth built WithIDP makes every adapter serve GET <prefix><JWKSPath>,
// public and ahead of any middleware, exactly where the reference registers
// it (auth.router.ts:473-475).
JWKSPath string
// JWKSCORSOrigins is the allowlist for the Access-Control-Allow-Origin
// header on the JWKS response: the reference's idProvider.jwksCorsOrigins
// (auth.router.ts:492-500).
//
// Nil is the reference's default, "*". Otherwise the request's Origin is
// echoed back when it appears in the list, and no Access-Control-Allow-Origin
// header is sent at all when it does not — not an empty one, and not a
// refusal: the body is served either way, since CORS bounds what a browser
// script may read, not what the server answers.
//
// The reference's type is `string | string[]`, and only its string form is
// the wildcard: it compares the whole value against "*" (auth.router.ts:493),
// so every array is an allowlist and an entry "*" inside one is an ordinary
// entry, matching only an Origin header of literally "*" — one no browser
// sends. Go has no such union, so the one-element slice {"*"} stands in for
// the string form and is the single configuration that reads differently
// from the reference; see the jwks-cors-wildcard-string-form entry in
// compatibility.go.
//
// Neither the reference nor this port sends Vary: Origin, while both send
// Cache-Control: public, max-age=3600 (auth.router.ts:492-502). An
// allowlisted response must therefore not reach a shared cache, which would
// store one origin's header and replay it to another, or store the
// header-less variant and break the allowlisted origin: front an allowlist
// with a private cache, or leave this nil so every origin may read the
// document.
JWKSCORSOrigins []string
// JWKSURL overrides the jwks_uri of the discovery document RegisterHandlers
// serves. Empty derives it as Issuer + JWKSPath, which is the canonical
// location whenever Issuer carries the mount prefix — the convention every
// other endpoint in that document already follows, since they are all
// derived from Issuer alone. Set it when the IdP is reached through a
// gateway whose external URL is not Issuer + the mounted path.
JWKSURL string
}
IDPConfig configures the OIDC Identity Provider.
type InboundWebhookStore ¶ added in v0.8.0
type InboundWebhookStore interface {
// FindByProvider returns the inbound configuration registered for provider,
// and false when there is none — the reference's WebhookConfig | null, in
// the (value, ok, error) shape TemplateStore already uses for the same
// thing.
//
// Note what it does not filter on: IsActive. FindByEvent's contract is
// "active configurations only" and this one's is not, because the only
// caller tests the result for a jsScript and nothing else
// (tools.router.ts:257-259). Deactivating a webhook stops outgoing
// deliveries; it does not stop an inbound script from running. That is the
// reference's behaviour and it is reproduced rather than corrected, but it
// is a trap worth knowing before M9 wires the route, and a store must not
// quietly fix it by filtering here.
FindByProvider(ctx context.Context, provider string) (WebhookConfig, bool, error)
}
InboundWebhookStore is the reference's findByProvider? (webhook-store.interface.ts:143-149): the lookup POST /tools/webhook/:provider does to find the script it should run. It is its own capability rather than a method of WebhookAdminStore because it answers a different question for a different caller, and because a deployment that never accepts inbound webhooks should be able to say so by not implementing it.
The tools router that calls it is M9, and what it does with the result — running JSScript — this port will not do in process; see WebhookConfig's inbound fields and U25's InboundScriptRunner.
type JWK ¶ added in v0.5.0
type JWK struct {
Kty string `json:"kty"`
Use string `json:"use"`
Alg string `json:"alg"`
Kid string `json:"kid"`
N string `json:"n"`
E string `json:"e"`
}
JWK is one RSA public key as the JWKS endpoint publishes it: the six members the reference's JWK interface has (jwks.service.ts:5-12), under the same JSON names and in the same order. Only signing keys are published, so Kty is always "RSA", Use "sig" and Alg "RS256" for a key NewRSAJWK built; the fields are exported so that a host can carry a key it obtained elsewhere into IDPConfig.PublicKeys unchanged.
func NewRSAJWK ¶ added in v0.5.0
NewRSAJWK converts an RSA public key to the JWK the reference's publicKeyToJwk emits (jwks.service.ts:168-179): kty "RSA", use "sig", alg "RS256", the given kid, and n and e as the unpadded base64url of their big-endian bytes (RFC 7518 section 6.3.1).
A nil key, or one with no modulus, is an error rather than a panic: the key a host feeds IDPConfig.PublicKeys was obtained elsewhere (kms:GetPublicKey, a file), and the failed fetch that leaves it nil should surface as an error at the call site, not as a crash when the document is built.
func (JWK) RSAPublicKey ¶ added in v0.6.0
RSAPublicKey converts a published JWK back into an RSA public key — the inverse of NewRSAJWK, and of the reference jwkToPublicKey (jwks.service.ts:199-202).
Only RSA signing keys are accepted: kty must be RSA, and alg, when the document states one, must be RS256. A key published for another algorithm would otherwise be verified as if it were an RS256 key, which is the algorithm-confusion the alg allow-list exists to prevent.
type JWKS ¶ added in v0.5.0
type JWKS struct {
Keys []JWK `json:"keys"`
}
JWKS is the document the JWKS endpoint serves: {"keys": [...]} (jwks.service.ts:14-16, :184-186).
type JWKSClient ¶ added in v0.6.0
type JWKSClient struct {
// contains filtered or unexported fields
}
JWKSClient is a cached reader of a remote JWKS document.
The cache is stale-while-revalidate, exactly as the reference (jwks.service.ts:49-92):
- a document inside its TTL is returned from memory;
- a fetch already in flight is joined rather than duplicated, so a burst of requests on a cold cache causes one HTTP call;
- a document past its TTL is returned anyway, immediately, and a refresh runs in the background;
- a refresh that fails changes nothing, so the stale document keeps serving — an issuer that is briefly unreachable does not take the resource server down with it.
The zero value is not usable; construct one with NewJWKSClient. A client is safe for concurrent use, and is meant to be long-lived: it is the cache.
func NewJWKSClient ¶ added in v0.6.0
func NewJWKSClient(cfg ResourceServerConfig) *JWKSClient
NewJWKSClient returns a client for cfg.JWKSURL. Only the JWKS fields of cfg are read; Issuer belongs to the verifier.
It panics when JWKSURL is empty or is not an absolute http or https URL. A resource server whose issuer endpoint is misconfigured cannot verify a single bearer token, so the failure belongs at startup, where it is one line in a log, rather than at the first request, where it is a blanket 401 that looks like an authentication problem. The reference throws for the same reason (jwks-auth.middleware.ts:38-40).
func (*JWKSClient) Document ¶ added in v0.6.0
func (c *JWKSClient) Document(ctx context.Context) (JWKS, error)
Document returns the cached JWKS, fetching or refreshing as the stale-while-revalidate rules require. The returned value is a copy down to the key slice, so a caller may sort or filter it freely.
func (*JWKSClient) GetKey ¶ added in v0.6.0
GetKey returns the RSA public key published under kid, fetching or refreshing the document as the cache rules require (jwks.service.ts:95-98).
A document that carries no such key is not an error of the transport, and the two are told apart by the caller: the error wraps errJWKSKeyNotFound, which is what makes VerifyRS256 invalidate and retry once instead of giving up. Both forms wrap ErrInvalidToken.
func (*JWKSClient) Invalidate ¶ added in v0.6.0
func (c *JWKSClient) Invalidate()
Invalidate drops the cached document so the next read fetches fresh keys. It is what the verifier calls when a token names a kid the cache does not carry, which is how a key rotation is picked up before the TTL expires (jwks.service.ts:101-105).
func (*JWKSClient) URL ¶ added in v0.6.0
func (c *JWKSClient) URL() string
URL reports the JWKS endpoint this client reads.
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 `json:"userId"`
TenantID string `json:"tenantId"`
Email string `json:"email"`
// Token is the plaintext token the matching verify route accepts.
Token string `json:"token"`
// 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 `json:"expiresAt"`
// LinkBase is the base the link is built under for this request —
// "https://app.example.com/auth" — resolved by the adapter from the request's
// Origin or Referer against Config.SiteURLs (Auth.ResolveSiteURL, then
// HTTPConfig.LinkBase), which is the reference's siteUrlOverride argument
// (auth.router.ts:1104/1114 → magic-link.strategy.ts:25). It wins over
// MagicLinkMailer.BaseURL when set; empty when no site URL is configured or
// the service was called directly, in which case that static base applies.
LinkBase string `json:"linkBase,omitempty"`
// Lang is the request's emailLang body field, passed through untouched
// (auth.router.ts:1080, 1104/1114). "it" and "en" select the built-in
// template set over MagicLinkMailer.Locale; anything else, the empty string
// included, defers to it (resolveLang, mailer.service.ts:255-259).
Lang string `json:"lang,omitempty"`
}
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.
The JSON tags — on this struct and on the four others a sender is handed — are the shape DeliveryWebhook posts (delivery_webhook.go). Nothing else in this package serialises a delivery.
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 when the delivery
// carries no LinkBase of its own: the static fallback for a deployment with
// no Config.SiteURLs, or for a service called directly. A delivery's
// LinkBase — the base the adapter resolved for the request — wins over it.
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 the default, not the rule: a delivery whose Lang is "it" or "en" —
// the request's emailLang body field — renders in that language instead,
// and any other Lang defers to this (resolveLang, mailer.service.ts:255-259,
// where Locale plays config.defaultLang).
Locale string
// Templates renders subject and bodies. NewMagicLinkMailer fills this in;
// set Templates.Store to render from a TemplateStore outside a service call
// (inside one, Config.Templates is found on its own).
Templates *MailTemplater
}
MagicLinkMailer is the ready-made MagicLinkSender: it renders the magic-link template (TemplateMagicLink — the built-in, or a TemplateStore's override) and hands the result to a MailerTransport, text alternative included. 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 (mailer.service.ts:190-193).
Use it as a sender through its Send method:
mailer, err := auth.NewGatewayMailerTransport(auth.MailerConfig{
Endpoint: endpoint, APIKey: apiKey, From: "noreply@example.com",
})
// handle err
auth.WithMagicLinkSender(auth.NewMagicLinkMailer(
mailer,
"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 what a template mentioning {{.appName}} shows; the built-ins mention none.
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 ¶
type MailMessage struct {
To string
Subject string
// Body is the message: HTML when IsHTML is set, plain text otherwise.
Body string
IsHTML bool
// Text is the plain-text alternative of an HTML Body — the reference's
// `text` payload field (mailer.service.ts:133-141). It is read only when
// IsHTML is set; a plain-text Body already is the text. Left empty, the
// gateway transport sends the HTML in its place, as the reference's
// sendCustom does (`text ?? html`, mailer.service.ts:246).
//
// omitempty keeps the deprecated HTTPMailerTransport's body byte-identical
// for the callers that never set it.
Text string `json:",omitempty"`
}
MailMessage is a single email to be sent.
type MailRendered ¶ added in v0.4.0
MailRendered is a rendered mail: what RenderMail returns and what a ready-made mailer hands its transport as Subject, Body (the HTML) and Text.
type MailTemplate ¶ added in v0.4.0
type MailTemplate struct {
// ID is one of the six reference ids — TemplatePasswordReset and its
// siblings — or any id of the deployment's own.
ID string `json:"id"`
BaseHTML string `json:"baseHtml"`
BaseText string `json:"baseText"`
// Translations is lang -> key -> value. The key "subject" is the mail's
// subject; every other key is what {{T.key}} resolves to.
Translations map[string]map[string]string `json:"translations"`
}
MailTemplate is a stored mail template (template-store.interface.ts:1-6). The JSON tags are the reference's key names.
BaseHTML and BaseText are the bodies, with two kinds of placeholder: {{T.key}} is looked up in Translations for the rendering language, and {{key}} is looked up in the data the mailer supplies — link, token, newEmail and the rest; see MailTemplater.RenderMail for the rule and the keys. A template is only used when both bodies are non-empty (mailer.service.ts:158); until then the built-in one keeps rendering.
Placeholders are substituted as they are, in BaseHTML included: no HTML escaping, as the reference does none (mailer.service.ts:163-167). BaseHTML is the deployment's own markup; the values are not all its own — newEmail is the address a user typed, lowercased and trimmed, and an Extra entry is whatever the caller passed — and a stored template that shows them mails them verbatim, exactly as the reference's does. The built-in templates, rendered by html/template, escape them.
type MailTemplateData ¶
type MailTemplateData struct {
AppName string
UserName string
Token string
URL string
Code string
// NewEmail and OldEmail are the two addresses of the email-changed notice.
NewEmail string
OldEmail string
Extra map[string]any
}
MailTemplateData holds the variables a template is rendered with.
A built-in template, or one added with Register, reads them as Go template fields — {{.link}}, {{.token}} … — and a stored template reads the same keys as {{link}}, {{token}} … (see RenderMail). The keys are the reference's, so a stored template written for it renders here unchanged:
token Token (mailer.service.ts:186, 191, 201)
link URL, for every id but welcome (:186, 191, 201, 211)
loginUrl URL, for welcome only — the name its template reads (:62); the
reference's router passes {loginUrl} there (auth.router.ts:724)
newEmail NewEmail (:206)
… every Extra entry under its own key, formatted with fmt.Sprint;
the welcome template's tempPassword (:63) travels here
appName AppName, the templater's, when it has one (a key of this port's)
name UserName (a key of this port's)
code Code (a key of this port's)
oldEmail OldEmail (a key of this port's)
An empty field is left out rather than set to "", so a stored template naming a key the caller did not supply shows the reference's "[key]" placeholder. The reference never passes appName, name, code or oldEmail, and they are absent here too until set, so a stored template written for it sees exactly the keys it sees there: {link, token} for password-reset, magic-link and verify-email (:186, :191, :201), {link} for invitation (:211), {newEmail} for email-changed (:206), {loginUrl, tempPassword} for welcome (auth.router.ts:724). The ready-made mailers supply what the reference's senders do and nothing else. A field set on the struct wins over an Extra entry of the same key.
type MailTemplatePatch ¶ added in v0.4.0
type MailTemplatePatch struct {
BaseHTML *string
BaseText *string
Translations map[string]map[string]string
}
MailTemplatePatch is what TemplateStore.UpdateMailTemplate applies: the reference's Partial<MailTemplate> (template-store.interface.ts:27), where a field that is present replaces the stored one and a field that is absent leaves it alone — the shallow spread of memory-template.store.ts:22, {...existing, ...template}.
So a nil BaseHTML or BaseText keeps the stored body, and a pointer to "" clears it. A nil Translations keeps every language; a non-nil one replaces the whole lang -> key -> value map — languages it does not name are dropped, as the reference drops them, because translations is one field of the partial and not a map the store merges into. A caller that wants to change one language reads the template first and sends the map back whole.
type MailTemplater ¶
type MailTemplater struct {
// Store is the template store RenderMail consults. Nil means the one the
// service put on the context — Config.Templates, set with WithTemplateStore,
// which reaches every sender the service calls — and, when there is none
// there either, the built-ins only. Set it to render from a store outside a
// service call, or from a store other than the configured one.
Store TemplateStore
// contains filtered or unexported fields
}
MailTemplater renders localized mail templates: the reference's six built-ins in en and it, any the host registers over them, and — when a TemplateStore is reachable — the stored override for an id.
It is the port's MailerService.render (mailer.service.ts:149-181): the store is consulted first and used when it holds both bodies; otherwise the registered template renders. Register is meant for set-up time; RenderMail and Render are safe to call concurrently, with each other and with Register.
func NewMailTemplater ¶
func NewMailTemplater(appName string) *MailTemplater
NewMailTemplater creates a templater with the built-in en and it templates for the six reference ids. appName is what a template mentioning {{.appName}} shows; the built-ins mention none, as the reference's do not, and it is carried as the appName data key for stored and registered ones.
func (*MailTemplater) Register ¶ added in v0.4.0
func (t *MailTemplater) Register(locale, id, subject, html, text string)
Register adds or replaces the template rendered for id in locale. The three parts are Go templates over the keys MailTemplateData lists — {{.link}}, {{.appName}} … — html/template for html, text/template for subject and text. It is what NewMailTemplater fills the built-ins in with, and the way a host replaces one, or adds a locale or an id of its own, without a TemplateStore. An alias id registers under the id it is an alias of.
A part that does not parse is kept and reported by RenderMail for that (locale, id) rather than dropped: a typo must surface, and Register has no error return to surface it through.
func (*MailTemplater) Render ¶
func (t *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 id: RenderMail without a context and without the text body, kept for the callers that want exactly that. The store the service configured is only reachable through the context, so a Render outside a service call sees Store and the built-ins.
func (*MailTemplater) RenderMail ¶ added in v0.4.0
func (t *MailTemplater) RenderMail(ctx context.Context, locale, id string, data MailTemplateData) (MailRendered, error)
RenderMail renders template id in locale: subject, HTML body and text body.
It is the reference's MailerService.render (mailer.service.ts:149-181):
- A stored template is looked up first — through Store, or the store the service put on ctx (Config.Templates) — and used only when both its BaseHTML and BaseText are non-empty (:158). A store error is returned.
- Its translations are those for locale, else for "en", else none (:159). Every {{T.key}} becomes translations[key], or "[key]" when that is missing or empty (:163); then every remaining {{key}} becomes the data value for key — the keys MailTemplateData lists, which for a template written against the reference are exactly the ones it passes — or "[key]" when there is none (:165-168). The subject is translations["subject"], else the registered subject for the language, interpolated the same way (:172-177). Values are substituted as they are, in the HTML body included — no HTML escaping, because the reference does none (:167). A stored BaseHTML is the deployment's own markup, but not every value is: newEmail is the address the user typed on /change-email/request, lowercased and trimmed and nothing more, and an Extra entry is whatever the caller put there. A stored template that shows them mails them verbatim, as the reference's does; the built-ins, rendered by html/template, escape them.
- Otherwise the registered template for locale renders — the built-in one unless Register replaced it — falling back to "en" when locale has none, with html/template's contextual escaping in the HTML body.
locale is used as given; the ready-made mailers pass what resolveLang picked. The four pre-reference ids are accepted as aliases (see templateAliases). An id with neither a usable stored template nor a registered one is an error.
type MailerConfig ¶ added in v0.4.0
type MailerConfig struct {
// Endpoint is the full URL the gateway accepts a POST at
// (auth-config.model.ts:20-21). It must be an absolute http or https URL;
// NewGatewayMailerTransport refuses anything else.
Endpoint string `json:"endpoint"`
// APIKey travels in the X-API-Key header (auth-config.model.ts:22-23).
APIKey string `json:"apiKey"`
// From is the sender address, sent as `from` (auth-config.model.ts:24-25).
From string `json:"from"`
// FromName is the sender's display name, sent as `fromName` when set
// (auth-config.model.ts:26-27).
FromName string `json:"fromName,omitempty"`
// Provider names the gateway-side provider, sent as `provider` when set
// (auth-config.model.ts:28-29).
Provider string `json:"provider,omitempty"`
// DefaultLang is the reference's default template language, "en" or "it"
// (auth-config.model.ts:30-35; read by resolveLang, mailer.service.ts:255-259).
// The transport does not read it: which template set a mail is rendered
// from is the mailers' decision (MagicLinkMailer.Locale and its siblings).
// It is carried so the block decodes as one unit and can be passed on.
DefaultLang string `json:"defaultLang,omitempty"`
// Client makes the request. Nil means a client with a 10-second timeout,
// the same bound HTTPMailerTransport and HTTPSMSTransport use.
Client *http.Client `json:"-"`
}
MailerConfig is the reference's email.mailer block (awesome-node-auth src/models/auth-config.model.ts:19-36): the settings of the mail gateway NewGatewayMailerTransport talks to. The JSON tags are the reference's key names, so a deployment that already holds that block for the reference can decode it and hand it over as is.
type MailerTransport ¶
type MailerTransport interface {
Send(ctx context.Context, msg MailMessage) error
}
MailerTransport delivers email messages.
func NewGatewayMailerTransport ¶ added in v0.4.0
func NewGatewayMailerTransport(cfg MailerConfig) (MailerTransport, error)
NewGatewayMailerTransport builds the transport for the family's mail gateway, the contract the reference's built-in mailer speaks (awesome-node-auth src/services/mailer.service.ts:261-291):
POST {endpoint}
Content-Type: application/json
X-API-Key: {apiKey}
{"to": …, "subject": …, "html": …, "text": …, "from": …, "fromName": …, "provider": …}
Any status outside 2xx is a failure (mailer.service.ts:280-284). The endpoint's own path and query are kept, as the reference keeps them (mailer.service.ts:269).
It fails when cfg.Endpoint is empty or not an absolute http(s) URL, so that a misconfiguration surfaces at start-up rather than as a 500 on the first send. The reference has no such check — `new URL(endpoint)` throws inside send — so the constructor is the port's only addition, and it adds no request.
type MemoryAPIKeyStore ¶ added in v0.8.0
type MemoryAPIKeyStore struct {
// contains filtered or unexported fields
}
MemoryAPIKeyStore is an in-process implementation of APIKeyStore and of all four of its companion interfaces. It is what a deployment gets before it writes a real one, what the examples wire, and what the tests in this package authenticate against.
It is correct only while every request is served by the same process. Keys live in maps and die with the process, so a serverless or multi-instance deployment that used this would issue a key on one instance and reject it on the next; those need a store over shared storage, which is the whole point of APIKeyStore being an interface.
Implementing all five interfaces is deliberate, and it is what makes this type the executable statement of APIKeyAdminStore's ordering rule: the order that doc comment declares normative is asserted here, in memory_api_key_store_test.go, rather than left as prose for the next implementor to interpret. A caller that needs a store which is *missing* one of the optional four — to exercise the reference's `501 IApiKeyStore.listAll is not implemented` branch, say (admin.router.ts:1259-1262) — has to write a narrower type; this one answers every question.
Records are copied on the way in and on the way out. APIKeyRecord carries two slices and two time pointers, so handing the stored value back directly would let a caller mutate a key's scopes or its expiry without going through Save, and would do it without the mutex — a data race under -race, and silent corruption without it.
func NewMemoryAPIKeyStore ¶ added in v0.8.0
func NewMemoryAPIKeyStore() *MemoryAPIKeyStore
func (*MemoryAPIKeyStore) Delete ¶ added in v0.8.0
func (s *MemoryAPIKeyStore) Delete(_ context.Context, id string) error
Delete removes the record and its prefix index entry. This is the hard delete the reference advises against — Revoke keeps the audit trail — and it is here so that a host can exercise both branches of the reference's DELETE /admin/api/api-keys/:id (admin.router.ts:1348-1354) against a real store.
The index entry is removed only when it still points at this record: a later key that happened to claim the same prefix owns the entry, and deleting the earlier record must not unhook the later one.
func (*MemoryAPIKeyStore) FindByID ¶ added in v0.8.0
func (s *MemoryAPIKeyStore) FindByID(_ context.Context, id string) (APIKeyRecord, error)
FindByID returns the record whatever its state, which is what the admin screens need: a revoked or expired key has to stay openable.
func (*MemoryAPIKeyStore) FindByPrefix ¶ added in v0.8.0
func (s *MemoryAPIKeyStore) FindByPrefix(_ context.Context, prefix string) (APIKeyRecord, error)
FindByPrefix returns the active record with this prefix, per the interface contract. A revoked record is ErrAPIKeyNotFound here and still readable through FindByID.
func (*MemoryAPIKeyStore) ListAll ¶ added in v0.8.0
func (s *MemoryAPIKeyStore) ListAll(_ context.Context, limit, offset int) ([]APIKeyRecord, error)
ListAll returns one page in the order APIKeyAdminStore declares: CreatedAt descending, ID ascending.
The whole set is copied and sorted on every call. That is quadratic-ish nonsense for a table of any size and entirely fine here: this store holds what one process minted since it started, and a store that needs to page a real table is a real store with a real index.
The returned slice is capped to its length, so a caller that appends to the page allocates rather than writing into the rows beyond it.
func (*MemoryAPIKeyStore) ListByServiceID ¶ added in v0.8.0
func (s *MemoryAPIKeyStore) ListByServiceID(_ context.Context, serviceID string) ([]APIKeyRecord, error)
ListByServiceID returns every key issued to one service identity, active or not, in the same order. There is no paging because the reference declares none.
The empty service identity is a real one and matches records whose ServiceID is empty: the reference stores `serviceId ?? null` (api-key.service.ts:59) and this port has no null, so "no service" and "" are one value here and a caller that asks for it gets the unattached keys.
func (*MemoryAPIKeyStore) LogUsage ¶ added in v0.8.0
func (s *MemoryAPIKeyStore) LogUsage(_ context.Context, entry APIKeyAuditEntry) error
LogUsage appends an audit entry. Nothing in this package calls it yet — see APIKeyAuditStore — so in this release it is written only by a host that decided to, and read only through Usage.
func (*MemoryAPIKeyStore) Revoke ¶ added in v0.8.0
func (s *MemoryAPIKeyStore) Revoke(_ context.Context, id string) error
Revoke clears IsActive and keeps the record. An unknown id is a no-op returning nil, not ErrAPIKeyNotFound; see APIKeyStore.Revoke for why the reference's route depends on that.
func (*MemoryAPIKeyStore) Save ¶ added in v0.8.0
func (s *MemoryAPIKeyStore) Save(_ context.Context, key APIKeyRecord) error
Save persists the record, replacing any earlier one with the same ID.
The prefix index is rewritten with it. A re-save that changes a record's Prefix — which Create never does, but a host restoring a backup might — drops the stale index entry, so the old prefix stops resolving instead of resolving to a record that no longer claims it.
func (*MemoryAPIKeyStore) UpdateLastUsed ¶ added in v0.8.0
UpdateLastUsed stamps the record. An unknown id is a no-op, as on Revoke.
func (*MemoryAPIKeyStore) Usage ¶ added in v0.8.0
func (s *MemoryAPIKeyStore) Usage() []APIKeyAuditEntry
Usage returns the audit entries LogUsage has collected, oldest first.
It is on this type and on no interface. APIKeyAuditStore is a write seam — the reference declares `logUsage` and no reader to go with it — so there is no contract to put this on, and inventing one would be inventing a method the reference does not have. It exists because a log nothing can read is not a log, and because the tests have to be able to assert what was written.
The slice is a copy; APIKeyAuditEntry holds nothing mutable, so a shallow one is enough.
type MemoryAuthCodeStore ¶ added in v0.4.0
type MemoryAuthCodeStore struct {
// contains filtered or unexported fields
}
MemoryAuthCodeStore is an in-process implementation of AuthCodeStore. It is the IDP's default and is correct only while /authorize and /token are served by the same process; see IDPConfig.Codes.
A single mutex covers both methods, so ConsumeCode's lookup-and-delete is atomic and exactly one of any number of concurrent consumers receives the record. Expired entries are dropped on the next SaveCode so a flow the user abandoned does not pin memory until restart.
func NewMemoryAuthCodeStore ¶ added in v0.4.0
func NewMemoryAuthCodeStore() *MemoryAuthCodeStore
func (*MemoryAuthCodeStore) ConsumeCode ¶ added in v0.4.0
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) GetAllRoles ¶ added in v0.8.0
func (s *MemoryRolesPermissionsStore) GetAllRoles(_ context.Context) ([]string, error)
GetAllRoles makes MemoryRolesPermissionsStore satisfy RoleLister: every role CreateRole has defined, name-ascending, matching GetPermissionsForRole and GetRolesForUser above.
The set is rolePermissions, not the roles users happen to hold. A role created and assigned to nobody is still a role — GET /admin/api/roles exists to show exactly that — and a role with no permissions is too, since CreateRole with an empty list creates one.
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) GetAllSessions ¶ added in v0.8.0
func (s *MemorySessionStore) GetAllSessions(_ context.Context, limit, offset int) ([]Session, error)
GetAllSessions makes MemorySessionStore satisfy SessionLister: every session of every user, ordered by Session.ID ascending, paged by the shared rules.
Revoked and expired sessions are both included, which is what SessionLister specifies and what ListSessionsForUser beside it already does — this store tombstones rather than deletes, so dropping the dead ones belongs above the store, not in it.
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 MemorySettingsStore ¶ added in v0.6.0
type MemorySettingsStore struct {
// contains filtered or unexported fields
}
MemorySettingsStore is the in-memory SettingsStore, safe for concurrent use. Everything handed out is a copy, so mutating a returned AuthSettings — or the UISettings behind it — does not reach the store.
func NewMemorySettingsStore ¶ added in v0.6.0
func NewMemorySettingsStore() *MemorySettingsStore
NewMemorySettingsStore returns a MemorySettingsStore holding nothing, so that GetSettings answers the zero AuthSettings until something is written.
func (*MemorySettingsStore) GetSettings ¶ added in v0.6.0
func (s *MemorySettingsStore) GetSettings(_ context.Context) (AuthSettings, error)
GetSettings returns a copy of the stored settings.
func (*MemorySettingsStore) UpdateSettings ¶ added in v0.6.0
func (s *MemorySettingsStore) UpdateSettings(_ context.Context, patch AuthSettings) (AuthSettings, error)
UpdateSettings applies patch with MergeSettings and returns a copy of what is stored afterwards.
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 MemoryTemplateStore ¶ added in v0.4.0
type MemoryTemplateStore struct {
// contains filtered or unexported fields
}
MemoryTemplateStore is the in-memory TemplateStore (memory-template.store.ts), safe for concurrent use. Everything handed out is a copy: mutating a returned template does not reach the store. Lists come back in first-insertion order, as the reference's Map iteration does (memory-template.store.ts:11-13, 29-31).
func NewMemoryTemplateStore ¶ added in v0.4.0
func NewMemoryTemplateStore() *MemoryTemplateStore
NewMemoryTemplateStore returns an empty MemoryTemplateStore.
func (*MemoryTemplateStore) GetMailTemplate ¶ added in v0.4.0
func (s *MemoryTemplateStore) GetMailTemplate(_ context.Context, id string) (MailTemplate, bool, error)
func (*MemoryTemplateStore) GetUITranslations ¶ added in v0.4.0
func (s *MemoryTemplateStore) GetUITranslations(_ context.Context, page string) (UITranslation, bool, error)
func (*MemoryTemplateStore) ListMailTemplates ¶ added in v0.4.0
func (s *MemoryTemplateStore) ListMailTemplates(_ context.Context) ([]MailTemplate, error)
ListMailTemplates returns every stored template in the order the ids were first written.
func (*MemoryTemplateStore) ListUITranslations ¶ added in v0.4.0
func (s *MemoryTemplateStore) ListUITranslations(_ context.Context) ([]UITranslation, error)
ListUITranslations returns every stored page in the order the pages were first written.
func (*MemoryTemplateStore) UpdateMailTemplate ¶ added in v0.4.0
func (s *MemoryTemplateStore) UpdateMailTemplate(_ context.Context, id string, patch MailTemplatePatch) (MailTemplate, error)
UpdateMailTemplate upserts: a template the store does not hold starts as {id, "", "", {}} (memory-template.store.ts:16-21) and the patch is spread on top (:22), as MailTemplatePatch describes. The stored result is returned.
func (*MemoryTemplateStore) UpdateUITranslations ¶ added in v0.4.0
func (s *MemoryTemplateStore) UpdateUITranslations(_ context.Context, page string, translations map[string]map[string]string) (UITranslation, error)
UpdateUITranslations replaces the page's translations wholesale (memory-template.store.ts:33-35): a page is set, not patched.
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 MemoryUploadStore ¶ added in v0.10.0
type MemoryUploadStore struct {
// contains filtered or unexported fields
}
MemoryUploadStore is the in-process UploadStore, safe for concurrent use. It is the counterpart of every other Memory*Store in this package: what the tests run against, what an example wires, and what a single-process deployment that does not mind losing its logos on restart can ship.
It holds every object in memory, so it is bounded by what is uploaded through it — UploadMaxBytes per object and no cap on the count. A deployment that leaves the console open to an administrator with a large disk should give it a real store rather than this one.
It is not the default. A nil Config.Uploads is a deployment with no upload feature, and defaulting to this one would turn that into a feature that silently forgets — which is the exact failure the Lambda /tmp shape has and the reason this seam exists.
func NewMemoryUploadStore ¶ added in v0.10.0
func NewMemoryUploadStore() *MemoryUploadStore
NewMemoryUploadStore returns an empty MemoryUploadStore.
func (*MemoryUploadStore) Delete ¶ added in v0.10.0
func (s *MemoryUploadStore) Delete(_ context.Context, key string) error
Delete removes one object, reporting ErrUploadNotFound if there was none.
func (*MemoryUploadStore) List ¶ added in v0.10.0
func (s *MemoryUploadStore) List(_ context.Context) ([]UploadedFile, error)
List returns every stored object, newest first with ties broken by key ascending, which is the order UploadStore documents.
func (*MemoryUploadStore) Open ¶ added in v0.10.0
func (s *MemoryUploadStore) Open(_ context.Context, key string) (io.ReadCloser, UploadedFile, error)
Open returns a reader over the stored bytes. The reader is a *bytes.Reader over the stored slice rather than a copy of it, which is safe because nothing mutates a stored slice: Put replaces the entry wholesale.
func (*MemoryUploadStore) Put ¶ added in v0.10.0
func (s *MemoryUploadStore) Put(_ context.Context, key string, content io.Reader) (UploadedFile, error)
Put reads content whole and stores it under key, replacing any object already there. A read error stores nothing, which is the contract UploadStore.Put states: the buffer is only published once it is complete.
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) ListUsers ¶ added in v0.8.0
func (s *MemoryUserStore) ListUsers(_ context.Context, tenantID string, limit, offset int) ([]User, error)
ListUsers makes MemoryUserStore satisfy AdminUserStore, and with it defines the order this package calls normative: User.ID ascending, by Go string comparison. See AdminUserStore for why the order is a contract and not an implementation detail, and for what tenantID does and does not select — in particular that an empty tenantID lists every user rather than only the users stored under the empty tenant.
func (*MemoryUserStore) MarkEmailVerified ¶
func (*MemoryUserStore) UpdateEmailChangeToken ¶
func (*MemoryUserStore) UpdateEmailVerificationToken ¶
func (*MemoryUserStore) UpdateIsAdmin ¶ added in v0.10.0
func (s *MemoryUserStore) UpdateIsAdmin(_ context.Context, userID, tenantID string, isAdmin bool) error
UpdateIsAdmin makes MemoryUserStore satisfy UserAdminFlagStore, which is what POST <admin>/users/:id/promote needs for method=flag. It writes the one flag and touches nothing else; see the interface.
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) UpdateRequire2FA ¶ added in v0.10.0
func (s *MemoryUserStore) UpdateRequire2FA(_ context.Context, userID, tenantID string, required bool) error
UpdateRequire2FA makes MemoryUserStore satisfy UserTwoFactorPolicyStore, which with ListUsers above is what turns the admin console's twoFAPolicy section on. It writes the one flag and touches nothing else; see the interface.
func (*MemoryUserStore) UpdateResetToken ¶
func (*MemoryUserStore) UpdateSMSCode ¶
func (*MemoryUserStore) UpdateTOTPSecret ¶
type MemoryWebhookStore ¶ added in v0.8.0
type MemoryWebhookStore struct {
// contains filtered or unexported fields
}
MemoryWebhookStore is the in-memory WebhookStore, and implements WebhookAdminStore and InboundWebhookStore as well. It is safe for concurrent use, and everything it hands out is a deep copy: mutating a returned configuration, its Events slice or its IsActive pointer does not reach the store.
The reference ships no in-memory webhook store to port — src/stores holds only memory-template.store.ts — so the ordering below is this port's to choose, and having chosen it, this package is where it is normative.
The order is first-insertion, everywhere: FindByEvent, ListWebhooks and FindByProvider all walk the configurations in the order AddWebhook saw them. Updating one does not move it; removing one closes the gap, and re-adding it puts it at the end. This is MemoryTemplateStore's rule (template_store.go) and the Map iteration order of the one store the reference does ship, it is the only order that makes ListWebhooks' limit/offset pagination stable across calls, and unlike sorting by ID it stays meaningful with ids that are random (newID).
func NewMemoryWebhookStore ¶ added in v0.8.0
func NewMemoryWebhookStore() *MemoryWebhookStore
NewMemoryWebhookStore returns an empty MemoryWebhookStore.
func (*MemoryWebhookStore) AddWebhook ¶ added in v0.8.0
func (s *MemoryWebhookStore) AddWebhook(_ context.Context, config WebhookConfig) (WebhookConfig, error)
AddWebhook assigns an id and appends the configuration to the insertion order. The ID of the argument is ignored.
func (*MemoryWebhookStore) FindByEvent ¶ added in v0.8.0
func (s *MemoryWebhookStore) FindByEvent(_ context.Context, event, tenantID string) ([]WebhookConfig, error)
FindByEvent applies WebhookConfig.Matches to every stored configuration, in insertion order.
func (*MemoryWebhookStore) FindByProvider ¶ added in v0.8.0
func (s *MemoryWebhookStore) FindByProvider(_ context.Context, provider string) (WebhookConfig, bool, error)
FindByProvider returns the first configuration registered for provider in insertion order, active or not. An empty provider never matches: every outgoing-only configuration leaves the field unset (webhook-store.interface.ts:41), so matching on "" would hand the router one of those.
func (*MemoryWebhookStore) ListWebhooks ¶ added in v0.8.0
func (s *MemoryWebhookStore) ListWebhooks(_ context.Context, limit, offset int) ([]WebhookConfig, error)
ListWebhooks returns one page of every stored configuration, active or not, in insertion order.
func (*MemoryWebhookStore) RemoveWebhook ¶ added in v0.8.0
func (s *MemoryWebhookStore) RemoveWebhook(_ context.Context, id string) error
RemoveWebhook deletes the configuration and closes the gap it leaves in the insertion order. An unknown id is a no-op and not an error.
func (*MemoryWebhookStore) UpdateWebhook ¶ added in v0.8.0
func (s *MemoryWebhookStore) UpdateWebhook(_ context.Context, id string, patch WebhookPatch) error
UpdateWebhook applies the patch in place, keeping the configuration where it is in the insertion order. An unknown id is a no-op and not an error.
type NotifyChannel ¶ added in v0.10.0
type NotifyChannel string
NotifyChannel is one delivery channel of AuthTools.Notify: the reference's `'sse' | 'email' | 'sms'` union (auth-tools.ts:119).
const ( NotifyChannelSSE NotifyChannel = "sse" NotifyChannelEmail NotifyChannel = "email" NotifyChannelSMS NotifyChannel = "sms" )
type NotifyOptions ¶ added in v0.10.0
type NotifyOptions struct {
// Type is the StreamEvent's `event:` line. Empty means "notification",
// which is the reference's `?? 'notification'` (auth-tools.ts:298).
Type string
// TenantID and UserID are carried on the frame and, for the email and SMS
// channels, UserID is what the user store is asked for. A notification with
// no UserID reaches neither of those two channels at all.
TenantID string
UserID string
// Metadata is carried on the frame and never interpreted.
Metadata map[string]any
// Channels are the deliveries attempted. Empty means just the SSE channel,
// which is the reference's `?? ['sse']` and its backward-compatible default
// (auth-tools.ts:293). An unknown channel name is ignored rather than
// refused, as it is there.
Channels []NotifyChannel
// EmailSubject overrides the subject line. Empty falls back to Type, and
// then to "Notification" (auth-tools.ts:322).
EmailSubject string
// SMSMessage overrides the message body. Empty falls back to the encoded
// payload (auth-tools.ts:334).
SMSMessage string
}
NotifyOptions is the second argument of AuthTools.Notify: the reference's NotifyOptions (auth-tools.ts:99-134).
type OAuthAccountConflictError ¶ added in v0.6.0
OAuthAccountConflictError is the outcome the reference's AuthError("...", "OAUTH_ACCOUNT_CONFLICT") carries, with the same two data fields (auth.router.ts:1346-1355): the address the provider asserted and the provider account that asserted it. The callback answers it the way the reference does — it stashes the pair and redirects the browser to /account-conflict — rather than as a JSON error, which is why nothing maps it in OAuthHTTPError.
It is raised only under OnEmailMatch = OAuthEmailMatchConflict: a provider account nobody has linked, asserting an address some account already holds.
func (*OAuthAccountConflictError) Error ¶ added in v0.6.0
func (e *OAuthAccountConflictError) Error() string
func (*OAuthAccountConflictError) Unwrap ¶ added in v0.6.0
func (e *OAuthAccountConflictError) Unwrap() error
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
// AdditionalAuthParams are extra query parameters for the authorization
// URL — provider-specific knobs such as Google's access_type or prompt. They
// are the reference's additionalAuthParams (generic-oauth.strategy.ts:63) and
// keep its precedence exactly (:113-120): an entry overrides client_id,
// redirect_uri, response_type and scope, and state overrides an entry. The
// PKCE pair this port adds (code_challenge, code_challenge_method) is written
// after them as well, so a configuration cannot weaken the code binding.
AdditionalAuthParams map[string]string
// GitHubEmailsURL is the endpoint the "github" provider consults when the
// user profile carries no email — a GitHub account whose address is private
// (github.strategy.ts:54-68). Empty means UserInfoURL + "/emails", which is
// https://api.github.com/user/emails for the preset; a test points it at a
// fake. Only the "github" provider reads it.
GitHubEmailsURL string
// MapProfile replaces the default mapping of the userinfo document for
// this provider — the reference's mapProfile hook
// (generic-oauth.strategy.ts:71-77), which getUserProfile returns as the
// profile whenever it is set (:151-153). It replaces the whole mapping,
// the github preset's /user/emails fallback included. The service fills
// Provider, and Raw when the hook left it nil; an error fails the exchange
// on the profile-failed path (401 OAUTH_PROFILE_FAILED on the wire). When
// both MapProfile and ProfileMap are set, MapProfile wins.
MapProfile func(raw map[string]any) (OAuthUserInfo, error)
// ProfileMap is the declarative form of MapProfile for a provider loaded
// from configuration: the keys id, email, emailVerified, name and picture
// (id required) mapped to expressions such as "$.data.user.id" or
// "$.mail ?? $.userPrincipalName". CompileProfileMap documents the grammar.
// NewOAuthService compiles it: a map that does not compile is recorded
// against the provider, whose exchanges then fail with that error before
// contacting the provider (500 on the wire); NewOAuthServiceWithConfig
// reports it at start-up instead. nil or empty means the default mapping.
ProfileMap map[string]string
}
OAuthProvider holds configuration for a single OAuth 2.0 provider. It is the reference's GenericOAuthProviderConfig (generic-oauth.strategy.ts:39-78) with the two hard-coded strategies, Google and GitHub, expressed as presets.
func GitHubProvider ¶
func GitHubProvider(clientID, clientSecret, redirectURL string) OAuthProvider
GitHubProvider builds a pre-configured GitHub OAuth2 provider — the reference's GithubStrategy (github.strategy.ts:23-31, :49-70), including the /user/emails fallback for accounts with a private address.
func GoogleProvider ¶
func GoogleProvider(clientID, clientSecret, redirectURL string) OAuthProvider
GoogleProvider builds a pre-configured Google OAuth2 provider — the reference's GoogleStrategy (google.strategy.ts:23-33, :53). The authorization request carries access_type=offline (:29), the one extra parameter that strategy sends; it sends no prompt.
type OAuthProvisioning ¶ added in v0.6.0
type OAuthProvisioning struct {
// AutoCreate allows the callback to create an account for a provider
// identity nothing here knows yet. With it false the callback answers
// 403 OAUTH_USER_NOT_PROVISIONED instead, so accounts can only be made by
// some other path (an invitation, an admin, /register).
AutoCreate bool
// AllowedEmailDomains restricts which addresses may sign in through any
// provider. Empty allows every address. An entry is a bare domain —
// "example.com", or "@example.com", which is accepted and normalised —
// matched case-insensitively against the part of the profile address after
// the last @, with no subdomain matching: "example.com" does not admit
// "mail.example.com". A profile with no address at all is refused whenever
// the list is non-empty, because nothing about it can be checked.
AllowedEmailDomains []string
// RequireVerifiedEmail demands that the provider actually asserted the
// address. A profile whose emailVerified is false — and one where the
// provider said nothing, which is most of them — is refused with
// 403 OAUTH_EMAIL_NOT_VERIFIED. It is off by default: turning it on for a
// provider that does not send the claim refuses every login through it.
RequireVerifiedEmail bool
// OnEmailMatch decides what happens when the provider account is unknown
// but some account already holds the profile address:
// OAuthEmailMatchLink (the default), OAuthEmailMatchConflict or
// OAuthEmailMatchReject.
OnEmailMatch string
// FieldMap fills further User fields from the raw provider profile when the
// callback creates an account. The keys are the fields it may write —
// firstName, lastName, phoneNumber and role — and the values are
// expressions in the ProfileMap grammar, evaluated against the same
// userinfo document (see CompileProfileMap): "$.given_name",
// "$.name.first ?? $.given_name", "$.roles[0]", or a quoted literal as the
// last resort. A key outside that list, or a value that does not compile,
// is refused by Validate and therefore by WithOAuth, so a typo in
// configuration fails at start-up rather than at the first login. An
// expression that resolves to nothing leaves its field alone; one that
// resolves to an object or an array fails the callback.
//
// The fields the profile itself owns are deliberately not targets: id and
// email come from OAuthUserInfo (map them with ProfileMap), tenantId comes
// from the wiring, loginProvider is the provider, and isEmailVerified is
// this policy's business rather than a mapping's.
FieldMap map[string]string
}
OAuthProvisioning is the declarative replacement for the abstract findOrCreateUser the reference makes every integrator write (generic-oauth.strategy.ts:169-172). It governs the whole callback, not only the create branch: RequireVerifiedEmail and AllowedEmailDomains are checked before the identity is resolved at all, exactly as a findOrCreateUser that began with those checks would.
The zero value is not the default policy — DefaultOAuthProvisioning is, and OAuthWiring.Provisioning is a pointer so that a deployment which sets nothing keeps the behaviour it had before this type existed. A policy given explicitly is taken as written: AutoCreate false means false. The one field whose empty value still means a default is OnEmailMatch, which has no empty mode.
func DefaultOAuthProvisioning ¶ added in v0.6.0
func DefaultOAuthProvisioning() OAuthProvisioning
DefaultOAuthProvisioning is the policy a wiring without one gets: create missing accounts, link a matching address. It is what this port did before OAuthProvisioning existed, so adding the type changed no deployment.
func (OAuthProvisioning) Validate ¶ added in v0.6.0
func (p OAuthProvisioning) Validate() error
Validate reports what is wrong with a policy loaded from configuration: an unknown OnEmailMatch mode, an empty or malformed domain entry, a FieldMap key that is not a field this policy may write, or a FieldMap expression that does not compile. WithOAuth runs it, so a deployment that wires an invalid policy fails at construction; it is exported so one can check a policy before wiring anything, as CompileProfileMap can be.
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. A ProfileMap that does not compile is not fatal here: the error is recorded and every exchange through that provider returns it, while the other providers keep working. NewOAuthServiceWithConfig surfaces it at start-up.
func NewOAuthServiceWithConfig ¶ added in v0.4.0
func NewOAuthServiceWithConfig(providers ...OAuthProvider) (*OAuthService, error)
NewOAuthServiceWithConfig is NewOAuthService with the configuration validated: it returns the ProfileMap compile errors of every provider that has one — joined, each naming its provider and the expression — and no service, so a deployment fails at start-up rather than at the first login.
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 under the default provisioning policy (DefaultOAuthProvisioning): accounts are created when the provider identity is unknown, and an address some account already holds is linked to it. It is HandleCallbackWithPolicy with that policy, kept as it was so an embedder driving the service directly is unaffected by the policy.
If linkToUserID is non-empty the provider is linked to that existing account.
func (*OAuthService) HandleCallbackWithPolicy ¶ added in v0.6.0
func (s *OAuthService) HandleCallbackWithPolicy( ctx context.Context, authSvc *Service, linkedAccounts LinkedAccountStore, info OAuthUserInfo, tenantID string, linkToUserID string, policy OAuthProvisioning, ) (User, AuthTokens, error)
HandleCallbackWithPolicy is HandleCallback under an explicit provisioning policy — the whole of what the reference leaves to the integrator's findOrCreateUser (generic-oauth.strategy.ts:169-172). The order is the reference's resolution order with the policy's terms in it:
- The policy gate: the provider must have asserted the address when RequireVerifiedEmail is set, and the address must be in an admitted domain. Both refuse before anything is read or written, so a refused identity leaves no trace.
- linkToUserID, when the caller named an account to link to.
- The existing (provider, providerAccountId) link, which is the only identification the reference's own store documentation considers safe (user-store.interface.ts:105-119).
- An account already holding the address: linked, refused as a conflict or refused outright, per OnEmailMatch.
- Creation, when AutoCreate allows it.
The conflict outcome is *OAuthAccountConflictError; the three refusals are this port's own and reach the wire as 403s with the codes in oauth_wire.go.
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
// EmailVerified is the provider's email_verified claim, or for GitHub the
// verified flag of the address picked from /user/emails. nil means the
// provider said nothing, the reference's `emailVerified: undefined`. The
// provisioning policy reads it twice: RequireVerifiedEmail refuses a
// callback that has no positive claim, and an account the callback creates
// is verified unless the provider positively said otherwise (D-11).
EmailVerified *bool
Raw map[string]any
}
OAuthUserInfo is the normalized profile returned after token exchange — the reference's {id, email, emailVerified?, name?, picture?} (generic-oauth.strategy.ts:143, :154-160), plus the provider name and the raw document.
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 port's spelling of the reference's cors.origins for
// origin resolution: together with Config.SiteURLs — the two lists merged in
// that order and deduplicated, as buildAllowedOrigins does
// (auth.router.ts:213-219) — it is the allowlist an initiating request's
// Origin or Referer is matched against, and the allowlist a state's origin
// has to be in to be redirected to. It is the same list Auth.ResolveSiteURL
// uses for emailed links. An empty merged 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. It predates Config.SiteURLs and stays as the
// override: when set it is the default site URL for the OAuth routes and for
// Auth.ResolveSiteURL alike; when empty the first Config.SiteURLs entry is.
// A deployment configuring only SiteURLs may leave it unset.
SiteURL string
// TenantID scopes users created or resolved by the callback.
TenantID string
// Provisioning is the policy the callback resolves an identity under — the
// declarative stand-in for the abstract findOrCreateUser the reference makes
// every integrator write (generic-oauth.strategy.ts:169-172). It is a
// pointer so that nil means DefaultOAuthProvisioning: a deployment that
// leaves it unset keeps exactly the behaviour it had before the policy
// existed (create missing accounts, link a matching address), and one that
// sets it is taken at its word, AutoCreate false included. WithOAuth
// validates it, so an unknown OnEmailMatch or a FieldMap that does not
// compile fails at construction.
Provisioning *OAuthProvisioning
// 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 OIDCMount ¶ added in v0.7.0
type OIDCMount struct {
// Path is the endpoint below the mount prefix, leading "/" included.
Path string
// Handler serves it, for every method.
Handler http.Handler
// ResourceServerGated reports that this endpoint creates or exchanges a
// credential, so HTTPConfig.ResourceServer leaves it unmounted the way it
// leaves /login and /refresh unmounted. Every adapter skips a gated mount
// under that flag; see oidcResourceServerGated for which two carry it and
// why. RegisterHandlers ignores the field: it mounts a host's own mux, with
// no HTTPConfig in sight, so the host decides what goes on it.
ResourceServerGated bool
}
OIDCMount is one OIDC endpoint as the adapters mount it: Path is below the mount prefix and Handler serves it.
The handler is mounted for every method, which is what (*IDP).RegisterHandlers has always done with a method-less http.ServeMux pattern, and what the handlers expect — /authorize reads r.Method itself to tell its login form from its credential post, /token and /userinfo read the form and the Authorization header rather than the method. So there is no method to narrow to without changing an answer, and the four adapters register the handler for all methods rather than for a list they would then have to keep in step.
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
// IDProvider adds the IdP's public JWKS route to the document. Set it when
// the adapter was mounted on an Auth built WithIDP, and leave it false
// otherwise: that route exists only under that configuration, so a document
// that always carried it would advertise an endpoint most deployments
// answer 404 for.
IDProvider bool
// JWKSPath is where that route is served below APIPrefix. Empty means
// DefaultJWKSPath, and it must match the IDPConfig.JWKSPath of the IdP the
// adapter was mounted with. Read only when IDProvider is true.
//
// A missing leading "/" is added, since the value is a path below the
// prefix. A value that collides with a route this document already
// describes is ignored rather than allowed to replace it, which would drop
// a real endpoint from the spec; NewIDP refuses such a JWKSPath outright,
// so the collision can only come from this field disagreeing with the IdP.
JWKSPath string
// OIDC adds the IdP's other four routes — the discovery document,
// authorize, token and userinfo — to the document, at the fixed paths
// OIDCDiscoveryPath, OIDCAuthorizePath, OIDCTokenPath and
// OIDCUserInfoPath below APIPrefix. Set it when the adapter was mounted on
// an Auth built WithIDP, and leave it false otherwise: those routes exist
// only under that configuration, exactly as the JWKS route does.
//
// It is a second flag rather than more of IDProvider because the two
// describe different things to different readers. IDProvider publishes a
// key so that somebody else's resource server can verify this IdP's
// tokens, and a deployment may well want that route documented and no
// more — it is the only one a verifier calls. OIDC advertises an
// authorization server a relying party is invited to drive. A deployment
// that serves the OIDC endpoints on a mux of its own, through
// (*IDP).RegisterHandlers under some other base path, leaves this false
// and documents them where it mounted them.
//
// Unlike JWKSPath these paths are not configurable, so there is nothing to
// tell this document; see the OIDC path constants.
//
// Setting ResourceServer alongside it drops OIDCAuthorizePath and
// OIDCTokenPath again, because the adapters do not mount those two in that
// mode; the discovery document and userinfo stay. See OIDCMount.
OIDC bool
// Docs mirrors HTTPConfig.Docs.Enabled: the two documentation routes the
// adapters then mount, GET <prefix>/openapi.json and GET <prefix>/docs, are
// described by the document too. Set the two together, or the spec omits
// two routes the server serves — or documents two it does not.
//
// The reference's generator describes neither, under any option: it emits
// the auth routes and stops (openapi.ts:68 onwards), so a document with
// this flag carries two path items the reference's never has. That half of
// the docs-routes-are-opt-in deviation is registered in compatibility.go.
// A document that hid the endpoint it is served from would be a document
// the conformance suite could not hold to the mount in both directions,
// which is the check that keeps the rest of it honest.
Docs bool
// ResourceServer mirrors HTTPConfig.ResourceServer: the credential routes
// the adapters then leave unmounted are left out of the spec too. Set the
// two together, or the spec documents routes that answer 404.
ResourceServer bool
// UI mirrors HTTPConfig.UI.Enabled: it adds GET <prefix>/ui/config, the
// document the built-in UI reads its branding and feature flags from. The
// adapters mount that route only under that flag, so set the two together.
UI bool
}
OpenAPIInfo holds metadata for the generated OpenAPI spec.
type Option ¶
type Option func(*authBuilder) error
Option configures Auth initialization.
func WithAPIKeyStore ¶ added in v0.10.0
func WithAPIKeyStore(store APIKeyStore) Option
WithAPIKeyStore registers the API-key store the admin console's 🔑 tab is served from: the reference's AdminOptions.apiKeyStore (admin.router.ts:121-124).
It turns the apiKeys feature flag on — `!!options.apiKeyStore` (:653) — and it is what the four routes under <admin>/api/api-keys answer 404 for when it is absent. What the store can *do* is a second question those routes ask by type assertion: without APIKeyAdminStore the listing is a 501 naming listAll, and without APIKeyDeleteStore the hard delete revokes instead and says so.
It is not read anywhere else. APIKeyMiddleware takes its store directly, which is how a host protects its own routes with a key, and registering one here does not put a key anywhere near the auth router's own credentials.
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 verify-email template, which is what the reference mails here.
func WithEmailChangedSender ¶ added in v0.4.0
func WithEmailChangedSender(sender EmailChangedSender) Option
WithEmailChangedSender wires the notice POST <prefix>/change-email/confirm mails to the OLD address once the change is applied — the reference's config.email.sendEmailChanged (auth.router.ts:1060-1066). Without it the route mails nothing and still answers 200. A failing sender answers the generic 500 with the change already committed, as the reference does.
Pass a callback, or EmailChangedMailer.Send for the built-in email-changed 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 WithEventBus ¶ added in v0.10.0
WithEventBus sets Config.Events: the bus the library raises its identity.* events on. It is the reference's routerOptions.eventBus (node-auth auth.router.ts:92-96, read by publishRouterEvent at :418-420).
bus := auth.NewEventBus()
bus.Subscribe(auth.EventAuthLoginSuccess, func(ev auth.Event) { … })
bus.Subscribe(auth.EventBusWildcard, func(ev auth.Event) { … })
a, err := auth.New(auth.WithEventBus(bus))
A nil bus is refused rather than accepted as "no bus", which is the opposite of leaving the option out. The distinction matters here more than it does for the store options: an event that is never raised fails silently and stays silent, so a host that wrote WithEventBus(b) with a b that turned out nil would get exactly the symptom this option exists to prevent. Omitting the option is how a deployment says it wants no events, and it is the default.
func WithIDP ¶ added in v0.6.0
WithIDP registers an OIDC Identity Provider on the Auth, which is what makes every adapter mount its JWKS document at GET <prefix><IDPConfig.JWKSPath>, public and ahead of any middleware.
The reference registers that route from the same condition — an idProvider block with a key, or enabled (auth.router.ts:473-475) — where here the condition is having built an IDP and passed it here.
The same switch mounts the four OIDC endpoints beside it, at the paths (*IDP).RegisterHandlers uses and below the same prefix: OIDCDiscoveryPath, OIDCAuthorizePath, OIDCTokenPath and OIDCUserInfoPath, each for every method. Those four are this port's own — the reference has no OIDC authorization server — and see OIDCMounts for the list and RegisterHandlers for the host that would rather serve them on a mux of its own. Nothing about how /login, /refresh or the session tokens work changes either way.
An IDP built with a nil Service adopts the one this Auth is being built around, which is the only order a host can write: NewIDP wants a *Service and a *Service only exists once New has returned. So
idp, err := auth.NewIDP(auth.IDPConfig{Signer: key}, nil)
a, err := auth.New(auth.WithUserStore(store), auth.WithIDP(idp))
leaves idp fully wired.
An IDP that already carries a different Service — one constructed directly, or one a previous auth.New already bound — is refused, and New returns an error: its /token and /userinfo would answer from that other Service's user store while this Auth's routes answered from this one. Build a second IDP for a second Auth. One IDP per Auth is also what keeps the binding free of a data race, since nothing rebinds an IDP that may already be serving requests.
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.
A Provisioning policy is validated here, so a deployment whose configuration names an unknown OnEmailMatch mode or a FieldMap target that does not exist fails at construction rather than at the first login.
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 password-reset template over a MailerTransport.
func WithPasswordVerifier ¶ added in v0.7.0
func WithPasswordVerifier(verify PasswordVerifier) Option
WithPasswordVerifier sets Config.PasswordVerifier, the migration seam: the hook POST <prefix>/login consults for a user whose stored hash did not verify the supplied password, so a deployment can accept the password its previous identity provider held, adopt it as a local hash and drop the dependency.
auth.WithPasswordVerifier(func(ctx context.Context, u auth.User, pw string) (bool, bool, error) {
if u.Metadata["legacyIdP"] != "acme" {
return false, false, nil
}
ok, err := acme.CheckPassword(ctx, u.Email, pw)
return ok, ok, err
})
The marker check on the first line of that example is a requirement, not decoration: the hook is reached for every account whose stored hash does not verify, including OAuth-only and magic-link-only accounts that never had a password, and it is reached from an unauthenticated route. See PasswordVerifier, which also explains why adopting a password overwrites whatever hash was stored.
It is an additive port extension: the reference verifies bcrypt directly on its login path and has no such hook (local.strategy.ts:19-29). Omitting this Option is the reference's behaviour. Passing nil is refused, in the style of the sender Options: an explicit setter reached with nothing to set is a caller mistake, not a request to turn the seam off.
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 WithSettingsStore ¶ added in v0.6.0
func WithSettingsStore(store SettingsStore) Option
WithSettingsStore sets Config.Settings: the store holding the global runtime settings an administrator flips — the reference's routerOptions.settingsStore (auth.router.ts:92). Only AuthSettings.Require2FA is acted on, by POST <prefix>/2fa/disable; see SettingsStore for what the rest is for.
store := auth.NewMemorySettingsStore()
required := true
_, _ = store.UpdateSettings(ctx, auth.AuthSettings{Require2FA: &required})
auth.WithSettingsStore(store)
func WithSiteURLs ¶ added in v0.4.0
WithSiteURLs sets Config.SiteURLs: the base URLs the front ends are served from. The first is canonical, every one of them is allowlisted for the per-request origin match — see Config.SiteURLs and Auth.ResolveSiteURL.
auth.WithSiteURLs("https://www.example.com", "https://app.example.com")
Calling it with no arguments leaves the list empty, which is the reference's "no siteUrl configured" and not an error.
func WithTemplateStore ¶ added in v0.4.0
func WithTemplateStore(store TemplateStore) Option
WithTemplateStore sets Config.Templates: the store whose mail templates override the built-ins for every ready-made mailer the service calls — the reference's config.templateStore (mailer.service.ts:146, 156-179). See TemplateStore and MailTemplater.RenderMail for the rendering rule.
store := auth.NewMemoryTemplateStore() auth.WithTemplateStore(store)
func WithTenantProvider ¶
func WithTenantProvider(store TenantStore) Option
WithTenantProvider wires optional tenant support.
func WithTokenClaimsBuilder ¶
func WithTokenClaimsBuilder(fn TokenClaimsBuilder) Option
WithTokenClaimsBuilder customizes token claims creation. The hook may add claims and override the six base claims; the session claims (sid, tid, jti, typ, iss, iat, exp) are reserved and cannot be set from it — see Config.BuildTokenClaims.
TokenClaimsBuilder is an alias of the func type this always took, so a literal still satisfies it; StaticClaims, UserFieldClaims, ChainClaims and (*ClaimsWebhook).Build are ready-made ones (claims.go, claims_webhook.go).
func WithTokenTTLs ¶
WithTokenTTLs configures access and refresh TTL.
func WithTwoFactorAppName ¶ added in v0.4.0
WithTwoFactorAppName sets Config.TwoFactorAppName: the issuer an authenticator app shows for a TOTP enrolment made through POST <prefix>/2fa/setup. It is the reference's `twoFactor.appName`.
auth.WithTwoFactorAppName("Example App")
Left unset, the URI carries Config.Issuer, as it always has — see Config.TwoFactorAppName. Passing "" is the same as not calling it.
func WithUploadStore ¶ added in v0.10.0
func WithUploadStore(store UploadStore) Option
WithUploadStore sets Config.Uploads: the store the admin console's upload routes write through and the built-in UI serves uploaded assets back from — the seam over the reference's uploadDir (admin.router.ts:135).
One store wires both halves. With it set, POST <admin>/api/upload/logo and its three siblings are registered, the console's upload feature flag turns on, and <prefix>/ui/assets/logo/ and <prefix>/ui/assets/uploads/ serve what was uploaded — the last of those without setting UIOptions.Uploads, which stays as the override for a host that wants the read side to come from somewhere else.
store := auth.NewMemoryUploadStore() a, err := auth.New(auth.WithUploadStore(store))
MemoryUploadStore keeps everything in process memory and is for tests and single-process deployments; a deployment that must not lose an uploaded logo supplies its own. See UploadStore for the contract, and ValidUploadKey for the one rule an implementation has to honour.
func WithUserStore ¶
WithUserStore sets a custom UserStore implementation.
func WithWebhookStore ¶ added in v0.10.0
func WithWebhookStore(store WebhookStore) Option
WithWebhookStore registers the outgoing-webhook store the admin console's 🔗 tab is served from: the reference's AdminOptions.webhookStore (admin.router.ts:127-131).
It turns the webhooks feature flag on — `!!options.webhookStore` (:654) — and the four routes under <admin>/api/webhooks answer 404 without it. A store that does not implement WebhookAdminStore serves events and answers 501 to each of the four, naming the single method the reference names.
It is the same store AuthToolsOptions.Webhooks takes, and a deployment that has both passes it to both: this one is the console's management screen and that one is the delivery path. Neither writes what the other reads behind its back — the screen's PATCH changes a row the emitter will read next time it fans out, which is the point of the screen.
type OutgoingWebhookEvent ¶ added in v0.8.0
type OutgoingWebhookEvent struct {
Event string `json:"event"`
// Version is the event schema version, OutgoingWebhookVersion unless the
// host overrides it (auth-tools.ts:175).
Version string `json:"version"`
Timestamp string `json:"timestamp"`
// Data is the event payload, and the key is always present: the reference
// writes data ?? null, so an event with nothing to say sends null rather
// than omitting the field (auth-tools.ts:256). No omitempty, for that
// reason.
Data any `json:"data"`
// Metadata carries userId, tenantId, sessionId and correlationId
// (auth-tools.ts:257-262). The reference always builds the object and lets
// JSON.stringify drop the undefined members, so the key is present whenever
// any one of the four is known and the absent ones simply are not there —
// which is what omitempty over a map reproduces.
Metadata map[string]any `json:"metadata,omitempty"`
}
OutgoingWebhookEvent is the JSON body POSTed to a webhook endpoint (webhook-store.interface.ts:75-89). Event.OutgoingWebhook builds it and WebhookSender.Send serialises it; the bytes it produces are what the X-Webhook-Signature header signs.
Timestamp is a string, not a time.Time, because the reference's is a string (:84) and the two do not encode alike: JavaScript's toISOString() emits exactly three fractional digits and a Z, where time.Time's RFC 3339 encoding emits as many digits as the value has — none for a whole second, nine for a monotonic-clock reading — and a receiver that signature-checks the raw body sees a different string for the same instant. Keeping it a string forces the producer to choose the format deliberately.
type PasswordResetDelivery ¶ added in v0.3.0
type PasswordResetDelivery struct {
UserID string `json:"userId"`
TenantID string `json:"tenantId"`
// 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 `json:"email"`
// Token is the plaintext token POST <prefix>/reset-password accepts.
Token string `json:"token"`
// 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 `json:"expiresAt"`
// LinkBase and Lang are what MagicLinkDelivery's are: the base the adapter
// resolved for this request (auth.router.ts:785-786) and the request's
// emailLang (:779, :788). LinkBase wins over PasswordResetMailer.BaseURL when
// set; a Lang of "it" or "en" wins over its Locale.
LinkBase string `json:"linkBase,omitempty"`
Lang string `json:"lang,omitempty"`
}
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 password-reset template (TemplatePasswordReset — the built-in, or a TemplateStore's override) and hands the result to a MailerTransport, as the reference's sendPasswordReset does (mailer.service.ts:185-188).
Use it as a sender through its Send method:
mailer, err := auth.NewGatewayMailerTransport(auth.MailerConfig{
Endpoint: endpoint, APIKey: apiKey, From: "noreply@example.com",
})
// handle err
auth.WithPasswordResetSender(auth.NewPasswordResetMailer(
mailer,
"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 what a template mentioning {{.appName}} shows; the built-ins mention none.
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 PasswordVerifier ¶ added in v0.7.0
type PasswordVerifier func(ctx context.Context, user User, password string) (ok bool, migrated bool, err error)
PasswordVerifier is the seam itself: a host-supplied check consulted for a user whose stored hash did not verify the password a login request carried.
It is handed the user record as the store holds it, so that a migration marker the host wrote - a metadata key, a column of its own, a magic placeholder in the hash - is what it keys on, and the plaintext password. A verifier is host code that may log or forward what it receives, so it receives what it needs to answer and nothing else.
Key on that marker first, and answer (false, false, nil) before doing anything else when it is absent. That is a requirement of writing a verifier, not a stylistic flourish, and it follows from which logins reach the hook: every login whose stored hash did not verify does.
- That is not only the imported row. An account that never had a password - OAuth-only, magic-link-only - carries an empty PasswordHash, so its failed logins reach the hook too, carrying whatever plaintext the request held. The reference refuses those before it compares anything (local.strategy.ts:23-25); this port hands them to host code instead, and the marker is what keeps host code from doing anything with them.
- The hook sits on an unauthenticated route. A verifier that calls the old provider for any address it is handed turns POST <prefix>/login into an amplifier aimed at that provider - and at its lockout counters - for every address an attacker cares to name. A marker read out of the user record already in hand costs nothing and cuts that off; whatever call survives the marker must still be bounded and rate-limited by the verifier itself.
The results:
ok=false: not the password. The login answers ErrInvalidCredentials, the same value, and therefore the same 401 body, that a wrong local password produces.
ok=true, migrated=true: the password, and the service should adopt it. The login hashes it at Config.BcryptCost and writes it through UserPasswordStore.UpdatePassword - the path ResetPassword and ChangePassword already write through - before it issues anything. From then on the local hash verifies and the hook is never reached for this user again, which is how the migration ends. A marker the host keeps in its own data is the host's to clear, and the verifier is the natural place to clear it from.
Adoption overwrites whatever hash was stored, not only an empty one. The hook is reached for any hash that failed to verify, so a verifier that is not gated on a migration marker can answer ok for a live account and replace its working local password with whatever password the request carried. This is the sharpest edge on the seam and the marker gate is what blunts it.
The adopted password is deliberately not measured against Config.MinPasswordLen. It is an inherited credential rather than one being chosen here, and refusing it would strand the account on the old system for good; Register, ResetPassword and ChangePassword, where a password is actually chosen, all still enforce the length. The one value refused is the empty string: if a verifier answers ok for an empty password the login proceeds, but nothing is written, because a bcrypt hash of "" would make empty-password login succeed forever and would permanently close the passwordless initial-password path that keys on an empty PasswordHash (wire_password_email.go:347). An empty password behaves as migrated=false.
ok=true, migrated=false: the password, but keep nothing. The login proceeds and the stored hash is left alone, so the next login consults the verifier again. This is for a host that means to keep owning the credential rather than hand this library a copy of it.
A non-nil err is a failure to decide, not a rejection: the old system was unreachable, the call timed out, the answer did not parse. The login fails closed on it and ok is ignored.
The call is made on the request goroutine, inside the login, so a verifier that talks to the network must honour ctx and bound its own time.
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 {
// Sub is the identifier under the name the reference uses and the shipped
// clients require. Both cast it non-nullably — awesome-node-auth-flutter
// does `json['sub'] as String` (auth_user.dart:76) and ng-awesome-node-auth
// declares `sub: string` (auth.service.ts:10) — so omitting it does not
// degrade a client, it throws inside one. It duplicates ID rather than
// replacing it because `id` shipped in 0.2.0 and both clients read that as
// optional, so emitting both breaks nobody.
Sub string `json:"sub"`
ID string `json:"id"`
Email string `json:"email"`
// Role is the reference's single-role field, distinct from Roles. Omitted
// when empty, which it always is until something writes it.
Role string `json:"role,omitempty"`
// LoginProvider is always emitted, LoginProviderLocal when nothing recorded
// one: the reference's /me body is its token payload, which carries
// `loginProvider: user.loginProvider ?? 'local'` unconditionally
// (auth.router.ts:379, :666-668).
LoginProvider string `json:"loginProvider"`
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 ResourceServerConfig ¶ added in v0.6.0
type ResourceServerConfig struct {
// JWKSURL is the full URL of the issuer JWKS endpoint, for example
// https://auth.example.com/auth/.well-known/jwks.json. It is required:
// NewJWKSClient panics on an empty one, and on one that is not an
// absolute http or https URL, rather than building a client that refuses
// every bearer token for the lifetime of the process. The reference refuses
// the same misconfiguration the same way, by throwing when the mode is
// wired without its settings (jwks-auth.middleware.ts:38-40).
JWKSURL string
// Issuer is the expected iss claim. Empty means the claim is not checked,
// which is the reference behaviour for an absent issuer
// (token.service.ts:137).
Issuer string
// CacheTTL is how long a fetched document stays fresh. Zero means
// DefaultJWKSCacheTTL.
CacheTTL time.Duration
// FetchTimeout bounds one fetch. Zero means DefaultJWKSFetchTimeout.
FetchTimeout time.Duration
// MinRefreshInterval is the shortest interval between two refetches forced
// by an unknown kid. Zero means DefaultJWKSMinRefreshInterval; a negative
// value turns the limit off and restores the reference behaviour, which
// refetches on every unknown kid.
MinRefreshInterval time.Duration
// Client is the HTTP client the fetch goes through. Nil means a client of
// this package, which carries no cookie jar and no redirect policy of its
// own. A host that needs a proxy, a pinned CA pool or a tracing transport
// supplies one here.
Client *http.Client
}
ResourceServerConfig is how a deployment points itself at the JWKS of the instance that issues its tokens. It is the reference ResourceServerConfig (auth-config.model.ts:112-140) minus the enabled flag: in Go the flag is the act of constructing the middleware, and unmounting the credential routes is HTTPConfig.ResourceServer.
type RoleLister ¶ added in v0.8.0
RoleLister enumerates every role name defined in the store. It is the reference's IRolesPermissionsStore.getAllRoles (roles-permissions-store.interface.ts:155), consumed by GET /admin/api/roles (admin.router.ts:1129 onwards).
It is separate from RolesPermissionsStore rather than a method on it for the same reason the reference marks it optional: every other method there is keyed on a role, a user or a permission the caller already names, and a store can serve all of them without ever being able to enumerate. Folding this in would make the whole RBAC surface unimplementable for such a store.
There is no limit/offset, matching the reference — the roles of a deployment are a bounded, human-authored set, and the route pairs each name with its permissions in a fan-out it makes no attempt to page (admin.router.ts:1136-1141). The order is the role name ascending, matching GetPermissionsForRole and GetRolesForUser. Roles with no permissions are still roles and are still listed; CreateRole with an empty permission list creates one.
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 `json:"userId"`
TenantID string `json:"tenantId"`
// 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 `json:"phone"`
// Code is the plaintext one-time code; the store holds only its hash.
Code string `json:"code"`
ExpiresAt time.Time `json:"expiresAt"`
}
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) Authenticate ¶ added in v0.5.0
Authenticate verifies an access token and returns the user it names. The token's signature, type, issuer and lifetime are checked, the session is validated when Config.SessionCheckOn is "allcalls", and the user is loaded from the UserStore; the failure sentinels are Me's — ErrInvalidToken, ErrSessionNotFound, ErrSessionRevoked. It is what the adapters' Middleware calls for every request to a protected route.
The user comes back with the optional stores' enrichment — metadata, roles, permissions, tenants — because host handlers behind the middleware read those off UserFromContext for their own authorisation decisions, and each is a local store read that never fails the request: a store error is logged and the field left empty, as it always was. What Authenticate does not do is run Config.BuildTokenClaims, so CustomClaims is nil. The builder is a mint-time hook — with a ClaimsWebhook behind it, a network round trip — and what it computed is already inside the token the request carried; running it again on every request would make each protected route pay for a claim set nothing reads. Me is the call that runs it, for the one route whose body is the profile. This is also where the reference draws the line: its authMiddleware verifies the token and hands the route the verified payload as req.user (auth.middleware.ts:44-61). It does consult the session store — the allcalls check at :47-53 and the last-active touch at :56-58 — but never the user store and never buildTokenPayload; only its /me calls buildPayload again. Authenticate reads the user store in addition, because the Go middleware hands the route a User rather than a payload.
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
ConfirmEmailChange spends an email-change token: the pending address becomes the account's, the token is cleared, and then the OLD address is sent the email-changed notice through Config.SendEmailChanged — the reference's order (auth.router.ts:1056-1066: oldEmail and newEmail read off the user, updateEmail, updateEmailChangeToken(null…), then sendEmailChanged(oldEmail, newEmail)).
A failing notice is returned as an error, joined to ErrDeliveryFailed, with the change already applied. That is the reference's behaviour, reproduced rather than softened: its sendEmailChanged call is inside the route's try block, so a throwing mailer reaches handleError and the route answers the generic 500 for a change that has been committed (:1068-1069). Nothing is rolled back, in either port — the token is spent and the address has moved.
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) ListAllRoles ¶ added in v0.8.0
ListAllRoles asserts on s.rbac, which — unlike s.users and s.sessions — is optional and may be nil. A type assertion on a nil interface value fails rather than panicking, so the one check covers both "no RBAC store configured" and "the configured one cannot enumerate"; the route answers 404 for the first and 501 for the second (admin.router.ts:1130, :1133), and tells them apart by asking whether it was given a store, not by reading this error.
func (*Service) ListAllSessions ¶ added in v0.8.0
func (*Service) ListSessions ¶
func (*Service) ListUsers ¶ added in v0.8.0
func (s *Service) ListUsers(ctx context.Context, tenantID string, limit, offset int) ([]User, error)
ListUsers, ListAllSessions and ListAllRoles resolve the three admin listers. They are the capability half of AdminUserStore, SessionLister and RoleLister: the assertion that fails here is what M8's `GET /admin/api/users`, `/admin/api/sessions` and `/admin/api/roles` turn into the reference's 501 (admin.router.ts:754, :1093, :1133), so ErrFeatureNotSupported on these three means "the configured store cannot enumerate", never "there is nothing there". An empty page and this error are different answers and the router must keep them apart.
Nothing is filtered, clamped or defaulted on the way through. `limit` clamped to 100 and defaulting to 20, `offset` defaulting to 0, the in-memory `filter` over a 500-row batch at offset 0, and the best-effort `total` are all query semantics of the route rather than of the store (admin.router.ts:750-782), and they belong to M8; passing them through untouched is what leaves M8 free to implement them without reopening this seam.
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) Me ¶
Me is Authenticate plus the custom claims: the profile GET /me renders, with CustomClaims filled from Config.BuildTokenClaims so that the body reflects the hook the way the reference's /me does — its body is buildPayload(user) (auth.router.ts:656-680). A builder failure is logged and leaves CustomClaims nil rather than failing the call, as it always has: /me is a read, and a read should not go dark because a mint-time hook is down.
func (*Service) Register ¶
func (s *Service) Register(ctx context.Context, in RegisterInput) (User, AuthTokens, error)
Register creates a user and opens a session for them.
The two fields are required, and their absence is refused before any store is touched — the order the default register handler on the private dev line node-auth works in, which throws AuthError('Email and password are required', 'INVALID_INPUT', 400) before it hashes the password or calls userStore.create (node-auth auth.router.ts:515-525, resolved against DevLineRevision). That is the dev line and not the published reference at ReferenceRevision, whose /register is mounted only when the host supplies options.onRegister (auth.router.ts:713) and which has no default handler and no INVALID_INPUT code at all. Without the check an empty password reached the length test and came back as ErrWeakPassword, which tells a caller who sent no password to choose a stronger one, and an empty address reached the store and created a user with a blank email.
The check runs after normalisation, so an address that is only whitespace is refused here rather than stored as the empty string. The dev line tests the untrimmed body value and would accept it; this is the narrower reading, and it can only refuse a request the dev line would have turned into an account with no usable address.
A successful call also opens a session, which the published reference does not: see the register-issues-a-session entry in CompatibilityNotes.
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 SessionLister ¶ added in v0.8.0
type SessionLister interface {
GetAllSessions(ctx context.Context, limit, offset int) ([]Session, error)
}
SessionLister lists sessions across all users, for the admin sessions table. It is the reference's ISessionStore.getAllSessions (session-store.interface.ts:91), consumed by GET /admin/api/sessions (admin.router.ts:1086 onwards).
Unlike AdminUserStore this carries no tenantID, and the asymmetry is deliberate rather than an oversight. The tenant parameter on the user lister buys reachability: without it, a tenant's users can only be found through a membership table that a single-tenant deployment never populates. Sessions have no such dead end — every session is already listable, and Session.TenantID rides on each record returned — so a tenant parameter here would buy a predicate a caller can apply itself, at the price of a second signature the reference does not have. GET /admin/api/sessions has no tenant notion either.
The order is Session.ID ascending, and the paging rules are the ones stated above.
Nothing is filtered out. Revoked and expired sessions are both returned, which matches SessionAdminStore.ListSessionsForUser and is the same division of labour: this port tombstones a revoked session rather than deleting it, because /refresh needs the tombstone to answer SESSION_REVOKED instead of "not found", so dropping the dead ones is the presentation layer's job and Auth.ListSessions is where it already happens. The reference's interface doc says "all active sessions" (session-store.interface.ts:85) and gets that for free, because its revokeSession deletes the row. A store that also deletes is therefore not in conflict with this rule — it simply has nothing to return.
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 SettingsStore ¶ added in v0.6.0
type SettingsStore interface {
GetSettings(ctx context.Context) (AuthSettings, error)
UpdateSettings(ctx context.Context, patch AuthSettings) (AuthSettings, error)
}
SettingsStore keeps the global runtime settings (settings-store.interface.ts:28-40). It is optional: without one nothing changes, exactly as in the reference, where the settings check on /2fa/disable is skipped altogether when no store is configured (auth.router.ts:890).
GetSettings returns the zero AuthSettings when nothing has been configured — the reference empty object (:31-32) — and reserves the error for the store itself failing. A failing GetSettings fails the route it was called from closed, with the reference generic 500: see Auth.TwoFactorPolicy.
UpdateSettings applies patch as a shallow merge over what is stored: a field the patch does not carry is left untouched (:36-38), a field it does carry replaces the stored one outright. MergeSettings is that rule, and every implementation has to apply it. The reference method returns void and leaves the caller to read the result back; this one returns what is stored afterwards, as TemplateStore does.
A store is wired once with WithSettingsStore (Config.Settings).
type SseConnectionMeta ¶ added in v0.10.0
SseConnectionMeta is the optional user and tenant context a connection is registered with: the reference's `meta` parameter (sse-manager.ts:128).
TenantID is the one that has an effect. An event carrying a tenant id is withheld from a connection whose tenant id is set and different — and, deliberately, from nobody else. A connection registered with no tenant id receives every tenant's events on the topics it holds, which is the reference's rule (sse-manager.ts:211) and is a rule the caller has to mean: it is there so that an operator-facing connection can watch a whole deployment, and it makes an empty TenantID a privilege rather than an omission.
type SseDistributor ¶
type SseDistributor interface {
// Publish sends one event to the distribution bus under topic. An error
// makes the manager deliver the event to its local connections instead.
Publish(ctx context.Context, topic string, event StreamEvent) error
// Subscribe registers fn to receive events from the bus and returns once
// the subscription is established; the pump that calls fn is the
// implementation's own. fn may be called from any goroutine and may be
// called concurrently. ctx bounds the subscription's life: when it is
// cancelled the implementation must stop calling fn.
//
// The reference calls this from its constructor and does not await the
// promise (sse-manager.ts:110-112), so there a subscription that fails
// leaves a manager that silently never receives a remote event. Here the
// error reaches NewSseManager, which refuses to build one.
Subscribe(ctx context.Context, fn func(topic string, event StreamEvent)) error
}
SseDistributor carries events between instances: the reference's ISseDistributor (sse-distributor.interface.ts:10-25).
SSE connections live in the memory of one process. Every deployment that runs more than one process therefore has a fan-out problem, and this is the only seam that solves it — the manager has no other way to reach a connection it does not hold. For awesome-lambda-auth it is not an optional scaling knob at all: on a Function URL every connection is its own execution environment, so a manager without a distributor can only ever deliver an event to the one connection that happens to share an environment with whatever published it.
What a distributor owes, stated here because the reference's interface states none of it and an implementor otherwise has to infer it from the manager's code:
- Ordering. Nothing is required. The manager applies no sequence numbers and no reordering: frames go out in the order Subscribe delivers them, so the order a subscriber sees is the distributor's order and not the publisher's. A distributor that preserves per-topic order gives clients per-topic order; one that does not, does not.
- Duplication. At-most-once is safe: nothing here retries, acknowledges or replays, so a dropped event is simply not delivered and no state is left inconsistent. At-least-once is also safe, but only just — the manager's deduplication compares an event's id against the *previous* event sent to that connection and nothing older, so a redelivery that arrives immediately is absorbed and one that arrives after another event is delivered twice. A distributor that can redeliver out of order should assume its subscribers will see duplicates.
- Unavailability. Publish returning an error makes Broadcast fall back to a local-only delivery (sse-manager.ts:191-194): the event reaches the connections this instance holds and no others. That is the whole of the failure handling — there is no queue, no retry and no signal to the caller, because broadcast returns void in the reference and Broadcast returns nothing here.
The sharpest consequence, and the one to design around: when a distributor is configured, Broadcast does *not* deliver locally. It publishes, and the local connections are served only when the distributor calls back the subscription (sse-manager.ts:190-197). A distributor that does not echo a publisher's own events back to it delivers every event to every instance except the one that raised it. A distributor that fails silently — returns no error and drops the event — delivers it to nobody at all, including the connections in the same process.
type SseManager ¶ added in v0.10.0
type SseManager struct {
// contains filtered or unexported fields
}
SseManager holds the open SSE streams and broadcasts StreamEvents to the ones subscribed to a topic: the reference's SseManager (sse-manager.ts:88).
Topics are matched by exact string equality against the set a connection was registered with (sse-manager.ts:206). The reference documents a hierarchical scheme —
global
tenant:{tenantId}
tenant:{tenantId}:role:{role}
tenant:{tenantId}:group:{groupId}
user:{userId}
session:{sessionId}
custom:{namespace}
— and it is a naming convention and nothing more: no prefix, no wildcard and no ancestor matching exists in either implementation, so a connection holding `tenant:acme` does not receive `tenant:acme:role:admin`. A publisher that wants both sends to both, which is what the deduplication is for.
The server decides a connection's topics. There is no client-supplied channel anywhere in the protocol, which is the property that makes SSE here safe to expose to a browser at all (sse-manager.ts:85-86).
What this port adds, because Go has no Express Response that stays open on its own: Serve blocks the request goroutine for the life of the stream, a bounded queue stands between a publisher and a connection so that one slow reader cannot stall the publisher, and a connection whose queue overflows is disconnected. That last is the one client-visible difference and it is registered as sse-slow-consumer-is-disconnected.
What neither implementation does, stated here because it is the question every consumer asks second: there is no resume. Nothing reads the Last-Event-ID request header a browser sends when its EventSource reconnects, nothing retains a delivered event, and the per-connection lastEventID exists only to compare against the next event's id (sse-manager.ts:214) — it is never read on a new connection, because a new connection is a new entry in the map with an empty one. A client that reconnects resumes from *now*, and the events raised while it was away are gone. The `id:` line is therefore an identity and a deduplication key, not a cursor. Anything that needs delivery across a reconnect has to add an event log of its own, and it would be adding a guarantee rather than implementing one.
func NewSseManager ¶ added in v0.10.0
func NewSseManager(ctx context.Context, opts ...SseOption) (*SseManager, error)
NewSseManager builds a manager. With no options it is the reference's `new SseManager()`: heartbeats every 30 seconds, deduplication on, no distributor.
It takes a context and can fail, where the reference's constructor does neither. Both follow from the distributor: ISseDistributor.subscribe returns a promise the reference neither awaits nor catches (sse-manager.ts:110-112), so a distributor that cannot reach its bus yields a manager that looks healthy and receives nothing from the rest of the fleet. Here the subscription is established before the manager is returned and a failure to establish it is a failure to construct, which is the difference between a deployment that fails at start-up and one that fails silently under load. ctx bounds the subscription and nothing else: cancelling it stops the distributor's callbacks, and open connections are ended by their own requests' contexts.
func (*SseManager) BridgeEventBus ¶ added in v0.10.0
func (m *SseManager) BridgeEventBus(ctx context.Context, bus *EventBus, topics func(Event) []string) (cancel func())
BridgeEventBus broadcasts every event on bus to the topics resolved for it, and returns the function that stops doing so.
This is the seam and not the policy. The reference's equivalent lives in AuthTools.track, which decides the topics, the payload, and the order the telemetry store, the bus, the SSE manager and the webhooks are served in (auth-tools.ts:199-269); that belongs to the AuthTools facade and is U21's, together with the resolveTopics rules. What is here is the minimum that proves the design end to end: a bus event becomes a StreamEvent, reaches a connection, and stops when the subscription is cancelled.
The event's id is generated once and reused across the topics, which is what makes the deduplication do its job — see Broadcast. The StreamEvent's Data is the bus event's own Data; the reference puts the whole telemetry record there instead (auth-tools.ts:250-252), and which of the two a client sees is part of the wire shape U21 settles.
A nil topics resolver publishes to no topic. That is a bridge that delivers nothing rather than a panic recovered once per event in the bus's safeCall.
func (*SseManager) Broadcast ¶ added in v0.10.0
func (m *SseManager) Broadcast(ctx context.Context, topic string, ev StreamEvent)
Broadcast sends ev to every connection subscribed to topic: the reference's broadcast (sse-manager.ts:179-198).
An empty ID and an empty Timestamp are filled, and Topic is overwritten with topic. It returns nothing, as the reference's does, so a failure to reach anyone is not reported to the caller; the fallbacks are described on SseDistributor.
A caller fanning one event across several topics must build the StreamEvent once, with its id, and pass the same value to each call. Letting Broadcast generate an id per call defeats the deduplication and delivers the event once per matching topic to a connection holding several of them; the reference's own caller builds it once for exactly this reason (auth-tools.ts:236-243, :245-247).
func (*SseManager) ConnectionCount ¶ added in v0.10.0
func (m *SseManager) ConnectionCount() int
ConnectionCount is the number of open connections: the reference's connectionCount getter (sse-manager.ts:241-243).
func (*SseManager) Deduplicate ¶ added in v0.10.0
func (m *SseManager) Deduplicate() bool
Deduplicate reports whether events are deduplicated by id per connection: the reference's readonly deduplicate (sse-manager.ts:102).
The name is the reference's and it promises more than it does. What it compares is the incoming event's id against the id of the *last* event sent to that connection, and nothing older (sse-manager.ts:214), so it suppresses an immediate repeat and not a repeat with anything in between. That is enough for the case it exists for — one event fanned out to several topics one after another, where a connection subscribed to two of them would otherwise receive it twice — and it is not a general guard against redelivery. See SseDistributor.
func (*SseManager) Disconnect ¶ added in v0.10.0
func (m *SseManager) Disconnect(connectionID string)
Disconnect closes and forgets one connection: the reference's disconnect (sse-manager.ts:226-236). It is idempotent, and safe to call on an id that is already gone.
The id is the one the client read from the connectionId of its connected frame, which is the only place it is published.
func (*SseManager) HeartbeatInterval ¶ added in v0.10.0
func (m *SseManager) HeartbeatInterval() time.Duration
HeartbeatInterval is the configured heartbeat interval, zero when disabled: the reference's readonly heartbeatIntervalMs (sse-manager.ts:97).
func (*SseManager) Serve ¶ added in v0.10.0
func (m *SseManager) Serve(w http.ResponseWriter, r *http.Request, topics []string, meta SseConnectionMeta) error
Serve registers an SSE connection on w for the given topics and streams to it until the client goes away, the request's context is cancelled, or the connection is disconnected. It returns nil on an ordinary end and an error when the stream failed.
This is the reference's connect (sse-manager.ts:125-172) plus the part Express does implicitly. There, connect writes the headers and the first frame, arms a setInterval for the heartbeat, registers a `close` listener and returns while the Response stays open; the framework holds the socket. Go has nothing that holds it: a handler that returns has ended the response. So the whole life of the connection is inside this call, the request goroutine is the connection's pump, and the two shapes are the same protocol written for two runtimes.
The order of operations is the reference's, and it is observable: the connected frame is written *before* the connection is registered, so an event broadcast in the window between them reaches this connection not at all. The frame also does not seed the deduplication state, because the connection it would seed does not exist yet (sse-manager.ts:140 against :166).
topics is the server's decision and is not read from the request. meta is the user and tenant context; see SseConnectionMeta for what an empty TenantID means.
type SseOption ¶ added in v0.10.0
type SseOption func(*SseManager)
SseOption configures an SseManager. The options exist rather than an options struct because two of the settings are tri-state in the reference — absent means one thing and zero means another — and a Go struct field cannot hold that distinction. WithSseHeartbeat(0) is the reference's `heartbeatIntervalMs: 0`, and passing no option at all is its `undefined`.
func WithSseDeduplicate ¶ added in v0.10.0
WithSseDeduplicate turns per-connection deduplication by event id off. It is on by default, matching `options?.deduplicate !== false` (sse-manager.ts:106).
func WithSseDistributor ¶ added in v0.10.0
func WithSseDistributor(d SseDistributor) SseOption
WithSseDistributor attaches a distributor for cross-instance fan-out. See SseDistributor, and note that attaching one changes where local connections are served from.
func WithSseHeartbeat ¶ added in v0.10.0
WithSseHeartbeat sets the interval between heartbeat comments. Zero disables them, as the reference's 0 does (sse-manager.ts:48-51, :156). A negative value is treated as zero.
func WithSseSendBuffer ¶ added in v0.10.0
WithSseSendBuffer sets how many frames may be queued for one connection before that connection is disconnected. It has no counterpart in the reference, which queues without limit; see the deviation sse-slow-consumer-is-disconnected. Values below one are raised to one.
type StreamEvent ¶ added in v0.10.0
type StreamEvent struct {
// ID is the event's identity: the `id:` line of the frame, and the value
// per-connection deduplication compares. Broadcast fills an empty one with
// a fresh UUIDv4 (sse-manager.ts:184).
//
// A publisher that fans one event out to several topics must generate the
// id once and reuse it, because that is the only thing that makes the
// deduplication work — see the comment on Deduplicate.
ID string `json:"id"`
// Type is the event name: the `event:` line of the frame, which is what an
// EventSource dispatches on.
Type string `json:"type"`
// Timestamp is ISO 8601. Broadcast fills an empty one.
Timestamp string `json:"timestamp"`
// Topic is the channel the event was published to. Broadcast overwrites it
// with the topic it was called with, so a value set here is not honoured;
// the reference's input type omits the field for the same reason
// (sse-manager.ts:181).
Topic string `json:"topic"`
// Data is the payload. It reaches the browser under the key `rawData`, not
// `data` — see sseFrame.
Data any `json:"data"`
// UserID and TenantID are optional context. TenantID is not decoration: it
// is read by the tenant check in broadcastLocal.
UserID string `json:"userId,omitzero"`
TenantID string `json:"tenantId,omitzero"`
// Metadata is optional extra context, carried and never interpreted.
Metadata map[string]any `json:"metadata,omitzero"`
}
StreamEvent is one real-time event as it reaches an SSE subscriber: the reference's StreamEvent (sse-manager.ts:8-25).
The JSON tags are not the tags of the SSE frame. They are the shape a distributor carries, and that distinction is the first thing an implementor of SseDistributor needs: ISseDistributor.publish takes the whole StreamEvent object (sse-distributor.interface.ts:17), so a Redis or an SNS implementation serialises *this*, while the `data:` line of the frame a browser receives is a different, reshaped object — see sseFrame. A distributor written against the reference in TypeScript and one written against this port in Go therefore agree on the bytes they exchange, which is the only reason a mixed fleet can work.
Timestamp is a string rather than a time.Time because it crosses the wire verbatim: the reference produces it with Date.prototype.toISOString (sse-manager.ts:185), and a round trip through time.Time would renormalise the one field whose spelling a client may be matching on. Broadcast fills an empty one in toISOString's own form — UTC, exactly three fractional digits, a literal Z — which is webhookTimestampLayout, already defined for the outgoing webhook envelope in delivery_webhook.go and the same format for the same reason.
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
SessionID string
// CorrelationID is the caller's trace handle, never generated here. See
// EventContext.CorrelationID.
CorrelationID 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: the reference's TelemetryEvent (telemetry-store.interface.ts:4-25).
SessionID and CorrelationID were missing and are the reference's (telemetry-store.interface.ts:18, :20). They arrived with the event vocabulary because the Event that will feed this record now carries both, and a store that could not hold them would drop, on the way to the one consumer that is allowed to see everything, the two identifiers that make a record joinable — to the other events of the same session, and to the request that caused it. Success and Error have no counterpart in the reference and are this port's own; they are left alone here.
type TelemetryFilter ¶
type TelemetryFilter struct {
UserID string
TenantID string
SessionID string
EventName string
Since time.Time
Until time.Time
Limit int
}
TelemetryFilter restricts query results: the reference's TelemetryFilter (telemetry-store.interface.ts:30-39).
SessionID is the reference's `sessionId` (:34), added with the field it filters on. The reference's `offset` still has no counterpart here, which predates this change and is a gap rather than a decision.
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 TemplateStore ¶ added in v0.4.0
type TemplateStore interface {
GetMailTemplate(ctx context.Context, id string) (MailTemplate, bool, error)
ListMailTemplates(ctx context.Context) ([]MailTemplate, error)
UpdateMailTemplate(ctx context.Context, id string, patch MailTemplatePatch) (MailTemplate, error)
GetUITranslations(ctx context.Context, page string) (UITranslation, bool, error)
ListUITranslations(ctx context.Context) ([]UITranslation, error)
UpdateUITranslations(ctx context.Context, page string, translations map[string]map[string]string) (UITranslation, error)
}
TemplateStore keeps mail templates and UI translations (template-store.interface.ts:13-43). It is optional: without one the built-in templates render, exactly as the reference without a templateStore.
The Get methods answer (value, false, nil) for an id or page the store does not hold — the reference's null — and reserve the error for the store itself failing. Update is an upsert in both halves: a missing id is created from an empty template (memory-template.store.ts:16-21) and a page is created on its first translations. Both Update methods return what is stored afterwards, which the reference's void methods leave the caller to read back.
A store is wired once with WithTemplateStore (Config.Templates) and reaches every ready-made mailer the service calls; a MailTemplater used outside a service call reads its own Store field. The UI half — GetUITranslations and its siblings — is kept for the UI router, which is what reads it in the reference (ui.router.ts); nothing in this package serves it yet.
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 TokenClaimsBuilder ¶ added in v0.5.0
TokenClaimsBuilder is the type of Config.BuildTokenClaims and of the hook WithTokenClaimsBuilder accepts: given the user a token is being minted for, it returns the claims to spread over the six base claims. It is an alias, not a distinct type, so a plain func literal satisfies both exactly as it did before the name existed.
The result may override sub, email, role, loginProvider, isEmailVerified and isTotpEnabled; the session claims sid, tid, jti, typ, iss, iat and exp are reserved — written after the merge — and cannot be set from any builder (see issueToken). A builder error fails the mint: the login, refresh or step-up that needed the token answers 500 rather than issuing a token carrying fewer claims than the deployment configured.
func ChainClaims ¶ added in v0.5.0
func ChainClaims(builders ...TokenClaimsBuilder) TokenClaimsBuilder
ChainClaims returns a builder that runs builders in order and merges their results, later ones winning on a shared claim name — so a StaticClaims placed after a UserFieldClaims overrides it, and a hand-written hook placed last has the final say. The first error stops the chain and is returned as it is; the builders after it do not run. Nil builders are skipped, and a chain of none adds nothing.
func StaticClaims ¶ added in v0.5.0
func StaticClaims(claims map[string]any) TokenClaimsBuilder
StaticClaims returns a builder that adds the same claims to every token: the {const: <value>} form of the consumer's extraClaims table.
The map is copied on construction and again on every call, so neither a caller that goes on mutating the map it passed in nor one that edits a returned result can change what the next token carries. A nil or empty map is a builder that adds nothing.
StaticClaims has no error to return, so it cannot refuse a reserved session claim name the way UserFieldClaims does: an entry named sid, tid, jti, typ, iss, iat or exp is discarded by issueToken on every mint, exactly as the same name from a hand-written hook is. A deployment that decodes the {const: ...} table should check the names before building.
func UserFieldClaims ¶ added in v0.5.0
func UserFieldClaims(fields map[string]string) (TokenClaimsBuilder, error)
UserFieldClaims returns a builder that copies fields of the User into named claims: the {fromUserField: <field>} form of the consumer's extraClaims table. fields maps each claim name to the User field it reads, spelled as the field is on GET /me:
id, email, role, tenantId, firstName, lastName, phoneNumber, isEmailVerified, isTotpEnabled, loginProvider
A field name outside that list, or an empty claim name, is an error from this constructor and never from a mint: a mapping is configuration, and a typo in configuration should fail at startup rather than turn every login into a 500. So is a claim name that is one of the reserved session claims — sid, tid, jti, typ, iss, iat, exp: issueToken writes those after the merge, so such a mapping would never reach a token, and a mapping that can never reach a token is a configuration error rather than a claim. The six base claims (sub, email, role, loginProvider, isEmailVerified, isTotpEnabled) are not reserved and may be mapped over, as the reference allows. loginProvider reads as LoginProviderLocal when the user has none recorded, as the base claim and /me do.
Every mapped claim is emitted on every token with whatever the field holds: an empty firstName becomes an empty-string claim, not an absent one. A mapping declares that a claim exists; making its presence depend on the value would leave a consumer unable to tell "not configured" from "empty".
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 when the
// delivery carries no LinkBase of its own — the static fallback, as
// MagicLinkMailer.BaseURL is. A delivery's LinkBase wins over it.
BaseURL string
// Locale selects the built-in template set: "en" (the default) or "it". An
// unknown locale falls back to English, as MailTemplater.RenderMail does.
//
// It is the default, as MagicLinkMailer.Locale is: a delivery whose Lang is
// "it" or "en" — the request's emailLang body field — renders in that
// language instead, and any other Lang defers to this (resolveLang).
Locale string
// Templates renders subject and bodies. The constructors fill this in; set
// Templates.Store to render from a TemplateStore outside a service call
// (inside one, Config.Templates is found on its own).
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 ToolsAccess ¶ added in v0.10.0
type ToolsAccess struct {
// Middleware is the guard every route the reference spreads ...protect onto
// goes behind: track, notify, stream and the telemetry query
// (tools.router.ts:141, :166, :192, :227). Nil is the reference's empty
// slot — no guard — and is only reachable through ToolsPublic.
//
// Two of the six route groups never see it, there or here. The inbound
// webhook is registered with no protect at all (:251), because the caller
// is a third-party provider that has no session to present; and the two
// documentation routes carry none either (:333, :348). U25 owns the first
// of those and should verify the sender some other way.
Middleware func(http.Handler) http.Handler
}
ToolsAccess is the reference's authMiddleware slot (tools.router.ts:43, read at :135), with the third state the reference does not distinguish.
It is a struct behind a pointer for the reason AdminAccessPolicy is: there are three configurations and not two — a guard, a deliberate absence of one, and nothing said at all — and only the first two are configurations this port will serve. A nil *ToolsAccess is "not set", and HTTPConfig.ToolsMounted is false for it.
Build one with ToolsProtected or ToolsPublic rather than by hand, so that the decision reads as one call in a host's configuration.
func ToolsProtected ¶ added in v0.10.0
func ToolsProtected(mw func(http.Handler) http.Handler) *ToolsAccess
ToolsProtected is the ordinary configuration: every guarded route goes behind mw, which is the reference's authMiddleware. A host usually passes its adapter's Middleware() — the same value it protects its own routes with — so that a tools call is authenticated exactly as an API call is.
A nil mw returns nil rather than an unguarded router. Passing one is a host whose middleware variable was never assigned, and a configuration that silently degrades from "guarded" to "open" is the failure this whole file exists to prevent; nil is "not set", so nothing mounts and every tools path answers 404 until the host looks.
func ToolsPublic ¶ added in v0.10.0
func ToolsPublic() *ToolsAccess
ToolsPublic is the reference's own default, asked for by name: no guard on any route, so track, notify and the stream answer to any caller that can reach the mount.
It exists because the reference's default has to stay reachable — a port that removed the configuration would be removing a capability rather than making it explicit — and because a decision spelled in the host's own source is a decision a reviewer can find. Read ToolsAccess and the head of this file before choosing it.
type ToolsFeatures ¶ added in v0.10.0
type ToolsFeatures struct {
// Telemetry is the reference's `telemetry`, and it gates two routes rather
// than one: POST /track/:eventName (tools.router.ts:140) and, when a
// telemetry store with a query is configured, GET /telemetry (:226).
Telemetry bool
Notify bool
Stream bool
Webhook bool
}
ToolsFeatures is the four feature flags resolved: which groups of routes the router mounts, in the reference's own spelling and polarity.
type ToolsOpenAPIInfo ¶ added in v0.10.0
type ToolsOpenAPIInfo struct {
// Title, Description and Version default to the reference's literals with
// the package name changed (openapi.ts:1628-1632). They name the host's
// tools surface, so a caller that wants its own fills them in.
Title string
Description string
Version string
// BasePath is where the tools router is mounted, and the prefix every
// documented path carries: the reference's basePath parameter, whose
// default is '/tools' (openapi.ts:1368). Empty means DefaultToolsPath.
BasePath string
// The four feature flags, in the reference's polarity: each one includes
// the path items of its group. They must match the ToolsOptions the adapter
// was mounted with, or the document describes routes the server does not
// serve — HTTPConfig.ToolsOpenAPIInfo is the way not to have to remember
// that.
//
// Telemetry and Notify select a path item each as well as a tag, from U23
// on. Stream and Webhook still select only the tags block, which is the
// reference's own flag-driven vocabulary declaration (openapi.ts:1621-1625),
// until U24 and U25 mount the routes they gate.
Telemetry bool
Notify bool
Stream bool
Webhook bool
// Docs mirrors ToolsOptions.Docs.Enabled: the two documentation routes the
// adapters then mount, GET <tools>/openapi.json and GET <tools>/docs, are
// described by the document too. Set the two together, or the document
// omits two routes the server serves — or describes two it does not.
//
// The reference's generator describes neither, under any option: it emits
// the four feature groups and stops. That is the same half-deviation
// OpenAPIInfo.Docs carries on the auth document, registered as
// docs-routes-are-opt-in, and it is here for the same reason — the wire
// conformance suite compares the document to the mount in both directions,
// and a document that hid the endpoint serving it would have to be exempted
// from that comparison.
Docs bool
}
ToolsOpenAPIInfo is what the tools document is generated from: the feature flags that select its path items, and the base path they are written under.
It is the reference's Pick<ToolsRouterOptions, 'telemetry' | 'notify' | 'stream' | 'webhook' | 'telemetryStore'> plus basePath (openapi.ts:1366-1369). HTTPConfig.ToolsOpenAPIInfo fills one in from a mounted configuration, which is how the adapters get it; a caller building one by hand is describing a mount it is responsible for matching.
type ToolsOptions ¶ added in v0.10.0
type ToolsOptions struct {
// Enabled mounts the tools router, the way Docs.Enabled mounts the
// documentation routes and Admin.Enabled mounts the console. Unset — the
// default — nothing under Path is registered and every tools path answers
// 404.
//
// Enabled on its own is not sufficient: AuthTools and Access must both be
// set too, which is what HTTPConfig.ToolsMounted reports. See the head of
// this file for the second of those.
Enabled bool
// Path is where the tools router mounts, on the same router the adapter was
// given and beside HTTPConfig.APIPrefix rather than under it. Empty means
// DefaultToolsPath, which is the reference's own default (:127).
Path string
// AuthTools is the facade every route of this router calls: the reference's
// first argument to createToolsRouter (:117). Nil mounts nothing, because a
// router whose every handler needs it would answer 500 on each of them.
//
// It is here, and not on the Auth, because U21 left the question open in
// exactly these terms: how the facade reaches the adapters is route
// mounting, and route mounting is what HTTPConfig describes. Nothing in the
// service layer calls it — Track and Notify are a host's fan-out, not the
// library's — so an Option that put it on the Auth would be storing a value
// only the adapters read. NewAuthTools takes a context and returns an
// error, so the host builds it and hands it over, the way it hands over a
// RateLimiter.
AuthTools *AuthTools
// Access decides who reaches the guarded routes. Nil is "not configured"
// and mounts nothing at all; ToolsProtected(mw) is the ordinary
// configuration and ToolsPublic() is the reference's default.
//
// This is the field the head of this file is about.
Access *ToolsAccess
// The four feature flags, spelled in the negative because the reference's
// default is on and Go's zero value is off (tools.router.ts:121-124). A
// zero ToolsOptions therefore selects what `createToolsRouter(tools, {})`
// selects: all four groups. Naming them for what a host has to set to
// change that is the stdlib's own answer to this (http.Transport's
// DisableKeepAlives, DisableCompression), and it beats a *bool whose three
// states are two.
//
// Each of them gates the routes of one group and nothing else. Read them
// through Features, which resolves the negation once.
DisableTelemetry bool // the reference's `telemetry: false` — POST /track/:eventName and GET /telemetry
DisableNotify bool // `notify: false` — POST /notify/:target
DisableStream bool // `stream: false` — GET /stream
DisableWebhook bool // `webhook: false` — POST /webhook/:provider
// Docs mounts this router's own two documentation routes, GET
// <tools>/openapi.json and GET <tools>/docs (tools.router.ts:332-352).
//
// It is DocsOptions, the same type HTTPConfig.Docs is, because the
// reference's two options are the same pair: `swagger` and
// `swaggerBasePath` (:93, :101) against the auth router's (auth.router.ts:
// 123-139). Everything that type's doc comment says applies here — in
// particular that Enabled is a plain bool where the reference's option is
// also the string "auto", and that the page it serves loads swagger-ui-dist
// from a CDN.
//
// BasePath is this router's swaggerBasePath: empty means ToolsPath(). It
// moves the description and the page's spec URL, never the mount.
Docs DocsOptions
}
ToolsOptions is HTTPConfig.Tools: whether the adapters mount the tools router, where, with which feature groups, and behind what.
It is the reference's ToolsRouterOptions (tools.router.ts:13-102) less the store and callback slots that belong to the routes that read them — telemetryStore, webhookStore, settingsStore and onWebhook arrive with U25, which owns the two routes that consult them.
func (ToolsOptions) Features ¶ added in v0.10.0
func (o ToolsOptions) Features() ToolsFeatures
Features resolves the four flags. It is the one place the negation is undone, so the switch in ToolsHandler, the OpenAPI document and any later route read the same answer.
type TrackOptions ¶ added in v0.10.0
type TrackOptions struct {
UserID string
TenantID string
SessionID string
CorrelationID string
IP string
UserAgent string
}
TrackOptions is the metadata of one tracked event: the reference's TrackOptions (auth-tools.ts:87-94), field for field.
All six are optional. Each one that is set both lands on the record the telemetry store keeps and — for the three that name a principal — adds a topic the event is broadcast to; see EventTopics.
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 UIBranding ¶ added in v0.7.0
type UIBranding struct {
PrimaryColor string
SecondaryColor string
LogoURL string
SiteName string
CustomLogo string
BgColor string
BgImage string
CardBg string
}
UIBranding is the static branding block, under the reference's field names (auth-config.model.ts:353-380). Every member is optional, and the empty string means unset: the reference's chain of `||` treats an empty string and an absent key alike, so a value configured empty falls through to the next candidate exactly as a missing one does.
CustomLogo and LogoURL are both here because the reference has both, and because it prefers the first: the served logoUrl is the stored settings.ui.logoUrl, then config.ui.customLogo, then config.ui.logoUrl (ui.router.ts:128). customLogo is the documented field (auth-config.model.ts:354-355) and logoUrl is kept there as a legacy one (:357-361).
type UIConfig ¶ added in v0.7.0
type UIConfig struct {
APIPrefix string `json:"apiPrefix"`
Features UIFeatures `json:"features"`
UI UIConfigBranding `json:"ui"`
Translations map[string]string `json:"translations"`
Lang string `json:"lang"`
// Headless echoes UIOptions.Headless. It is the one member the route adds
// rather than getUiConfig (ui.router.ts:166-168), which is why it is last
// and why the error fallback carries it too.
Headless bool `json:"headless"`
}
UIConfig is the body of GET <prefix>/ui/config, with the reference's keys in the reference's order (ui.router.ts:136-142 plus :168).
apiPrefix is where the API routes are mounted, which is what lets the UI be served from one path and call the API at another. The reference derives it from the mounted router's baseUrl with a trailing /ui removed (:98); here it is HTTPConfig.Prefix, which is the same value by construction.
translations is the flat key -> value map for lang, never null: a deployment with no template store, a store holding no config page, or a page holding neither the requested language nor English all yield the empty object (:105-112).
type UIConfigBranding ¶ added in v0.7.0
type UIConfigBranding struct {
PrimaryColor string `json:"primaryColor"`
SecondaryColor string `json:"secondaryColor"`
LogoURL string `json:"logoUrl,omitempty"`
SiteName string `json:"siteName"`
CustomCSS string `json:"customCss,omitempty"`
BgColor string `json:"bgColor,omitempty"`
BgImage string `json:"bgImage,omitempty"`
CardBg string `json:"cardBg,omitempty"`
}
UIConfigBranding is the served ui object (ui.router.ts:125-134): the static branding with the stored settings applied on top.
The five optional members are omitted rather than sent null, which is what JSON.stringify does with the undefined the reference leaves them as. The three that are always present have defaults behind them and can never be empty.
customCss is here and customLogo is not: the logo is resolved into logoUrl, and the CSS is passed through from the static configuration.
type UIFeatures ¶ added in v0.7.0
type UIFeatures struct {
Register bool `json:"register"`
MagicLink bool `json:"magicLink"`
SMS bool `json:"sms"`
Google bool `json:"google"`
GitHub bool `json:"github"`
ForgotPassword bool `json:"forgotPassword"`
VerifyEmail bool `json:"verifyEmail"`
TwoFactor bool `json:"twoFactor"`
// contains filtered or unexported fields
}
UIFeatures is the served features object (ui.router.ts:114-123): which login affordances the UI should offer. Each flag says that this deployment can actually perform the thing, which is why they are derived from the wiring rather than configured.
- register is always true. The reference mounts POST /register only when routerOptions.onRegister is supplied and reports that same condition here (auth.router.ts:712-715, ui.router.ts:115); this port always mounts the route, so the honest answer about the routes it serves is always true. Registered as the deviation register-route-is-always-mounted.
- magicLink, sms, forgotPassword and verifyEmail follow the delivery seam: Config.SendMagicLink, Config.SendSMSCode, Config.SendPasswordReset and Config.SendEmailVerification. The reference reads config.email.sendX or a configured mailer for the three email ones and config.sms for the other (:116-121); this port has no mailer block on Config, so the sender is the whole condition.
- verifyEmail additionally requires the deployment to want verification at all: Config.EmailVerificationMode other than none, as normalised by the service. The reference asks emailVerificationMode !== 'none' || requireEmailVerification (:121), and an unset mode passes that test while behaving as none everywhere else in its own code (auth-config.model.ts:286-289). This port has no requireEmailVerification and normalises an unset mode to none throughout, so it answers false where the reference answers true for a deployment that configured a verification sender and left the mode alone. Registered as the deviation ui-config-verify-email-follows-the-effective-mode, which is where the reasoning lives.
- google and github report a configured OAuth provider of that name (:118-119, WithOAuth here).
- twoFactor is Config.TwoFactorAppName, this port's spelling of the reference's twoFactor block, which holds nothing but appName (auth-config.model.ts:272-274, ui.router.ts:122). Config.Require2FA is not read: it makes the factor mandatory, not available, and the reference has no equivalent of it.
func (UIFeatures) MarshalJSON ¶ added in v0.7.0
func (f UIFeatures) MarshalJSON() ([]byte, error)
MarshalJSON writes the eight flags, or the three the error fallback writes.
The fallback object really is shorter (ui.router.ts:147): a client that keyed on features.magicLink gets undefined from a deployment whose settings store is down, rather than false. That is a bug in the reference — it drops five flags on an error path the client cannot distinguish from success — and it is reproduced rather than fixed (reference-issues N32), because the family's clients are written against the document as it is and a port that filled the five in would be answering something no node-auth server ever answers.
The flag is unexported and set in one place, so the type stays a plain collection of booleans for anyone building or reading one. Round-tripping a collapsed value through an encoder and a decoder yields an uncollapsed one.
type UIOptions ¶ added in v0.7.0
type UIOptions struct {
// Enabled mounts the UI. The reference gates its whole ui router on
// config.ui.enabled (auth.router.ts:1639-1648); this port gates
// GET <prefix>/ui/config on the same flag, so with it false the route is not
// registered and the router answers whatever it answers for an unknown path.
//
// It is also what points an emailed link at a UI page rather than at the API
// route itself — see HTTPConfig.UILink. The deprecated HTTPConfig.UIEnabled
// is an alias for it.
Enabled bool
// Headless is the reference's config.ui.headless (auth-config.model.ts:343):
// the SPA case, in which the hosting application provides its own pages and
// the router serves only this document and the static assets.
//
// It decides two things. UIHandler short-circuits on it and serves no HTML
// page at all (ui.router.ts:172-183), and it is echoed in this document,
// which is where auth.js reads it from to stop redirecting on an expired
// session (:334-336, ui.router.ts:168).
Headless bool
// Branding is the static half of the served ui object: the deployment's
// colours, logo and site name, each of which a settings store may override
// per request. See UIBranding.
Branding UIBranding
// CustomCSS is the reference's config.ui.customCss
// (auth-config.model.ts:348-353), served as the ui object's customCss. It is
// the one member of that object a settings store cannot override, because
// the reference reads it from the static config alone (ui.router.ts:130).
CustomCSS string
// DefaultLang is the language the document falls back to when the request
// carries no lang query parameter. The reference reads that fallback from
// config.email.mailer.defaultLang (ui.router.ts:103); this port has no
// mailer block on Config — MailerConfig belongs to the transport a host
// builds — so the UI's own default lives here. Empty means UIDefaultLang.
DefaultLang string
// Assets is the reference's uiAssetsDir (ui.router.ts:11-14): the built-in
// pages and scripts UIHandler serves and renders. nil — the default — is the
// vendored copy of the reference's own assets, UpstreamUIAssetFS(), which is
// that option's "if not provided, the internal Vanilla JS UI will be
// served". A host supplying its own set is taken at its word: the pages are
// looked up in it and nothing falls back to the vendored ones, because a
// half-replaced UI is worse than a missing one.
//
// It is an fs.FS rather than a directory path so that a host can serve a
// built SPA from its own embed.FS without unpacking it, and os.DirFS is the
// one-line spelling of the reference's directory.
Assets fs.FS
// Uploads is the reference's uploadDir (:16-20): the logos and backgrounds
// an administrator uploaded, served under both <prefix>/ui/assets/logo/ and
// <prefix>/ui/assets/uploads/ (ui.router.ts:185-191).
//
// It is read-only on purpose, and U15 is where the other half arrived:
// Config.Uploads is the UploadStore the admin console writes through, and
// UploadFS presents one as exactly this fs.FS. UIHandler composes the two
// so that a host wires one thing:
//
// - Uploads set wins, always. A host that wants the read side served from
// somewhere else — a read-through cache, a snapshot, an os.DirFS over a
// mounted volume — says so here and the store is not consulted.
// - Uploads nil with an UploadStore configured serves that store's
// objects, through UploadFS. This is the ordinary arrangement and is
// what makes the URL the upload routes hand back resolve against this
// same deployment.
// - Neither mounts neither path, exactly as the reference's
// `if (uploadDir)` leaves them unmounted, so an unconfigured deployment
// answers 404 there rather than failing on a store it does not have.
//
// The one thing UploadFS cannot carry is a request context: fs.FS.Open takes
// none. A host that needs per-request cancellation on the read path sets
// this field to its own fs.FS. See UploadFS.
Uploads fs.FS
}
UIOptions is the reference's config.ui block (auth-config.model.ts:321-381): everything the built-in UI is configured with. It reaches the route as HTTPConfig.UI.
type UISettings ¶ added in v0.6.0
type UISettings struct {
PrimaryColor *string `json:"primaryColor,omitempty"`
SecondaryColor *string `json:"secondaryColor,omitempty"`
LogoURL *string `json:"logoUrl,omitempty"`
SiteName *string `json:"siteName,omitempty"`
LogoPath *string `json:"logoPath,omitempty"`
BgColor *string `json:"bgColor,omitempty"`
BgImage *string `json:"bgImage,omitempty"`
CardBg *string `json:"cardBg,omitempty"`
}
UISettings is the reference's AuthSettings.ui (settings-store.interface.ts:79-91): the branding the admin UI customization panel writes, which the UI config route will serve once it lands.
The fields are pointers so that a patch can carry one of them without blanking the other seven, and omitempty so that a key nobody ever set is absent rather than null — the reference stores whatever object the admin router last merged (admin.router.ts:979-981) and JSON.stringify emits only the keys it holds. The reference types every field as required inside the optional ui object; in practice its own PATCH route writes partial objects, so required is the type rather than the data.
type UITranslation ¶ added in v0.4.0
type UITranslation struct {
Page string `json:"page"`
Translations map[string]map[string]string `json:"translations"`
}
UITranslation is the stored translation set of one UI page (template-store.interface.ts:8-11): lang -> key -> value.
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 UploadStore ¶ added in v0.10.0
type UploadStore interface {
Put(ctx context.Context, key string, content io.Reader) (UploadedFile, error)
List(ctx context.Context) ([]UploadedFile, error)
Open(ctx context.Context, key string) (io.ReadCloser, UploadedFile, error)
Delete(ctx context.Context, key string) error
}
UploadStore is where uploaded assets live: the seam over the reference's uploadDir. It is optional, and leaving it nil is not a degraded mode — it is the deployment that has no upload feature at all. The four upload routes are then not registered, the way GET <admin>/api/templates/* is registered only with a TemplateStore, and adminFeatures reports upload false so the console draws no file picker.
Keys ¶
Every key passed to any method must satisfy ValidUploadKey, and an implementation may assume it does — every call this package makes is checked first. An implementation that is also called directly by its host should check too, because that grammar is the only thing standing between a key and the storage namespace behind it: see ValidUploadKey for why checking for "../" is the wrong shape of defence.
Put ¶
Put stores content under key, replacing whatever was there. It must read content to EOF, and it must leave nothing behind if content returns an error — a half-written object under a key a listing will report is worse than a failed upload, and a store that cannot write atomically should write to a temporary key and rename, or delete what it wrote before returning. The returned UploadedFile describes what is now stored.
content is bounded at UploadMaxBytes by the route that calls it, and the reader the route passes is an *bytes.Reader — so an implementation that wants a length or a checksum before it writes can type-assert io.Seeker rather than buffer a second copy.
List ¶
List returns every object the store holds, newest first, ties broken by key ascending. That order is part of the interface so a caller can rely on it without sorting, and it is total so that two stores holding the same objects answer alike.
The listing route re-applies the same order over the result anyway. That is not distrust of a particular implementation: the reference's route sorts what readdirSync handed it (admin.router.ts:1057) because readdir order is the filesystem's business, and a wire contract that holds only while every host's store is diligent is not a wire contract.
Open ¶
Open reads one object back, for the UI's asset mounts and for anything else that wants the bytes. It returns ErrUploadNotFound for a key the store does not hold, and the caller closes the reader. Returning an io.ReadSeeker where that is cheap lets the UI serve Range requests without buffering; see UploadFS.
Delete ¶
Delete removes one object and returns ErrUploadNotFound if there was none.
Concurrency ¶
Every method may be called concurrently.
type UploadedFile ¶ added in v0.10.0
type UploadedFile struct {
// Name is the key. The field is called name because the reference's key is
// called name, and a client reading this listing reads that.
Name string `json:"name"`
Size int64 `json:"size"`
ModTime time.Time `json:"mtime"`
}
UploadedFile is one stored object as the listing route reports it, and as Open and Put hand it back: the reference's `{ name, size, mtime: stat.mtime.toISOString() }` (admin.router.ts:1051-1055).
The JSON tags are that object's three keys, because this struct is what GET <admin>/api/upload/files serialises. mtime is written by encoding/json's RFC 3339 rather than by JavaScript's toISOString, so it carries no fixed three-digit fraction — the same treatment every other timestamp on the admin API already gets (adminUserRow.CreatedAt).
type UpstreamUIAsset ¶ added in v0.9.0
type UpstreamUIAsset struct {
// Name is the file's base name, which is both its name upstream and its
// name under ui/upstream/assets.
Name string `json:"name"`
// UpstreamPath is the path in the reference tree, relative to that tree's
// root, so the value carries enough to locate the original on its own.
UpstreamPath string `json:"upstreamPath"`
// Commit is UpstreamUIAssetCommit, carried per entry so that a caller which
// serialises this table ships the revision the hashes belong to.
Commit string `json:"commit"`
// Size is the file's length in bytes.
Size int `json:"size"`
// SHA256 is the lowercase hex sha256 of the file's contents — of the LF
// bytes the reference publishes, which is what `git cat-file blob` emits
// and what the .gitattributes entry for these paths keeps on disk.
SHA256 string `json:"sha256"`
}
UpstreamUIAsset is one vendored file and the provenance that makes it checkable: where it came from, and what it has to hash to.
func UpstreamUIAssets ¶ added in v0.9.0
func UpstreamUIAssets() []UpstreamUIAsset
UpstreamUIAssets returns the provenance table: every vendored asset, where it came from, and what it hashes to.
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. UpstreamPath and Commit are filled in here rather than repeated on all fourteen rows of the literal above, because they are the same for every entry and a repeated constant is a constant that can be got wrong once.
type User ¶
type User struct {
ID string
Email string
PasswordHash string
TenantID string
PhoneNumber string
FirstName string
LastName string
Role string
// IsAdmin is the reference's BaseUser.isAdmin (user.model.ts:81-90): the
// stored flag the admin router's 'is-admin-flag' access policy reads, as
// `granted = user.isAdmin === true` (admin.router.ts:370).
//
// It is a persisted field and not a projection of Role, and that is the
// reference's own arrangement rather than a choice made here. The obvious
// reading — that an admin is a user whose role says so — is the one the
// reference explicitly declines: its doc on the field routes anyone wanting
// role-shaped admin-ness away from it and to RBAC instead ("For more
// granular RBAC control use IRolesPermissionsStore and a custom accessPolicy
// function"), and buildPolicyGuard reads no role anywhere. The four access
// policies it does offer are 'open', 'first-user', this flag, and a
// host-supplied predicate that is handed the user and the RBAC store
// (admin.router.ts:25-30, :366-382) — so a deployment that wants
// `Role == "admin"` writes six lines of predicate, and the flag stays what it
// is: a switch a host sets on a record, with no derivation behind it.
//
// Deriving it here anyway, in the shape of models.go's own
// loginProviderOrLocal, would therefore not be reproducing the reference but
// inventing a rule it does not have — and inventing it in the one place where
// guessing wrong grants the admin console to the wrong person. A store that
// never persisted the column keeps working: absent is false in TypeScript
// (the field is `isAdmin?: boolean`, and the guard compares `=== true`) and
// false is the zero value here, so the two agree without a migration.
//
// It is deliberately not projected onto PublicUser. The reference serialises
// isAdmin nowhere — not on /me, not on the admin users table, which picks its
// keys one by one and does not pick this one (admin.router.ts:764-773) — so no
// shipped client knows the field exists, and adding it to the wire would be an
// unrequested widening of the contract that also tells every caller which
// accounts are worth attacking.
IsAdmin bool
// LoginProvider names the identity provider that created the account: an
// OAuth provider's name for a user OAuthService.HandleCallback created,
// empty for one registered with a password. Empty is read as
// LoginProviderLocal wherever it reaches the wire — the reference's
// `user.loginProvider ?? 'local'` (auth.router.ts:379) — so a store that
// never persisted the field needs no migration to keep answering "local".
LoginProvider 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.
func AdminUserFromContext ¶ added in v0.10.0
AdminUserFromContext returns the principal the guard resolved, and whether there was one. It is the reference's `(req as any).user` (admin.router.ts:341, :390) and it is what every route behind AdminGuard.Protect reads.
The second result is false under AdminPolicyOpen and under the legacy secret, where the reference calls next() with no user on the request either: those two policies authorise a request without ever identifying anybody.
func UserFromContext ¶ added in v0.6.0
UserFromContext reads the authenticated principal a middleware stored.
func UserFromRS256Claims ¶ added in v0.6.0
UserFromRS256Claims builds the principal a resource server hands its handlers from verified RS256 claims, without reading any store.
This is the whole point of resource-server mode: the instance has no user database, so the token is the user record. The six named fields are the reference base payload (auth.router.ts:378-384) — sub, email, role, loginProvider, isEmailVerified, isTotpEnabled — plus tid, which this port writes as the tenant (token.go issueToken) and which a tenant-scoped handler cannot do without. Everything in the token, those included, is also left in CustomClaims, because the reference assigns the entire payload (jwks-auth.middleware.ts:69) and a scope or permission claim an issuer adds has to reach the handler somehow.
Fields the token cannot carry are zero: no PasswordHash, no Roles, no Permissions, no Tenants, no CreatedAt. A handler that needs them has to read its own store.
The name says RS256 because that is the path it was written for, but the mapping is the claim set, not the algorithm: the middleware's cookie path builds its principal with this same function, from the claims of a locally verified HS256 token. Both are the same six base claims plus tid, because issueToken writes exactly them (token.go).
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 UserAdminFlagStore ¶ added in v0.10.0
type UserAdminFlagStore interface {
UpdateIsAdmin(ctx context.Context, userID, tenantID string, isAdmin bool) error
}
UserAdminFlagStore sets User.IsAdmin on one user. It is the seam POST <admin>/users/:id/promote needs for method=flag, and the assertion's failure is that route's 501.
Why it is one flag and not the reference's patch ¶
The dev line's seam is a *general* writer: it casts the configured store to `IUserStore & { update?(userId, patch: Partial<BaseUser>): Promise<void> }` and calls it with the object literal `{isAdmin: true}` (node-auth admin.router.ts:1035-1040, the type at :212-214). A faithful transcription would be `Update(ctx, userID string, patch map[string]any) error` or a *User-shaped patch type, and both were declined:
- Partial<T> has no Go counterpart that keeps the field names checked. A map[string]any patch moves every field name from the compiler to a string literal, on the one call in this package that grants somebody the admin console.
- The route is the interface's only consumer and writes one field. A seam wider than its consumer is a seam an implementor has to guess the contract of — "which fields may arrive, and what does a store do with one it does not persist?" — and nothing in either tree answers that, because nothing in either tree calls update with any other patch.
So it is narrowed to what the route actually does, the way UserTwoFactorPolicyStore is narrowed to the one flag its route writes. The wire is unaffected: the 501 a store that does not implement this gets is the reference's own message, naming the reference's own method.
What an implementor owes ¶
Write the flag and nothing else. This is the field the 'is-admin-flag' access policy reads (admin.go, AdminPolicyIsAdminFlag) — a store that also granted a role, or cleared one, would be making an authorisation decision the route did not ask for and the reference does not make. It is also why there is no revoke beside it: neither tree has a route that clears the flag, and adding one here would be a product decision rather than a port. AuthConfigurator's revokeAdmin is the dev line's only such site and it is a facade method, not a route (node-auth auth-configurator.ts:133).
tenantID is the parameter the reference has not got, carried for the reason UserAccountStore.DeleteUser carries one: every seam on this surface has it, so a store implements one convention rather than two. The route passes "", because the reference's update(id, patch) addresses a user by id alone — see (*Auth).adminPromoteUser for what that costs a multi-tenant deployment, which is exactly what DELETE <admin>/api/users/:id already costs it.
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 UserTwoFactorPolicyStore ¶ added in v0.10.0
type UserTwoFactorPolicyStore interface {
UpdateRequire2FA(ctx context.Context, userID, tenantID string, required bool) error
}
UserTwoFactorPolicyStore sets the require-2FA flag on one user. It is the reference's optional IUserStore.updateRequire2FA — a method its interface does not declare and which the admin router reaches for with a `typeof … === 'function'` test (admin.router.ts:827, :649) — expressed here the way SessionAdminStore and AdminUserStore are: a narrow interface the core type-asserts the configured store to, with the assertion's failure answering the reference's own 501.
It has exactly one consumer, POST /admin/api/2fa-policy, which walks the whole user table in pages of 100 and calls this once per row. Two consequences follow for an implementor:
- It is called N times for a table of N users and it is not batched. That is the reference's shape (admin.router.ts:843) and there is no bulk form to offer instead, because the route has no transaction either: a failure part-way leaves the prefix already written and answers 500.
- It must write the flag and nothing else. The route is a policy switch and the value it carries is the administrator's; a store that also cleared a TOTP secret or revoked a session on the way through would be making a decision the route did not ask for and the reference does not make.
tenantID is the parameter the reference has not got, added for the reason every other write in this file carries one: the walk lists users across every tenant, so the row's own TenantID is the only thing that says which user an id names. Together with AdminUserStore it is also what turns the console's twoFAPolicy feature flag on — the reference requires both methods for the same flag (:649-650).
type VerifyEmailInput ¶
type VerifyEmailInput struct {
Token string
}
type WebhookAdminStore ¶ added in v0.8.0
type WebhookAdminStore interface {
// ListWebhooks returns one page of every configuration the store holds,
// active or not, in the store's documented order (:119-123).
//
// offset is zero-based. A limit of zero or less returns no rows, as LIMIT 0
// does; an offset past the end returns an empty slice, not an error. The
// reference clamps limit to 100 and defaults it to 20 in the route rather
// than in the store (admin.router.ts:1366-1367), so the store honours what
// it is given.
ListWebhooks(ctx context.Context, limit, offset int) ([]WebhookConfig, error)
// AddWebhook stores a new configuration and returns it with the ID the
// store assigned (:125-129).
//
// The reference's add takes an Omit<WebhookConfig, "id">, which Go has no
// spelling for; the shape this package already uses for the same thing is
// CreateTenant and CreateUser in store.go — take the value, return what was
// stored — so that is the shape here. The difference from those two is that
// the store owns the identifier: config.ID is ignored, and reading the
// assigned one off the returned value is the only way to get it.
//
// The configuration is stored as given otherwise. No field is defaulted on
// the way in: Active, Retries and RetryDelay resolve nil at the point of
// use, and the reference's own defaulting of events to ["*"] and isActive
// to true happens in the admin route (admin.router.ts:1403-1404), which is
// U14's to reproduce.
AddWebhook(ctx context.Context, config WebhookConfig) (WebhookConfig, error)
// UpdateWebhook applies a partial update (:137-141). An id the store does
// not hold is not an error — see WebhookPatch.
UpdateWebhook(ctx context.Context, id string, patch WebhookPatch) error
// RemoveWebhook permanently deletes a configuration (:131-135). An id the
// store does not hold is not an error: the reference's remove returns void
// and its route answers {"success": true} without asking whether anything
// was deleted (admin.router.ts:1429-1440), so a second DELETE of the same
// id succeeds.
RemoveWebhook(ctx context.Context, id string) error
}
WebhookAdminStore is the management half of the reference's IWebhookStore — listAll?, add?, remove? and update? (webhook-store.interface.ts:119-141) — as one optional capability. A store that does not implement it can still serve events; only the admin Webhooks screens need it, and those are U14 (M8).
The four are separately optional in the reference, which guards each call site and answers its own 501 per method ("IWebhookStore.listAll is not implemented", admin.router.ts:1368-1371, and the three that follow). They are bundled here because they exist for one screen over one store, the way SessionAdminStore bundles the reference's equally-separate getAllSessions? and deleteExpiredSessions? (session-store.interface.ts:91, :100), and because bundling costs nothing on the wire: a store that can list but not write implements all four and returns ErrFeatureNotSupported from the writers, which HTTPErrorFor already maps to the same 501 as a failed type assertion (wire.go:187). U14 reaches the reference's per-method answer either way.
type WebhookAttempt ¶ added in v0.10.0
type WebhookAttempt struct {
// ConfigID is WebhookConfig.ID: which subscription this delivery belongs
// to. It is here for a host's logs, metrics and dead-letter routing, and it
// is the only part of the configuration that crosses the seam — the URL
// below is the request's, and the secret never crosses at all.
ConfigID string
// URL is where to POST Body.
URL string
// Event is the event name, the same string as the X-Webhook-Event header.
Event string
// DeliveryID is the X-Webhook-Delivery value for this attempt.
//
// Fresh on every attempt, not once per event: the reference mints it inside
// the retry loop (webhook-sender.ts:27), so two attempts at the same
// envelope carry two different ids and a receiver cannot use it to
// deduplicate. See WebhookDeliverer for what that means for a host that
// redelivers.
DeliveryID string
// Attempt is zero for the first try and increments by one per retry.
Attempt int
// Remaining is how many further attempts the sender will make if this one
// returns an error. Zero means this is the last one, and a host that
// dead-letters wants to know that before it returns.
Remaining int
// Headers are the request headers, complete: Content-Type and the four
// X-Webhook-* the reference sends, with X-Webhook-Signature present only
// when the subscription carries a secret. Send them as given. The signature
// covers Body exactly, so an added, dropped or rewritten header is
// survivable and a changed body is not.
Headers http.Header
// Body is the serialised OutgoingWebhookEvent: the bytes the signature was
// computed over, and the bytes that have to arrive.
Body []byte
}
WebhookAttempt is one prepared delivery: everything a transport needs and nothing it has to decide.
The sender fills every field before calling WebhookDeliverer, so an implementation neither serialises the envelope nor signs it nor chooses a delivery id. It is safe to serialise this value onto a queue as it stands — it is the request, not the subscription, and it deliberately carries no secret.
func (WebhookAttempt) NewRequest ¶ added in v0.10.0
NewRequest builds the POST this attempt describes. It is what HTTPWebhookDeliverer calls, and it is exported for a host whose deliverer is still HTTP — one that adds a proxy, a client certificate or its own instrumentation — so that the header set stays in one place.
The returned request owns a copy of Headers and reads Body from memory, so it is safe to retry.
type WebhookConfig ¶ added in v0.8.0
type WebhookConfig struct {
// ID is assigned by the store, not by the caller: the reference's add takes
// an Omit<WebhookConfig, "id"> (:129). See WebhookAdminStore.AddWebhook.
ID string `json:"id"`
// URL is the endpoint events are POSTed to (:8). The store does not
// validate it; the admin route refuses an empty one before it ever reaches
// add (admin.router.ts:1397), and that check is U14's.
URL string `json:"url"`
// Events are the event names this configuration subscribes to (:14), or the
// single entry WebhookEventWildcard for all of them.
//
// An empty or nil Events matches nothing, and that is the reference's rule
// rather than a convenience: its matching is a positive containment test,
// so a row with no events is subscribed to none (:106). The removed
// WebhookDispatcher read an empty list the other way round, as every event,
// which is one more reason it could not simply be pointed at a store. The
// reference's admin route defaults a create with no events to ["*"]
// (admin.router.ts:1403), which is where that convenience lives there.
Events []string `json:"events"`
// Secret is the HMAC-SHA256 signing key (:16); empty means the delivery is
// unsigned. It is held in the clear, because the signature has to be
// reproducible and the reference holds it in the clear too — the admin
// listing is what masks it to "***" (admin.router.ts:1381), not the store.
Secret string `json:"secret,omitempty"`
// IsActive gates delivery (:18) and defaults to true when nil — see Active,
// which is the only place that default is applied.
IsActive *bool `json:"isActive,omitempty"`
// TenantID scopes the configuration to one tenant (:23); empty is a global
// webhook, which fires for every tenant. See WebhookStore.FindByEvent.
TenantID string `json:"tenantId,omitempty"`
// MaxRetries and RetryDelayMs are the delivery retry policy (:28, :33),
// resolved and acted on by WebhookSender.Send. Retries() and RetryDelay()
// below are how to read them: never dereference these pointers directly,
// because nil is the reference's default and a stored 0 is not.
MaxRetries *int `json:"maxRetries,omitempty"`
RetryDelayMs *int `json:"retryDelayMs,omitempty"`
// Provider, AllowedActions and JSScript are the inbound half of the
// reference's WebhookConfig (:35-72). In the reference they are executed:
// POST /tools/webhook/:provider looks the config up by Provider, intersects
// AllowedActions with AuthSettings.EnabledWebhookActions, and runs JSScript
// in a node vm sandbox with the request body and the surviving actions in
// scope (tools.router.ts:250-300).
//
// Here they are data, and they will stay data. This package's dependency
// rule is stdlib plus golang.org/x/crypto, and a JavaScript engine is not
// going to be the exception: an in-process interpreter would put
// administrator-authored script in the same address space as the signing
// keys and the password hashes, with a sandbox as the only boundary. The
// decision taken on 2026-09-12 is that the core exposes an
// InboundScriptRunner seam and the product implements it out of process,
// where an IAM role is the real sandbox. That seam is U25's; nothing in
// this package reads these three fields before it.
//
// So they are carried for the reason AuthCode.CodeChallenge,
// CodeChallengeMethod and Scope are carried in store.go: this package does
// not act on them yet, and a store that persists them today needs no schema
// change when something does. A store must round-trip them unchanged and
// must never interpret them.
Provider string `json:"provider,omitempty"`
AllowedActions []string `json:"allowedActions,omitempty"`
JSScript string `json:"jsScript,omitempty"`
}
WebhookConfig is one webhook subscription as a store holds it (webhook-store.interface.ts:4-73). The JSON tags are the reference's key names, so a stored document round-trips between this port and a node-auth deployment unchanged.
IsActive, MaxRetries and RetryDelayMs are pointers for the same reason every optional field of AuthSettings is (settings_store.go): nil means the key is absent, and absent is not the zero value. IsActive defaults to true, so a nil decoded as false would silence a webhook; MaxRetries and RetryDelayMs are resolved with ?? in the sender, which keeps 0 as 0 — maxRetries: 0 is "deliver once, never retry" and must not decay into "retry three times". Active, Retries and RetryDelay are those resolutions, in one place.
Secret, TenantID, Provider and JSScript stay plain strings: the reference tests each of them for truthiness, so "" and absent are the same value there and nothing is lost by collapsing them here.
func (WebhookConfig) Active ¶ added in v0.8.0
func (c WebhookConfig) Active() bool
Active is IsActive with the reference's default applied: a configuration that never set the field is active (webhook-store.interface.ts:17).
The reference's own example store reads the rule the other way — its query is .where("isActive", true) (:104), which in SQL excludes a NULL row — but the prose is what a store implementor is told, and the admin route writes the field explicitly on every create (isActive ?? true, admin.router.ts:1404), so a nil in real data is a hand-written row rather than anything the reference produces. The prose wins.
func (WebhookConfig) Matches ¶ added in v0.8.0
func (c WebhookConfig) Matches(event, tenantID string) bool
Matches reports whether c should receive event on behalf of tenantID: the three rules WebhookStore.FindByEvent is defined by, in one place.
It is exported for the stores that cannot express all three in a query. A DynamoDB implementation that fetches a tenant partition and filters in memory should finish with this rather than re-derive the rules, so that the wildcard, the empty-events case and the global-webhook case cannot drift per backend.
func (WebhookConfig) Retries ¶ added in v0.8.0
func (c WebhookConfig) Retries() int
Retries is MaxRetries with the reference's default of 3 applied (webhook-sender.ts:18). A stored 0 is returned as 0.
func (WebhookConfig) RetryDelay ¶ added in v0.8.0
func (c WebhookConfig) RetryDelay() time.Duration
RetryDelay is RetryDelayMs with the reference's default of 1000 ms applied (webhook-sender.ts:19), as a Duration. A stored 0 is returned as 0.
type WebhookDeliverer ¶ added in v0.10.0
type WebhookDeliverer interface {
DeliverWebhook(ctx context.Context, attempt WebhookAttempt) error
}
WebhookDeliverer is the transport an outgoing webhook leaves by: the seam that replaces the reference's bare fetch (webhook-sender.ts:36).
It exists because delivering on the goroutine that served the request is a deployment decision and not a library one. The in-process default, HTTPWebhookDeliverer, is the reference's behaviour; a Lambda deployment puts the attempt on a queue with a dead-letter queue behind it and returns as soon as the enqueue is durable, so that the request goroutine never waits on a third party it does not control.
The seam is deliberately below the format and below the policy. The sender has already built the envelope, signed it, numbered the attempt and decided how many more there will be; an implementation POSTs Body to URL with Headers and reports what happened. Nothing here is a place to re-derive a signature or re-read a WebhookConfig.
The contract ¶
Return nil when the delivery succeeded — for HTTP, a 2xx answer; for a queue, a durable enqueue. Return an error for anything else. The sender treats every error alike, because the reference does: a non-2xx and a network failure take the same branch (webhook-sender.ts:37-43).
An error is retryable. The sender waits the subscription's back-off and calls again with a fresh WebhookAttempt — a new DeliveryID, Attempt one higher, Remaining one lower — until Remaining reaches zero, then gives up.
What a host owes ¶
Ordering: nothing. Two events for the same endpoint are delivered on separate goroutines and may arrive in any order, exactly as the reference's unawaited sends may; an implementation need not preserve order either, and a receiver that cares orders by the envelope's timestamp. A queue that happens to be FIFO is a bonus nobody may depend on.
Delivery: this package promises at-most-once and nothing more. It persists nothing, so a process that exits mid-flight drops whatever was in the air, and a host that needs at-least-once is the one that buys it — by making DeliverWebhook durable before it returns nil, and by delivering from that durable record afterwards.
Retry: once DeliverWebhook returns nil, this package is finished with the delivery and will never hear about it again. So the back-off implemented here covers only failures to hand the attempt over — for a queueing deliverer, failures to enqueue. The retry policy for the real request becomes the host's the moment it accepts one, and a host that wants a receiver to see the reference's behaviour reproduces it on the far side: at most WebhookConfig.Retries() further attempts, the first after WebhookConfig.RetryDelay() and each subsequent wait twice the last. Those two helpers apply the reference's defaults, so read the policy off the configuration rather than hard-coding 3 and one second. A dead-letter queue is likewise the host's, and this package will never learn that a delivery reached one.
Idempotency: do not mint a DeliveryID of your own. It is per attempt rather than per event, so it is not a deduplication key and a receiver that treats it as one is already wrong against the reference; but a host that redelivers from a queue should resend the header it was handed, so that the identifiers a receiver sees are the ones this package actually issued.
Concurrency: DeliverWebhook is called from one goroutine per delivery and must be safe for concurrent use.
Blocking: the call owns its goroutine and the sender applies no timeout around it. Honour ctx, or bound the work some other way — HTTPWebhookDeliverer does the latter with its own Timeout. On the WebhookEmitter path ctx carries the caller's values but no deadline and no cancellation, because a fire-and-forget delivery must not die with the request that caused it.
type WebhookDelivererFunc ¶ added in v0.10.0
type WebhookDelivererFunc func(ctx context.Context, attempt WebhookAttempt) error
WebhookDelivererFunc adapts a plain function to WebhookDeliverer.
func (WebhookDelivererFunc) DeliverWebhook ¶ added in v0.10.0
func (f WebhookDelivererFunc) DeliverWebhook(ctx context.Context, attempt WebhookAttempt) error
DeliverWebhook calls f.
type WebhookEmitter ¶ added in v0.10.0
type WebhookEmitter struct {
// Store is where the subscriptions live. Nil is "no outgoing webhooks", not
// an error.
Store WebhookStore
// Sender delivers. Nil means a zero WebhookSender, which delivers over HTTP
// in process.
Sender *WebhookSender
// Version is OutgoingWebhookEvent.Version, the reference's
// AuthToolsOptions.webhookVersion. Empty means OutgoingWebhookVersion.
Version string
// OnError, when set, is called once per subscription whose delivery failed
// after every attempt, and once with a zero WebhookConfig when the store
// itself failed.
//
// Nil is the default and nil is the reference: track discards the store
// error with `.catch(() => [])` (:265) and each send with `.catch(() => {})`
// (:280), and logging where the reference stays silent would be a difference
// this package would then have to register. So the default is silence and
// the hook is how a deployment buys observability back. It is called on the
// delivery goroutine and must be safe for concurrent use; it must not block,
// because nothing is waiting on it and a slow hook only accumulates
// goroutines.
OnError func(config WebhookConfig, err error)
}
WebhookEmitter fans one Event out to the subscriptions a WebhookStore holds: step 4 of the reference's AuthTools.track (auth-tools.ts:263-281), and exactly that step.
The zero value does nothing. A Store is what turns it on, which is the reference's rule too — `if (this.webhookStore)` (:263); without one, outgoing webhooks are off.
Where the line to U21 is drawn ¶
track does four things per event: persists a telemetry record, publishes on the bus, broadcasts over SSE and fires webhooks. This type is the fourth, on its own, with no opinion about the other three or about the order they run in. That order is observable — a subscriber that reads the telemetry store sees a different world depending on whether the save has happened — so it is one decision, made once, by the facade that owns all four. Implementing a second, partial ordering here and asking U21 to unpick it would be the worse inheritance.
So U21 may call Emit from wherever in track's sequence it decides webhooks belong, and will find that this type has already settled the parts that are local to webhooks: the store failure is swallowed, the envelope is built once per event rather than once per subscription, and every delivery is fire-and-forget.
func (*WebhookEmitter) Emit ¶ added in v0.10.0
func (e *WebhookEmitter) Emit(ctx context.Context, ev Event)
Emit delivers ev to every subscription the store returns for it.
The shape is the reference's, and each part of it is a decision the reference already made:
- The lookup is synchronous, on the calling goroutine. track awaits findByEvent before it sends anything (auth-tools.ts:265), so a slow store delays whatever published — in this port, the service call and therefore the HTTP response. That is reproduced rather than improved: moving the lookup onto a goroutine would reorder it against the caller's own work for no gain a deployment asked for.
- A store error is not an error. It becomes an empty result, so a failing store means "no webhooks for this event" and never a failed publish — `.catch(() => [])` (:265). OnError hears about it; nothing else does.
- The envelope is built once and shared by every delivery (:266-273). Two subscriptions receive byte-identical bodies and therefore comparable signatures, which is what a receiver fronting several endpoints expects.
- Every delivery is fire-and-forget, on its own goroutine, and Emit returns without waiting: the reference does not await send (:280). Nothing is ordered against anything, and a webhook that will retry for seven seconds does not hold the response open for seven seconds.
ctx is used for the store lookup as given. The deliveries get context.WithoutCancel(ctx): they keep the caller's values, for a host whose deliverer reads a trace from them, and lose its deadline and cancellation, because a delivery that died when the response was written would be a retry policy that never retried. Nothing is waited on and nothing is persisted, so a process that exits drops whatever is in flight — see WebhookDeliverer for which half of that a host can buy back.
func (*WebhookEmitter) Subscribe ¶ added in v0.10.0
func (e *WebhookEmitter) Subscribe(bus *EventBus)
Subscribe wires this emitter to every event a bus carries, so that publishing an event delivers it to the subscriptions a store holds.
It is one line, and the line is written out here rather than hidden because it is the whole of the bridge:
bus.Subscribe(auth.EventBusWildcard, func(ev auth.Event) {
emitter.Emit(context.Background(), ev)
})
Neither tree ships this bridge. There, AuthTools.track is the only caller of the sender and the routers' own publications reach the bus and stop (auth-tools.ts:263-281 against node-auth auth.router.ts:418-433). Here the service layer publishes onto the bus, so until U21 brings the facade across this is how a deployment gets an outgoing webhook at all — and it stays a call a host makes rather than something New() does, so that a host who wants U21's ordering later has nothing to switch off.
The context is Background: a bus handler is handed an Event and no context, and a delivery is fire-and-forget in any case. There is no way to unsubscribe, because EventBus has none; that arrives with the first consumer that needs it.
type WebhookPatch ¶ added in v0.8.0
type WebhookPatch struct {
URL *string
Events []string
Secret *string
IsActive *bool
TenantID *string
MaxRetries *int
RetryDelayMs *int
Provider *string
AllowedActions []string
JSScript *string
}
WebhookPatch is what WebhookAdminStore.UpdateWebhook applies: the reference's Partial<Omit<WebhookConfig, "id">> (webhook-store.interface.ts:141), where a field that is present replaces the stored one and a field that is absent leaves it alone. It is MailTemplatePatch's shape, for MailTemplatePatch's reasons (template_store.go).
A nil pointer leaves the stored field alone; a pointer to a zero value sets it to that zero — *IsActive = false is how the admin UI's toggle turns a webhook off, and it has to be distinguishable from "not in this patch". The two slices follow MailTemplatePatch.Translations: nil leaves the stored slice alone, and a non-nil one replaces it whole rather than appending to it, so a caller changing one event sends the list back complete. A non-nil empty slice clears.
What a patch cannot do is put an optional field back to absent: nil is already spent on "leave alone". Neither can the reference's, without sending an explicit null through a spread, and no route of its own does. A store that needs to clear MaxRetries reads the config, removes it and adds it back.
type WebhookSender ¶ added in v0.10.0
type WebhookSender struct {
// Deliverer is the transport. Nil means an HTTPWebhookDeliverer with the
// default client and DefaultWebhookTimeout — the reference's behaviour.
Deliverer WebhookDeliverer
// contains filtered or unexported fields
}
WebhookSender delivers one OutgoingWebhookEvent to one subscription, with the reference's headers, the reference's signature and the reference's retry policy: WebhookSender.send (webhook-sender.ts:17-48).
The zero value is usable and delivers over HTTP.
func (*WebhookSender) Send ¶ added in v0.10.0
func (s *WebhookSender) Send(ctx context.Context, config WebhookConfig, event OutgoingWebhookEvent) error
Send delivers event to config, retrying on failure, and reports what happened.
The wire, in full. One POST to config.URL with the serialised event as the body and
Content-Type: application/json X-Webhook-Event: <event.Event> X-Webhook-Delivery: <a fresh UUID, per attempt rather than per event> X-Webhook-Timestamp: <event.Timestamp, the envelope's own string> X-Webhook-Signature: sha256=<hex HMAC-SHA256 of the body> (only with a secret)
Three of those repay a second look, because each is a place a reasonable implementation would differ and a receiver would notice:
- X-Webhook-Delivery changes on every attempt. The reference mints it inside the retry loop (webhook-sender.ts:27), so a receiver that saw the same event twice sees two identifiers and cannot deduplicate on this header. It is reproduced rather than corrected.
- X-Webhook-Timestamp is the envelope's timestamp, not the attempt's (:28). A retry an hour later still carries the instant the event happened, which is what makes the header and the body agree.
- The signature covers the serialised body and the body only — not the headers, not the URL, not the timestamp as a separate input. The body is serialised once, before the first attempt (:20), so every attempt at an event carries identical bytes and an identical signature.
The retry policy is likewise the reference's, and it is worth stating in attempts rather than in retries because the two are off by one: `while (attempt <= maxRetries)` runs config.Retries()+1 times, so the default 3 is four requests, not three. A non-2xx and a transport failure are the same failure here; both retry while attempts remain. Between them the sender waits config.RetryDelay() doubled once per retry already made — with the defaults 1s, 2s, 4s — which is `baseDelayMs * 2^(attempt-1)` evaluated after the increment at :45-46. The last attempt is not followed by a wait: the reference returns out of the loop before the delay (:39, :42), and so does this.
Two edge cases fall out of that loop and both are the reference's. config.Retries() of 0 is one attempt and no retry, which is why WebhookConfig.MaxRetries is a *int — a stored 0 that decayed into the default would turn "deliver once" into four deliveries. A negative one sends nothing at all: `while (attempt <= -1)` never runs, no request is made, and Send reports no error because nothing failed.
What Send returns is where this is not a transcription, and the difference is off the wire. The reference's send resolves void whether it delivered or gave up: its one caller discards the promise (auth-tools.ts:280), so the failure is silent by construction. Here the exhausted case returns an error wrapping the last failure, because a Go caller has no other way to learn it and because the host implementing a queueing WebhookDeliverer needs one. The silence is reproduced where the reference is actually silent — one level up, in WebhookEmitter, which discards this error exactly as `.catch(() => {})` does and logs nothing unless a host asks it to.
Send does not consult config beyond URL, Secret, MaxRetries and RetryDelayMs. It checks neither IsActive nor Events, because the reference's sender checks neither: the store is the only filter in the chain and a store that over-returns delivers. That is stated on WebhookStore.FindByEvent, and this method is what makes it true.
type WebhookStatusError ¶ added in v0.10.0
type WebhookStatusError struct {
// StatusCode is the status the endpoint answered with.
StatusCode int
}
WebhookStatusError is what HTTPWebhookDeliverer returns when the endpoint answered but refused: a status outside 2xx.
The sender does not look at it — the reference retries a refusal and a transport failure identically (webhook-sender.ts:37-43) and so does this port — but a host's telemetry, and a host deliverer deciding whether a dead-letter is worth writing, want the two apart. The body is deliberately not carried: a receiver that echoes the request would put the payload back into an error string that is logged further than the request ever travelled.
func (*WebhookStatusError) Error ¶ added in v0.10.0
func (e *WebhookStatusError) Error() string
type WebhookStore ¶ added in v0.8.0
type WebhookStore interface {
// FindByEvent returns every configuration that should receive event on
// behalf of tenantID (webhook-store.interface.ts:112-117).
//
// Three rules, all of them the reference's, all of them in
// WebhookConfig.Matches:
//
// - Active only. A configuration whose IsActive is false is not returned.
// Nothing downstream re-checks it — the sender delivers to whatever it
// is handed — so this filter existing in the store is the whole of the
// feature.
// - Tenant scope. A non-empty tenantID returns both the configurations
// scoped to it and the global ones, which is what the interface says
// (:114-115). An empty tenantID returns the global ones only, which is
// what the interface's example query does: tenantId ?? null makes the
// first branch unsatisfiable and leaves orWhereNull (:105).
// - Event match. An Events entry equal to the event name, or to
// WebhookEventWildcard, subscribes; nothing else does. An empty Events
// matches nothing.
//
// Order is part of the contract. The normative order is the one
// MemoryWebhookStore guarantees — the order the configurations were added —
// and a store that cannot reproduce it states its own, here and in the
// deviation register if it is a published one. The order is not observable
// on the wire, because WebhookEmitter starts a goroutine per configuration
// and nothing sequences them; it is observable in ListWebhooks' pagination
// (U14) and in the order the deliveries are started in, which a host's
// queueing WebhookDeliverer may well preserve.
//
// An error means the store failed, and the caller is expected to treat it
// as an empty result rather than as a failed emit — the reference's
// .catch(() => []) (auth-tools.ts:251). Returning an error is therefore
// never a way to refuse an event; return an empty slice for that.
FindByEvent(ctx context.Context, event, tenantID string) ([]WebhookConfig, error)
}
WebhookStore holds outgoing webhook subscriptions: the reference's IWebhookStore (webhook-store.interface.ts:111-150). It is optional in the sense the reference's is — without one, outgoing webhooks are off.
Only FindByEvent is required, because only FindByEvent is on a hot path. The reference's five other methods are optional (`?`), and optional here means what it means everywhere else in this package: a narrow interface the caller type-asserts the configured store to, answering ErrFeatureNotSupported when the assertion fails, as Service.ListSessions does with SessionAdminStore. They are WebhookAdminStore and InboundWebhookStore below.
Source Files
¶
- account.go
- admin.go
- admin_credentials.go
- admin_promote.go
- admin_read.go
- admin_upload.go
- admin_write.go
- api_keys.go
- auth.go
- auth_tools.go
- claims.go
- claims_webhook.go
- compatibility.go
- compatibility_markdown.go
- config.go
- csrf.go
- delivery.go
- delivery_password_email.go
- delivery_webhook.go
- docs.go
- errors.go
- event_context.go
- event_names.go
- events.go
- feature_stores.go
- idp.go
- jwks.go
- login_2fa.go
- mailer.go
- mcp.go
- memory_api_key_store.go
- memory_store.go
- models.go
- oauth.go
- oauth_profile_map.go
- oauth_provisioning.go
- oauth_wire.go
- openapi.go
- openapi_admin.go
- openapi_tools.go
- password_verifier.go
- passwordless.go
- resource_server.go
- security.go
- service.go
- settings_store.go
- sms.go
- sse.go
- store.go
- telemetry.go
- template_store.go
- token.go
- tools.go
- tools_stream.go
- tools_track_notify.go
- totp.go
- ui.go
- ui_config.go
- ui_pages.go
- ui_upstream.go
- upload_store.go
- webhook_sender.go
- webhook_store.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. |