Documentation
¶
Overview ¶
Package auth is GoFastr's authentication subsystem.
Auth in GoFastr is composed from an AuthManager plus a set of plugins. The manager holds shared state (UserStore, SessionStore, JWT config, rate-limit config) and orchestrates a plugin lifecycle; the plugins register routes, middleware, and storage extensions for individual authentication methods (email/password, OAuth, magic-link, TOTP 2FA, account linking, email verification, password reset).
Quick start ¶
mgr := auth.New(auth.AuthConfig{
JWTSecret: "your-secret-here",
UserStore: myUserStore, // implements auth.UserStore
// SessionStore left nil → in-memory default (single-instance only).
})
mgr.Use(auth.NewCorePlugin()) // /auth/login, /auth/register, /auth/me, /auth/logout
mgr.Use(auth.NewMagicLinkPlugin(auth.MagicLinkConfig{
BaseURL: "https://app.example.com",
EmailSender: mySender, // implements auth.MagicLinkEmailSender
}))
mgr.Use(auth.NewTwoFAPlugin(auth.TwoFAConfig{}))
mgr.Use(auth.NewAccountsPlugin()) // /auth/accounts, /auth/unlink/{provider}
if err := mgr.Init(nil); err != nil {
return err
}
mgr.RegisterRoutes(myRouter)
Defaults & posture ¶
Cookies default secure-by-default: AuthConfig resolves to SessionCookie="__Host-session", SessionSecure=true. For local HTTP development, set [AuthConfig.DevMode]=true to get plain SessionCookie="session_id" and Secure=false.
Per-IP rate limiting is opt-in via AuthConfig.LoginRateLimit and the per-plugin RateLimit fields. Per-account limiting is opt-in via AuthConfig.LoginRateLimitPerAccount. Neither is on by default — set them in production.
X-Forwarded-For is NOT trusted by default. Set [RateLimiterConfig.TrustForwardedFor]=true ONLY when you sit behind a proxy that strips client-supplied XFF.
Extension points ¶
Most plugin features are gated on optional interfaces the host's UserStore or SessionStore may implement:
- OAuthLinker: bind OAuth identities to local users by (provider, providerID). Without it, OAuth callback falls back to email-only matching (insecure for non-verifying providers).
- AccountLister / AccountUnlinker: required by AccountsPlugin for the /auth/accounts and /auth/unlink endpoints.
- PasswordChecker: lets AccountsPlugin refuse "unlink the last credential" correctly (otherwise it falls back to "must have at least one OAuth account remaining").
- EmailVerifier: required by EmailVerificationPlugin.
- PasswordSetter: required by PasswordResetPlugin.
- SessionTwoFAMarker / SessionPendingMarker: required by TwoFAPlugin for default-deny 2FA enforcement.
- TwoFactorChecker: plugins implementing this signal to CorePlugin that a user has 2FA enabled and a fresh session should be minted with [Session.PendingTwoFactor]=true.
Stores that DON'T implement an optional interface fall back to a safe-but-reduced behavior (or, where fail-closed is the only option, the feature returns 500 — see each plugin's docs).
Storage ¶
MemorySessionStore, MemoryMagicLinkTokenStore, and MemoryTwoFAStore are in-memory implementations suitable for single-instance deployments and tests. There is no in-memory UserStore — bring your own (typically EntityUserStore, which adapts a database table via the framework's entity system) or write a thin map-backed one for tests. For production deployments, supply EntityUserStore + EntitySessionStore + EntityTwoFAStore (backed by SQLite or PostgreSQL). Production mode (DevMode=false) refuses to boot on the in-memory 2FA store unless AuthConfig.AllowInMemoryStores acknowledges a deliberate single-node deployment — a restart wiping 2FA enrollment is a silent security downgrade, not a scaling nuisance.
Sentinel errors ¶
Handlers branch on these typed errors rather than string matching:
- ErrUserNotFound: UserStore.FindBy* returned no row.
- ErrEmailTaken: CreateUser hit a unique constraint on email.
- ErrUnauthorized: JWT verification failed.
- ErrForbidden: authenticated but not authorized.
- ErrSessionNotFound: SessionStore.Get returned no row or expired.
Index ¶
- Constants
- Variables
- func CSRF(opts ...CSRFOption) middleware.Middleware
- func CSRFInputFromCtx(ctx context.Context) template.HTML
- func CSRFInputHTML(r *http.Request) template.HTML
- func CSRFToken(r *http.Request) string
- func CSRFTokenFromCtx(ctx context.Context) string
- func CheckPassword(password, hash string) bool
- func GenerateSecret() string
- func GenerateTOTP(secret string, timeStep uint64) string
- func HasScope(ctx context.Context, scope string) bool
- func HashPassword(password string) (string, error)
- func MCPRole(roles ...string) func(ctx context.Context) error
- func MCPUser() func(ctx context.Context) error
- func PluginAs[T AuthPlugin](m *AuthManager, name string) (T, error)
- func RequireAPIScopes(apiPrefix string) middleware.Middleware
- func RequireAuth(jwt *JWTAuth) middleware.Middleware
- func RequireRole(roles ...string) middleware.Middleware
- func RequireScope(scope string) middleware.Middleware
- func RequireSession(opts ...RequireSessionOption) middleware.Middleware
- func RolePolicy(roles []string, opts ...PolicyOption) app.Policy
- func Roles(roles ...string) []string
- func ScopeMatch(held []string, want string) bool
- func SessionEntityConfig() entity.EntityConfig
- func SessionEntityFields() []schema.Field
- func SessionMiddleware(mgr *AuthManager, opts ...SessionMiddlewareOption) middleware.Middleware
- func SessionPolicy(opts ...PolicyOption) app.Policy
- func SetDefaultLoginErrorPath(path string)
- func TokenID(ctx context.Context) (string, bool)
- func TokenMiddleware(users UserStore, accounts ServiceAccountStore, tokens APITokenStore, ...) middleware.Middleware
- func TokenScopes(ctx context.Context) ([]string, bool)
- func TwoFAEntityFields() []schema.Field
- func UserEntityConfig() entity.EntityConfig
- func ValidOAuthToken(ctx context.Context, store OAuthTokenStore, provider OAuth2Provider, ...) (string, error)
- func ValidatePasswordStrength(password string) error
- func ValidateTOTP(secret, code string, period, skew uint) bool
- func VerifyAuthEntitiesPrivate(reg entity.Registry, usersName, sessionsName string, logger *slog.Logger)
- func WithBFFPosture(mgr *AuthManager, cfg BFFPostureConfig) framework.AppOption
- func WithTokenID(ctx context.Context, id string) context.Context
- func WithTokenScopes(ctx context.Context, scopes []string) context.Context
- type APIToken
- type APITokenStore
- type Account
- type AccountLister
- type AccountUnlinker
- type AccountsPlugin
- type AuditSink
- type AuthConfig
- type AuthManager
- func (m *AuthManager) Config() AuthConfig
- func (m *AuthManager) DefaultRoles() []string
- func (m *AuthManager) Init(app *framework.App) error
- func (m *AuthManager) JWT() *JWTAuth
- func (m *AuthManager) ListUsers(ctx context.Context, opts ListUsersOptions) ([]User, int, error)
- func (m *AuthManager) Middleware() func(http.Handler) http.Handler
- func (m *AuthManager) Name() string
- func (m *AuthManager) OnStart(ctx context.Context) error
- func (m *AuthManager) OnStop(ctx context.Context) error
- func (m *AuthManager) Plugin(name string) (AuthPlugin, bool)
- func (m *AuthManager) RegisterRoutes(r *router.Router)
- func (m *AuthManager) SessionStore() SessionStore
- func (m *AuthManager) SetSessionStore(store SessionStore)
- func (m *AuthManager) SetUserRoles(ctx context.Context, userID string, roles []string) error
- func (m *AuthManager) SetUserStore(store UserStore)
- func (m *AuthManager) Use(plugin AuthPlugin) *AuthManager
- func (m *AuthManager) UserStore() UserStore
- type AuthPlugin
- type AuthPluginMiddleware
- type AuthPluginOnStart
- type AuthPluginOnStop
- type AuthPluginRoutes
- type Authenticator
- type BFFPostureConfig
- type BasicUser
- type CSRFOption
- type Claims
- type CorePlugin
- type Credentials
- type EmailSender
- type EmailVerificationConfig
- type EmailVerificationPlugin
- type EmailVerifier
- type EntitySessionStore
- func (s *EntitySessionStore) Cleanup(ctx context.Context) (int, error)
- func (s *EntitySessionStore) Create(ctx context.Context, userID string, ttl time.Duration) (*Session, error)
- func (s *EntitySessionStore) Delete(ctx context.Context, token string) error
- func (s *EntitySessionStore) DeleteByUser(ctx context.Context, userID string) (int, error)
- func (s *EntitySessionStore) EnsureSchema(ctx context.Context) error
- func (s *EntitySessionStore) Get(ctx context.Context, token string) (*Session, error)
- func (s *EntitySessionStore) MarkPendingTwoFactor(ctx context.Context, token string) error
- func (s *EntitySessionStore) MarkTwoFactorVerified(ctx context.Context, token string) error
- type EntityTwoFAStore
- func (s *EntityTwoFAStore) ConsumeBackupCode(ctx context.Context, userID string, code string) (bool, error)
- func (s *EntityTwoFAStore) DeleteTwoFA(ctx context.Context, userID string) error
- func (s *EntityTwoFAStore) EnsureSchema(ctx context.Context) error
- func (s *EntityTwoFAStore) GetTwoFA(ctx context.Context, userID string) (*TwoFAState, error)
- func (s *EntityTwoFAStore) SetTwoFA(ctx context.Context, userID string, state *TwoFAState) error
- type EntityUserStore
- func (s *EntityUserStore) CreateUser(ctx context.Context, email, hashedPassword string, roles []string) (User, error)
- func (s *EntityUserStore) CreateUserNoPassword(ctx context.Context, email string, roles []string) (User, error)
- func (s *EntityUserStore) EnsureOAuthLinksSchema(ctx context.Context) error
- func (s *EntityUserStore) EnsureSchema(ctx context.Context) error
- func (s *EntityUserStore) FindByEmail(ctx context.Context, email string) (User, string, error)
- func (s *EntityUserStore) FindByID(ctx context.Context, id string) (User, error)
- func (s *EntityUserStore) FindByOAuth(ctx context.Context, provider, providerID string) (User, error)
- func (s *EntityUserStore) HasPassword(ctx context.Context, userID string) (bool, error)
- func (s *EntityUserStore) LinkOAuth(ctx context.Context, userID, provider, providerID string) error
- func (s *EntityUserStore) LinkOAuthEnriched(ctx context.Context, userID, provider, providerID string, ...) error
- func (s *EntityUserStore) ListAccounts(ctx context.Context, userID string) ([]Account, error)
- func (s *EntityUserStore) ListUsers(ctx context.Context, opts ListUsersOptions) ([]User, int, error)
- func (s *EntityUserStore) SetPassword(ctx context.Context, userID, hashedPassword string) error
- func (s *EntityUserStore) UnlinkOAuth(ctx context.Context, userID, provider string) error
- func (s *EntityUserStore) UpdateRoles(ctx context.Context, userID string, roles []string) error
- type GitHubProvider
- func (g *GitHubProvider) AuthURL(state string) string
- func (g *GitHubProvider) ExchangeCode(ctx context.Context, code string) (*OAuth2Token, error)
- func (g *GitHubProvider) FetchUserInfo(ctx context.Context, token string) (*OAuth2UserInfo, error)
- func (g *GitHubProvider) Name() string
- func (g *GitHubProvider) RefreshToken(ctx context.Context, refreshToken string) (*OAuth2Token, error)
- type GoogleProvider
- func (g *GoogleProvider) AuthURL(state string) string
- func (g *GoogleProvider) ExchangeCode(ctx context.Context, code string) (*OAuth2Token, error)
- func (g *GoogleProvider) FetchUserInfo(ctx context.Context, token string) (*OAuth2UserInfo, error)
- func (g *GoogleProvider) Name() string
- func (g *GoogleProvider) RefreshToken(ctx context.Context, refreshToken string) (*OAuth2Token, error)
- type JWTAuth
- type ListUsersOptions
- type MagicLinkConfig
- type MagicLinkEmailSender
- type MagicLinkPlugin
- type MagicLinkTokenStore
- type MemoryMagicLinkTokenStore
- type MemorySessionStore
- func (m *MemorySessionStore) Cleanup(_ context.Context) (int, error)
- func (m *MemorySessionStore) Create(_ context.Context, userID string, ttl time.Duration) (*Session, error)
- func (m *MemorySessionStore) Delete(_ context.Context, token string) error
- func (m *MemorySessionStore) DeleteByUser(_ context.Context, userID string) (int, error)
- func (m *MemorySessionStore) Get(_ context.Context, token string) (*Session, error)
- func (m *MemorySessionStore) MarkPendingTwoFactor(_ context.Context, token string) error
- func (m *MemorySessionStore) MarkTwoFactorVerified(_ context.Context, token string) error
- type MemoryTwoFAStore
- func (m *MemoryTwoFAStore) ConsumeBackupCode(_ context.Context, userID string, code string) (bool, error)
- func (m *MemoryTwoFAStore) DeleteTwoFA(_ context.Context, userID string) error
- func (m *MemoryTwoFAStore) GetTwoFA(_ context.Context, userID string) (*TwoFAState, error)
- func (m *MemoryTwoFAStore) SetTwoFA(_ context.Context, userID string, state *TwoFAState) error
- type OAuth2Config
- type OAuth2Plugin
- type OAuth2Provider
- type OAuth2Token
- type OAuth2UserInfo
- type OAuthAccountProfile
- type OAuthEnrichedLinker
- type OAuthLinker
- type OAuthTokenRecord
- type OAuthTokenRefresher
- type OAuthTokenStore
- type OAuthUserCreator
- type OIDCClaimsMapping
- type OIDCConfig
- type OIDCProvider
- type PasswordChecker
- type PasswordResetConfig
- type PasswordResetPlugin
- type PasswordSetter
- type PolicyOption
- type RateLimitStore
- type RateLimiter
- type RateLimiterConfig
- type RedirectOpt
- type RequireSessionOption
- type SQLAPITokenStore
- func (s *SQLAPITokenStore) Create(ctx context.Context, t APIToken, sha256Hash string) error
- func (s *SQLAPITokenStore) FindByHash(ctx context.Context, sha256Hash string) (*APIToken, error)
- func (s *SQLAPITokenStore) List(ctx context.Context, ownerKind, ownerID string) ([]APIToken, error)
- func (s *SQLAPITokenStore) ListAll(ctx context.Context) ([]APIToken, error)
- func (s *SQLAPITokenStore) Revoke(ctx context.Context, id, ownerKind, ownerID string) error
- func (s *SQLAPITokenStore) RevokeAny(ctx context.Context, id string) error
- func (s *SQLAPITokenStore) TouchLastUsed(ctx context.Context, id string, at time.Time) error
- type SQLAPITokenStoreOption
- type SQLMagicLinkTokenStore
- type SQLOAuthTokenStore
- type SQLOAuthTokenStoreConfig
- type SQLRateLimitStore
- type SQLServiceAccountStore
- func (s *SQLServiceAccountStore) Create(ctx context.Context, sa ServiceAccount) error
- func (s *SQLServiceAccountStore) Get(ctx context.Context, id string) (*ServiceAccount, error)
- func (s *SQLServiceAccountStore) List(ctx context.Context) ([]ServiceAccount, error)
- func (s *SQLServiceAccountStore) SetDisabled(ctx context.Context, id string, disabled bool) error
- type SQLServiceAccountStoreOption
- type SecurityEvent
- type ServiceAccount
- type ServiceAccountStore
- type Session
- type SessionMiddlewareOption
- type SessionPendingMarker
- type SessionStore
- type SessionTwoFAMarker
- type SessionUserPurger
- type TokenMiddlewareOption
- type TokenSpec
- type TokensPlugin
- type TwoFAConfig
- type TwoFAPlugin
- func (p *TwoFAPlugin) HasTwoFactorEnabled(ctx context.Context, userID string) (bool, error)
- func (p *TwoFAPlugin) Init(mgr *AuthManager) error
- func (p *TwoFAPlugin) Name() string
- func (p *TwoFAPlugin) RegisterRoutes(r *router.Router, basePath string)
- func (p *TwoFAPlugin) RequireTwoFA() func(http.Handler) http.Handler
- type TwoFAState
- type TwoFAStore
- type TwoFactorChecker
- type User
- type UserFieldMap
- type UserFields
- type UserLister
- type UserStore
Constants ¶
const ( OwnerKindUser = "user" OwnerKindService = "service" )
OwnerKindUser and OwnerKindService are the two valid APIToken.OwnerKind values. A token belongs to either a human user (resolved via UserStore) or a non-human service account (resolved via ServiceAccountStore).
const CSRFCookieName = "auth_csrf"
CSRFCookieName is the cookie name battery/auth uses by default. Override via WithCSRF(WithCSRFCookieName(...)).
const CSRFFormField = "_csrf"
CSRFFormField is the hidden-input name CSRFInputHTML emits.
const CSRFHeaderName = "X-CSRF-Token"
CSRFHeaderName is the request header name JavaScript clients use.
const RecommendedMinPasswordBytes = 8
RecommendedMinPasswordBytes is the minimum length application registration flows should enforce on new passwords. Applications use ValidatePasswordStrength to check it; HashPassword itself only rejects the empty string because hashing logic shouldn't dictate product policy (PIN flows, recovery tokens, machine accounts, etc. may legitimately fall outside this length).
const TokenPrefix = "gfsk_"
TokenPrefix marks every plaintext API token. The "gfsk_" prefix makes leaked tokens greppable by secret scanners (GitHub, truffleHog, …) the way "ghp_", "xoxb-", and "AKIA…" are.
Variables ¶
var ( ErrForbidden = errors.New("auth: forbidden") // ErrUserNotFound is returned by UserStore.FindByEmail / FindByID when // the user does not exist. Distinct from a DB / transport error so // downstream handlers (magic-link, OAuth) can decide whether to // auto-create. Treating any error as "not found" is the bug class // this sentinel exists to avoid. ErrUserNotFound = errors.New("auth: user not found") // ErrEmailTaken is returned by UserStore.CreateUser when the email // is already registered. Distinct from generic DB errors so the // register handler can return 409 instead of 500. ErrEmailTaken = errors.New("auth: email already taken") )
Sentinel errors for auth operations.
var ErrListUsersUnsupported = errors.New("auth: user store does not support listing users")
ErrListUsersUnsupported is returned by AuthManager.ListUsers when the configured UserStore does not implement UserLister. Distinct from a transport error so callers can present "feature unavailable" vs "DB down" appropriately.
var ErrOAuthTokenNotFound = errors.New("auth: oauth token not found")
ErrOAuthTokenNotFound is returned by OAuthTokenStore.Get when no token is stored for the (userID, provider) pair.
var ErrPasswordEmpty = errors.New("auth: password is empty")
ErrPasswordEmpty is returned by HashPassword when the input is the empty string — a blank field at signup would be silently accepted otherwise, and every login attempt with no password would match.
var ErrPasswordTooShort = errors.New("auth: password too short")
ErrPasswordTooShort is returned by ValidatePasswordStrength when the input is shorter than RecommendedMinPasswordBytes.
var ErrSessionNotFound = errors.New("auth: session not found")
ErrSessionNotFound is returned by SessionStore.Get when the token is unknown or already expired.
var ErrTokenNotFound = errors.New("auth: magic-link token not found or expired")
ErrTokenNotFound is returned when a token is unknown, already consumed, or expired.
Functions ¶
func CSRF ¶
func CSRF(opts ...CSRFOption) middleware.Middleware
CSRF returns a Middleware that enforces double-submit-cookie CSRF protection on unsafe HTTP methods (POST/PUT/PATCH/DELETE).
Behaviour:
- GET/HEAD/OPTIONS — sets the token cookie if missing; lets request through.
- POST/PUT/PATCH/DELETE — verifies the request header X-CSRF-Token (JSON path) OR a hidden _csrf form field (HTML form path) matches the cookie value AND has a valid HMAC signature.
- Bearer-token / X-API-Key requests are skipped (not cookie-based).
Auth defaults: cookie name "auth_csrf", header X-CSRF-Token, form field "_csrf". Pair with CSRFInputHTML in your templates.
Mount on the app router (or on a route group) BEFORE any form-POST route that needs protection:
app.Use(auth.CSRF()) // or, with a custom secret: app.Use(auth.CSRF(auth.WithCSRFSecret(secret)))
func CSRFInputFromCtx ¶
CSRFInputFromCtx is the ctx-based counterpart to CSRFInputHTML — same hidden-input markup, derived from middleware.TokenFromContext. Use from core-ui Screen.Render where only ctx is in scope:
func (s *EditScreen) Render() render.HTML {
return render.Join(
render.Raw(string(auth.CSRFInputFromCtx(s.ctx))),
...,
)
}
Returns "" when no token is available (route not gated by CSRF).
func CSRFInputHTML ¶
CSRFInputHTML returns the hidden <input> markup callers embed inside every HTML form they submit through the framework runtime. Safe for template/html — returns template.HTML so it isn't double-escaped.
<form method="POST" action="/save">
{{ auth.CSRFInputHTML .Request }}
…
</form>
When no token cookie is present yet, CSRFInputHTML returns "" — the next safe-method request to a CSRF-protected route will set the cookie, so the typical page flow lands the token before any form renders.
Most callers should prefer CSRFInputFromCtx for code paths that only have a context.Context (e.g. core-ui screens). They share the same renderer underneath.
func CSRFToken ¶
CSRFToken returns the current request's CSRF token. Prefers the value stashed on ctx by middleware.CSRF (which works on the SAME request that minted the cookie); falls back to reading the cookie (for follow-up requests where the cookie is already in r.Cookies()). Returns "" when no token is available — typically because the route isn't behind CSRF middleware.
func CSRFTokenFromCtx ¶
CSRFTokenFromCtx returns the request's CSRF token from a context. It reads the value the CSRF middleware stashes on ctx via middleware.TokenFromContext — works on the SAME request that minted the cookie, where the cookie isn't in r.Cookies() yet. Returns "" if the route isn't behind CSRF middleware or no token is available.
Use this from any code path that holds only a context.Context (e.g. core-ui Screen.Load, Screen.Render) rather than threading a *http.Request through.
func CheckPassword ¶
CheckPassword compares a plaintext password against a bcrypt hash. Returns true if the password matches.
The same SHA-256 pre-hash applied in HashPassword is applied here for inputs longer than 72 bytes, so a long passphrase that was hashed at registration time still verifies at login time.
func GenerateSecret ¶
func GenerateSecret() string
GenerateSecret creates a cryptographically random 20-byte secret and returns it as a base32-encoded string (no padding). Panics if crypto/rand fails — entropy starvation makes the rest of the auth system unsound.
func GenerateTOTP ¶
GenerateTOTP produces a TOTP code for the given base32 secret and time-step counter using HMAC-SHA1 as specified in RFC 6238.
func HasScope ¶ added in v0.15.0
HasScope reports whether the request may exercise the given scope. It is true when the request is NOT token-authenticated (sessions/JWT carry full user capability), or when the token's scopes contain the target or a granting wildcard:
- exact match "posts:read" grants "posts:read"
- "posts:*" grants any "posts:…" verb
- "*:*" grants everything
An empty-scopes token HasScope false for everything.
func HashPassword ¶
HashPassword hashes a plaintext password using bcrypt.
The empty string is rejected with ErrPasswordEmpty. No other length policy is enforced — call ValidatePasswordStrength from the registration flow when you want to require a minimum length.
Inputs longer than 72 bytes are pre-hashed with SHA-256 before bcrypt. bcrypt silently truncates anything past 72 bytes, so without the pre-hash a 200-character passphrase would be indistinguishable from its first 72 characters. The pre-hash is base64-encoded to stay within bcrypt's usual byte-range and to avoid NUL bytes that bcrypt would terminate on.
func MCPRole ¶ added in v0.25.0
MCPRole is an mcp.Gated precondition requiring an authenticated user holding ANY of the given roles — the tool-handler analogue of RequireRole.
app.MCP.RegisterTool("cache_flush", desc, schema,
mcp.Gated(auth.MCPRole("admin"), flushHandler))
func MCPUser ¶ added in v0.25.0
MCPUser is an mcp.Gated precondition requiring an authenticated user on the tool call's context. Works whenever the app's session/JWT middleware runs globally (fwApp.Use(auth.SessionMiddleware(...))) — the /mcp route sits on the same router, so the middleware resolves the caller before the tool handler runs.
app.MCP.RegisterTool("reports_rebuild", desc, schema,
mcp.Gated(auth.MCPUser(), rebuildHandler))
func PluginAs ¶
func PluginAs[T AuthPlugin](m *AuthManager, name string) (T, error)
PluginAs retrieves a plugin and type-asserts it to T.
func RequireAPIScopes ¶ added in v0.32.0
func RequireAPIScopes(apiPrefix string) middleware.Middleware
RequireAPIScopes returns middleware that scope-gates a whole auto-CRUD API tree for token-authenticated callers, so a token minted with ["customers:*"] really is limited to customers. The required scope is derived from the route: the first path segment after apiPrefix is the resource (the entity table), and the HTTP method maps to the verb — GET/HEAD need "<resource>:read", everything else "<resource>:write". Subroutes (/{id}, /_batch, /_events, /llm.md) inherit the resource.
Session/JWT callers (no token on the request) pass through unscoped, as do paths outside apiPrefix — this only *narrows* what a token may do, mirroring HasScope semantics. Mount once, after TokenMiddleware:
app.Use(auth.TokenMiddleware(users, accounts, tokens))
app.Use(auth.RequireAPIScopes("/api"))
Per-route RequireScope remains the tool for custom endpoints and non-CRUD scopes.
func RequireAuth ¶
func RequireAuth(jwt *JWTAuth) middleware.Middleware
RequireAuth returns middleware that validates a Bearer JWT token and stores the authenticated user in the request context.
func RequireRole ¶
func RequireRole(roles ...string) middleware.Middleware
RequireRole returns middleware that checks if the authenticated user has at least one of the required roles.
func RequireScope ¶ added in v0.15.0
func RequireScope(scope string) middleware.Middleware
RequireScope returns middleware that 403s token-authenticated requests lacking the scope. Non-token requests (sessions/JWT) pass through unscoped — sessions carry full user capability; scopes are an additional token-level restriction only.
func RequireSession ¶
func RequireSession(opts ...RequireSessionOption) middleware.Middleware
RequireSession returns middleware that rejects requests without a valid session-cookie-loaded user. Pair with SessionMiddleware upstream.
By default it returns JSON 401. Use WithRedirectOnFail to redirect browser (text/html-accepting) requests to a login page instead.
func RolePolicy ¶
func RolePolicy(roles []string, opts ...PolicyOption) app.Policy
RolePolicy returns an app.Policy that allows requests where the loaded user has at least one of the given roles. Default failure outcome is Block(403). Implies SessionPolicy — an anonymous request fails the role check and produces the configured failure outcome.
Pair with the Roles helper for ergonomic literal lists:
auth.RolePolicy(auth.Roles("admin"), auth.WithRedirect("/login"))
Note that the sibling HTTP middleware auth.RequireRole takes its roles as variadic strings; RolePolicy can't because it accepts PolicyOptions after the role list.
func Roles ¶
Roles is a tiny ergonomic helper so RolePolicy callers can write
auth.RolePolicy(auth.Roles("admin", "owner"), auth.WithRedirect("/login"))
instead of the literal []string{...}. The asymmetry with the sibling middleware auth.RequireRole(roles ...string) is forced by Go's no-double-variadic rule — RolePolicy needs to accept option values after the role list, so the roles can't themselves be variadic.
func ScopeMatch ¶ added in v0.21.0
ScopeMatch reports whether any scope in held grants want, using the resource:verb wildcard grammar owned by access.ScopeMatch (exact match, or a "*" on either half). Exported so other capability gates (e.g. framework/pluginhost) check grant sets against the SAME matcher rather than reimplementing a weaker one. This thin adapter exists for the []string-typed token-scope call site; new callers holding []access.Permission should call access.ScopeMatch directly.
func SessionEntityConfig ¶
func SessionEntityConfig() entity.EntityConfig
SessionEntityConfig returns an EntityConfig pre-configured for the auth sessions table: standard fields + CRUD=false + MCP=false. Auto-private by default. See UserEntityConfig for the rationale.
app.Entity("sessions", auth.SessionEntityConfig())
func SessionEntityFields ¶
SessionEntityFields returns the standard field definitions for a session entity.
func SessionMiddleware ¶
func SessionMiddleware(mgr *AuthManager, opts ...SessionMiddlewareOption) middleware.Middleware
SessionMiddleware returns middleware that loads the user identified by the session cookie (set by /auth/login or /auth/register) into the request context.
Behaviour:
- No cookie → request proceeds anonymously (user not set in ctx).
- Invalid/expired/pending-2FA session → request proceeds anonymously.
- Valid session → user is loaded from UserStore.FindByID and stashed in ctx via handler.SetUser. GetCurrentUser(ctx) then returns the User; CRUD owner-scoping picks it up automatically.
This is the missing counterpart to RequireAuth, which is JWT-only. Host apps with HTML/form auth want SessionMiddleware mounted on the routes that need to know "who is logged in" — typically the whole app router.
app.Use(auth.SessionMiddleware(mgr))
To gate routes behind a session, follow with RequireSession.
Pass WithSessionLogger to receive structured logs about store errors (which otherwise look identical to "user logged out" in production).
func SessionPolicy ¶
func SessionPolicy(opts ...PolicyOption) app.Policy
SessionPolicy returns an app.Policy that allows requests with a valid session and otherwise applies the configured failure outcome. Default failure outcome is Redirect("/login?next=<request-path>").
Attach via NewScreenGroup, ScreenGroup.WithPolicy, or Screen.WithPolicy. Pair with SessionMiddleware upstream so the policy can see the loaded user.
func SetDefaultLoginErrorPath ¶
func SetDefaultLoginErrorPath(path string)
SetDefaultLoginErrorPath configures the path that form-encoded auth errors redirect to when the request has no usable Referer (e.g. browser with Referrer-Policy: no-referrer, or a CSRF-stripping proxy). Without this, those flows fall through to a JSON 4xx body the user sees as a raw blob in their browser. Set to your login page (e.g. "/login") so users get bounced back to retry.
Pass "" to disable and restore the JSON-fallback behaviour. Safe to call concurrently and from package init.
func TokenID ¶ added in v0.28.0
TokenID returns (id, true) only for token-authenticated requests; ("", false) for sessions/JWT.
func TokenMiddleware ¶ added in v0.15.0
func TokenMiddleware(users UserStore, accounts ServiceAccountStore, tokens APITokenStore, opts ...TokenMiddlewareOption) middleware.Middleware
TokenMiddleware authenticates `Authorization: Bearer gfsk_…` requests.
Non-gfsk_ bearer tokens and requests without the header pass through UNTOUCHED (the session/JWT middleware handle those) — it does NOT clear an existing ctx user in that case. A gfsk_-prefixed credential that fails validation proceeds ANONYMOUSLY with the ctx user CLEARED (mirroring SessionMiddleware's anon semantics), never falling back to a prior identity.
Validation order:
- Extract bearer; if it doesn't start with gfsk_, pass through.
- hash := sha256(credential); FindByHash (uniform timing — no string comparison against stored plaintext anywhere).
- Unknown hash / RevokedAt set / ExpiresAt passed → anonymous (cleared) + audit token.auth_failed (reason ∈ unknown|revoked|expired; token PREFIX only, never the credential).
- Resolve owner (user → UserStore, service → ServiceAccountStore); missing owner or disabled service account → anonymous + audit (reason ∈ owner_missing|owner_disabled).
- Success: handler.SetUser(ctx, principal), WithTokenScopes(ctx, scopes), throttled best-effort TouchLastUsed.
func TokenScopes ¶ added in v0.15.0
TokenScopes returns (scopes, true) only for token-authenticated requests; (nil, false) for sessions/JWT (which carry full user capability and are unrestricted by the scope model).
func TwoFAEntityFields ¶ added in v0.21.0
TwoFAEntityFields returns the standard field definitions for a 2FA state entity, for hosts that want the table visible to the entity system (admin screens, migrations). The secret and backup-code hashes are Hidden so they can never leak through an API response.
func UserEntityConfig ¶
func UserEntityConfig() entity.EntityConfig
UserEntityConfig returns an EntityConfig pre-configured for the auth users table: standard fields + CRUD=false + MCP=false. Auto-private by default so host apps don't accidentally expose user rows through auto-generated REST or MCP tools.
Hosts that DO want to expose a /users endpoint can override after the fact (e.g. `cfg := auth.UserEntityConfig(); enabled := true; cfg.CRUD = &enabled`).
app.Entity("users", auth.UserEntityConfig())
func ValidOAuthToken ¶ added in v0.3.3
func ValidOAuthToken(ctx context.Context, store OAuthTokenStore, provider OAuth2Provider, userID string) (string, error)
ValidOAuthToken returns a currently-valid access token for the user, refreshing transparently when the stored token is expired or within refreshSkew of expiry. It is the recommended entry point for code making calls on the user's behalf.
SECURITY: as with RefreshOAuthToken, userID MUST be the authenticated principal's id — never request-supplied — or it is an IDOR.
func ValidatePasswordStrength ¶
ValidatePasswordStrength returns ErrPasswordEmpty for an empty input and ErrPasswordTooShort for anything shorter than RecommendedMinPasswordBytes. Use it from registration / password- change handlers to enforce a length floor without baking policy into the hash function.
func ValidateTOTP ¶
ValidateTOTP checks whether the provided code is valid for the given secret within ±skew time periods of the current time.
func VerifyAuthEntitiesPrivate ¶
func VerifyAuthEntitiesPrivate(reg entity.Registry, usersName, sessionsName string, logger *slog.Logger)
VerifyAuthEntitiesPrivate checks the named auth entities in `reg` and emits a WARN log if any are registered with CRUD enabled. The auth tables (users, sessions) hold password hashes and session tokens — exposing them via auto-CRUD is a footgun. The auto-private helpers UserEntityConfig() / SessionEntityConfig() set CRUD=false / MCP=false; hosts that still wire the bare UserEntityFields() / SessionEntityFields() fall back to the dangerous default.
Call once at startup after registering entities:
app.Entity("users", auth.UserEntityConfig())
app.Entity("sessions", auth.SessionEntityConfig())
auth.VerifyAuthEntitiesPrivate(app.Registry, "users", "sessions", nil)
usersName / sessionsName are the entity names — pass "" to skip either. logger=nil uses slog.Default. Unknown entity names are silently skipped (the entity may not be registered yet, or a different name was used).
func WithBFFPosture ¶ added in v0.41.0
func WithBFFPosture(mgr *AuthManager, cfg BFFPostureConfig) framework.AppOption
WithBFFPosture configures cookie-only browser authentication in one option: JSON login responses omit JWTs, session identity is hydrated globally, CSRF protects cookie-authenticated mutations, and requests carrying an Origin to the API prefix must match the exact allowlist. API tokens with the gfsk_ prefix remain available for non-browser automation; bearer JWTs are rejected on the BFF API surface. When no WithAPIPrefix is configured, entity CRUD mounts at the bare root, so the guard covers the entire app — list the app's own origin in AllowedOrigins in that case, or set an API prefix.
This option lives in battery/auth (rather than framework) because it needs an AuthManager and importing the auth battery from framework would create a Go import cycle.
func WithTokenID ¶ added in v0.28.0
WithTokenID stashes the authenticating token's ID in ctx. TokenMiddleware calls this on success so hosts can attribute a request to the SPECIFIC token — per-token metering, quotas, and audit trails need the token identity, not just the owner (one owner can hold many tokens).
Types ¶
type APIToken ¶ added in v0.15.0
type APIToken struct {
ID string
Name string // human label, required
OwnerKind string // "user" | "service"
OwnerID string // user id or service-account id
Prefix string // display prefix, first 12 chars of plaintext
Scopes []string // e.g. "posts:read"; empty = NO scopes (RequireScope always 403s)
ExpiresAt *time.Time // nil = no expiry
LastUsedAt *time.Time
RevokedAt *time.Time
CreatedAt time.Time
}
APIToken is one scoped, hashed API token (a PAT or a service-account credential). Only a sha256 hash of the plaintext is persisted; the Prefix field is the first tokenPrefixLen chars of the plaintext so listings can identify a token without revealing it.
func IssueToken ¶ added in v0.15.0
IssueToken mints the plaintext, persists the hashed record, and returns (plaintext, record, error). The plaintext is returned exactly ONCE and is never persisted, logged, or placed in any error string.
Validation: Name/OwnerKind/OwnerID required; OwnerKind ∈ {user,service}; scope strings must match ^[a-z0-9_-]+:[a-z0-9_*-]+$; max 32 scopes.
type APITokenStore ¶ added in v0.15.0
type APITokenStore interface {
Create(ctx context.Context, t APIToken, sha256Hash string) error
// FindByHash returns (nil, nil) for unknown hashes.
FindByHash(ctx context.Context, sha256Hash string) (*APIToken, error)
List(ctx context.Context, ownerKind, ownerID string) ([]APIToken, error)
// Revoke stamps RevokedAt; idempotent; scoped by owner so one owner
// cannot revoke another's token. Returns ErrTokenNotFound when no row
// matches (id, ownerKind, ownerID).
Revoke(ctx context.Context, id, ownerKind, ownerID string) error
TouchLastUsed(ctx context.Context, id string, at time.Time) error
}
APITokenStore persists API tokens. Implementations store ONLY the sha256 hash of the plaintext — never the plaintext itself.
type Account ¶
type Account struct {
Provider string `json:"provider"`
ProviderID string `json:"providerId"`
Email string `json:"email,omitempty"`
Name string `json:"name,omitempty"`
AvatarURL string `json:"avatarUrl,omitempty"`
LinkedAt *time.Time `json:"linkedAt,omitempty"`
}
Account represents one linked OAuth provider for a user. The optional profile fields are populated when the store implements OAuthEnrichedLinker — useful for rendering "you're connected to Google as alice@example.com (Alice, 📸)" in a settings UI. Stores that only implement OAuthLinker continue to return the minimum (Provider + ProviderID), the rest stay empty.
type AccountLister ¶
type AccountLister interface {
ListAccounts(ctx context.Context, userID string) ([]Account, error)
}
AccountLister is the optional UserStore extension that returns the linked OAuth accounts for a user. Required by AccountsPlugin.
type AccountUnlinker ¶
AccountUnlinker is the optional UserStore extension that removes a link. Required by AccountsPlugin's unlink endpoint.
type AccountsPlugin ¶
type AccountsPlugin struct {
// contains filtered or unexported fields
}
AccountsPlugin provides:
- GET /auth/accounts → list linked OAuth providers.
- DELETE /auth/unlink/{provider} → remove a link, refusing if it would leave the user without any way to log in (last credential).
func NewAccountsPlugin ¶
func NewAccountsPlugin() *AccountsPlugin
NewAccountsPlugin constructs the plugin.
func (*AccountsPlugin) Init ¶
func (p *AccountsPlugin) Init(mgr *AuthManager) error
func (*AccountsPlugin) Name ¶
func (p *AccountsPlugin) Name() string
func (*AccountsPlugin) RegisterRoutes ¶
func (p *AccountsPlugin) RegisterRoutes(r *router.Router, basePath string)
type AuditSink ¶ added in v0.14.0
type AuditSink interface {
SecurityEvent(ctx context.Context, ev SecurityEvent)
}
AuditSink receives security events. Implementations must be fast or buffer internally; they are called on the request path. A nil sink disables auditing entirely — AuthManager.emitSecurity no-ops.
The built-in NewSQLAuditSink writes one audit_log row per event via framework.AppendAuditEvent, sharing the CRUD audit table and its sanitisation so the two trails never drift apart.
func NewSQLAuditSink ¶ added in v0.14.0
NewSQLAuditSink builds an AuditSink that writes security events into the given audit table (empty → "audit_log", the framework default). It calls framework.EnsureAuditTable once at construction so the table exists before the first event; a failure here is returned rather than deferred to the first request, where a missing table would silently drop events.
One-line wiring:
sink, err := auth.NewSQLAuditSink(db, "")
mgr := auth.New(auth.AuthConfig{ AuditSink: sink, … })
type AuthConfig ¶
type AuthConfig struct {
// JWTSecret is the signing key for JWT tokens. Required for JWT-based auth.
// In production mode (DevMode=false) it is mandatory: Init fails closed
// with an error when it is empty, because an empty HMAC key yields
// forgeable JWTs. In DevMode a random per-process secret is minted.
JWTSecret string
// JWTExpiry is the duration for which JWT tokens are valid.
// Defaults to 1 hour if zero.
JWTExpiry time.Duration
// CookieOnly suppresses JWTs in login responses. Session cookies remain
// HttpOnly, Secure in production, and SameSite=Strict. Prefer enabling this
// through WithBFFPosture so session hydration, CSRF, and Origin enforcement
// are installed together instead of creating a partial posture.
CookieOnly bool
// SessionCookie is the cookie name for session-based auth.
// Defaults to "session_id".
SessionCookie string
// SessionTTL is how long a session lives. Defaults to 7 days.
SessionTTL time.Duration
// SessionSecure sets the Secure flag on session cookies.
// Should be true in production (HTTPS).
SessionSecure bool
// BasePath is the URL prefix for all auth routes. Defaults to "/auth".
BasePath string
// UserStore looks up and creates users. Unlike SessionStore, there
// is NO default: if nil, the register/login/magic-link/OAuth
// handlers fail with "user store not configured". Wire an
// auth.EntityUserStore (or your own implementation) in every
// deployment, including dev/test.
UserStore UserStore
// SessionStore is the storage backend for sessions.
// If nil, an in-memory store is used (dev/test only).
SessionStore SessionStore
// LoginRateLimit, when non-nil, applies a per-IP rate limit to
// /auth/login. nil means unlimited (which you almost never want
// in production — set defaults if unsure).
LoginRateLimit *RateLimiterConfig
// LoginRateLimitPerAccount, when non-nil, applies a per-account
// (keyed on the submitted email, lowercased) rate limit to
// /auth/login. Combined with the per-IP limiter it defeats the
// attacker who pivots through a botnet's worth of IPs to pound a
// single account. nil disables the per-account limit.
LoginRateLimitPerAccount *RateLimiterConfig
// RegisterRateLimit applies a per-IP rate limit to /auth/register.
// Defaults to 10/min with a 15-minute block when nil — unthrottled
// registration is account-table flooding and, once an EmailSender
// is wired, an email-bombing primitive. To disable, pass a config
// with a huge MaxAttempts.
RegisterRateLimit *RateLimiterConfig
// DevMode loosens the cookie defaults for local HTTP development.
// When true: SessionSecure=false, SessionCookie="session_id".
// When false (production default): SessionSecure=true, cookie name
// "__Host-session" — the prefix forces browsers to reject the cookie
// unless Path=/, Secure, no Domain, blocking subdomain cookie
// injection.
DevMode bool
// AllowInMemoryStores acknowledges a deliberate single-node
// deployment. Without it, production mode (DevMode=false) logs a
// WARN at Init when sessions live in the default in-memory store
// (they don't survive restarts and never resolve on a second
// replica), and REFUSES to boot when 2FA state does — a restart
// silently reverting enrolled accounts to password-only auth is a
// security downgrade, not a scaling nuisance. With the flag set,
// the session warning is silenced and the 2FA refusal downgrades
// to a WARN. See the horizontal-scaling doc for the DB-backed
// alternatives (EntitySessionStore, EntityTwoFAStore).
AllowInMemoryStores bool
// DefaultRoles are the server-assigned roles stamped onto every
// newly created account — register, magic-link auto-create, and
// OAuth auto-create all draw from it. When empty (the default),
// ["user"] is used.
//
// These are operator configuration, NEVER request data: the
// registration and auto-create flows are anonymous, so honoring a
// client-supplied roles key would let anyone self-promote. The
// handlers read this value through AuthManager.DefaultRoles() and
// ignore any roles field on the incoming request.
DefaultRoles []string
// AuditSink, when non-nil, receives security events: login success
// and failure, 2FA enrolment/challenge/disable, password reset,
// OAuth link and login, magic-link request and consume. The events
// land in the same audit_log table as the CRUD hooks (via
// NewSQLAuditSink) so an operator has one trail for "who did what".
// nil disables auditing entirely — emit calls are no-ops. Wire it
// with auth.NewSQLAuditSink(db, "") for the one-line default.
AuditSink AuditSink
}
AuthConfig is the top-level configuration for the auth battery. It is passed to New() and stored on AuthManager.
type AuthManager ¶
type AuthManager struct {
// contains filtered or unexported fields
}
AuthManager is the core orchestrator for the auth system. It manages the plugin lifecycle, provides shared state (stores, config), and implements the framework.Battery interface so it can be registered directly on an App.
Usage:
mgr := auth.New(auth.Config{
JWTSecret: "secret",
})
mgr.Use(oauth2plugin.New(oauth2plugin.Config{...}))
mgr.Use(twofaplugin.New())
app.RegisterBattery(mgr)
func New ¶
func New(config AuthConfig) *AuthManager
New creates a new AuthManager with the given configuration. The core auth plugin (email/password + sessions + JWT) is always loaded first.
func (*AuthManager) Config ¶
func (m *AuthManager) Config() AuthConfig
Config returns the auth configuration (read-only copy).
func (*AuthManager) DefaultRoles ¶ added in v0.18.0
func (m *AuthManager) DefaultRoles() []string
DefaultRoles returns the roles assigned to newly created accounts, from AuthConfig.DefaultRoles or ["user"] when that field is empty. It returns a fresh copy on every call so each caller owns its slice and cannot mutate the shared configuration through it.
This is the single source of truth the register handler and the OAuth/magic-link auto-create flows read; it is server-side configuration and is never influenced by request input.
func (*AuthManager) Init ¶
func (m *AuthManager) Init(app *framework.App) error
Init is called by the framework during App.Start. It initializes JWT (if configured), all registered auth plugins, and mounts their HTTP routes on app.Router() under the configured BasePath.
Init fails closed when DevMode=false and JWTSecret is empty: the app refuses to start rather than run with a forgeable signing key.
app may be nil for unit tests that exercise auth in isolation; in that case route mounting is skipped (the test wires routes directly onto a router it owns).
func (*AuthManager) JWT ¶
func (m *AuthManager) JWT() *JWTAuth
JWT returns the JWT auth helper (nil if JWT is not configured).
func (*AuthManager) ListUsers ¶ added in v0.18.0
func (m *AuthManager) ListUsers(ctx context.Context, opts ListUsersOptions) ([]User, int, error)
ListUsers enumerates accounts through the configured UserStore when it implements UserLister. It is the supported replacement for raw SQL against the auth_users table. There is no HTTP route — call it from trusted server code (an admin handler, a back-office screen).
opts is clamped: Limit<=0 becomes 50, values above 500 are capped at 500 (an unbounded list is a DoS vector on a large user table), and a negative Offset is treated as 0.
Returns ErrListUsersUnsupported when the store lacks the UserLister interface, so a deployment that forgot to wire a listable store is told explicitly rather than handed an empty page.
func (*AuthManager) Middleware ¶
func (m *AuthManager) Middleware() func(http.Handler) http.Handler
Middleware returns a composed middleware chain from all plugins that implement AuthPluginMiddleware. Middleware is applied in registration order.
func (*AuthManager) Name ¶
func (m *AuthManager) Name() string
Name returns the battery name for the framework's BatteryManager.
func (*AuthManager) OnStart ¶
func (m *AuthManager) OnStart(ctx context.Context) error
OnStart starts plugins that implement AuthPluginOnStart.
func (*AuthManager) OnStop ¶
func (m *AuthManager) OnStop(ctx context.Context) error
OnStop stops plugins that implement AuthPluginOnStop.
func (*AuthManager) Plugin ¶
func (m *AuthManager) Plugin(name string) (AuthPlugin, bool)
Plugin retrieves a registered plugin by name. Useful for cross-plugin communication (e.g., OAuth2 plugin checking if 2FA plugin is active).
func (*AuthManager) RegisterRoutes ¶
func (m *AuthManager) RegisterRoutes(r *router.Router)
RegisterRoutes mounts all sub-plugin routes under the configured auth base path. Called from AuthManager.Init when an app is supplied; also exported so users can mount auth routes onto a router they manage themselves.
func (*AuthManager) SessionStore ¶
func (m *AuthManager) SessionStore() SessionStore
SessionStore returns the session storage backend.
func (*AuthManager) SetSessionStore ¶
func (m *AuthManager) SetSessionStore(store SessionStore)
SetSessionStore allows plugins or external code to set the session store.
func (*AuthManager) SetUserRoles ¶ added in v0.20.0
SetUserRoles replaces the roles for an existing user via the configured UserStore. It is the supported server-side entry point for operator-driven role assignment (e.g. the admin back-office). There is no HTTP route — call it from trusted server code. The roles are OPERATOR input, never request data: the caller is responsible for sourcing them from an admin-gated screen, not from a client-supplied body.
func (*AuthManager) SetUserStore ¶
func (m *AuthManager) SetUserStore(store UserStore)
SetUserStore allows plugins or external code to set the user store after construction but before Init.
func (*AuthManager) Use ¶
func (m *AuthManager) Use(plugin AuthPlugin) *AuthManager
Use registers an auth plugin. Returns the manager for chaining. Plugins are initialized in registration order during Init().
func (*AuthManager) UserStore ¶
func (m *AuthManager) UserStore() UserStore
UserStore returns the user storage backend.
type AuthPlugin ¶
type AuthPlugin interface {
// Name returns the unique plugin identifier.
Name() string
// Init initializes the plugin with access to the AuthManager.
// The manager is fully configured (stores, config) but not yet started.
Init(mgr *AuthManager) error
}
AuthPlugin is the interface for composable auth features. Each plugin adds authentication capabilities (email/password, OAuth2, 2FA, etc.) to the AuthManager.
Plugins are registered via AuthManager.Use() and initialized in order during AuthManager.Init().
type AuthPluginMiddleware ¶
type AuthPluginMiddleware interface {
AuthPlugin
Middleware() func(http.Handler) http.Handler
}
AuthPluginMiddleware is the optional interface for plugins that provide middleware (e.g., session hydration, 2FA challenge).
type AuthPluginOnStart ¶
type AuthPluginOnStart interface {
AuthPlugin
OnStart(ctx context.Context) error
}
AuthPluginOnStart is the optional interface for plugins that need startup logic (e.g., OAuth2 provider validation, cleanup goroutines).
type AuthPluginOnStop ¶
type AuthPluginOnStop interface {
AuthPlugin
OnStop(ctx context.Context) error
}
AuthPluginOnStop is the optional interface for plugins that need shutdown logic (e.g., cleanup goroutines).
type AuthPluginRoutes ¶
type AuthPluginRoutes interface {
AuthPlugin
RegisterRoutes(r *router.Router, basePath string)
}
AuthPluginRoutes is the optional interface for plugins that register HTTP routes under the auth base path.
type Authenticator ¶
type Authenticator interface {
Authenticate(ctx context.Context, credentials Credentials) (User, error)
}
Authenticator verifies credentials and returns the authenticated user.
type BFFPostureConfig ¶ added in v0.41.0
type BFFPostureConfig struct {
AllowedOrigins []string
CSRFOptions []CSRFOption
}
BFFPostureConfig configures the explicit browser-backend security preset. AllowedOrigins must contain exact http(s) origins such as "https://app.example.com"; wildcards and opaque origins are rejected.
type CSRFOption ¶
type CSRFOption func(*middleware.CSRFConfig)
CSRFOption configures the auth CSRF middleware.
func WithCSRFCookieName ¶
func WithCSRFCookieName(name string) CSRFOption
WithCSRFCookieName overrides the cookie name. Defaults to "auth_csrf".
func WithCSRFCookieSecure ¶
func WithCSRFCookieSecure(secure bool) CSRFOption
WithCSRFCookieSecure marks the CSRF cookie Secure. Pair with HTTPS in production; leave false for local HTTP dev.
func WithCSRFSecret ¶
func WithCSRFSecret(key []byte) CSRFOption
WithCSRFSecret sets the HMAC key used to sign tokens. Recommended in production; without it a per-process random key is generated and rotates on every restart (invalidating all in-flight forms).
func WithCSRFSkipPaths ¶
func WithCSRFSkipPaths(prefixes ...string) CSRFOption
WithCSRFSkipPaths exempts the given path prefixes from CSRF enforcement alongside the default SkipBearerAuth predicate. Use for webhook endpoints authenticated by signature, health checks, and similar non-cookie surfaces. Without this option, hosts have to write a Skip closure that inspects r.URL.Path manually (V3 #9 friction):
app.Use(auth.CSRF(auth.WithCSRFSkipPaths("/webhooks/", "/health")))
Path matching is literal string-prefix — see middleware.CSRFSkipper.
func WithDevCSRFKey ¶
func WithDevCSRFKey(path string) CSRFOption
WithDevCSRFKey loads (or creates+writes) a stable HMAC key from path and uses it as SecretKey. Intended for dev only — survives process restarts so browsers don't 403 on the next form submit after every dev-server reload (V3 #5). In production, use WithCSRFSecret with a value sourced from your secret manager.
Returns an option that PANICS on key-load failure: in a dev startup path, failing loudly is the right behavior (silently falling back to the auto-key reintroduces exactly the UX problem this option exists to solve).
type Claims ¶
type Claims struct {
UserID string
Email string
Roles []string
ExpiresAt time.Time
IssuedAt time.Time
}
Claims represents the data embedded in a JWT token.
type CorePlugin ¶
type CorePlugin struct {
// contains filtered or unexported fields
}
CorePlugin is the always-loaded auth plugin providing email/password authentication, session management, and JWT token support. It wraps the original battery/auth functionality into the plugin architecture.
All existing auth handlers (login, logout, me) are here, reworked to use AuthManager's shared stores instead of receiving them as parameters.
func (*CorePlugin) Init ¶
func (c *CorePlugin) Init(mgr *AuthManager) error
Init stores a reference to the AuthManager and constructs the optional login rate limiters from manager config.
func (*CorePlugin) RegisterRoutes ¶
func (c *CorePlugin) RegisterRoutes(r *router.Router, basePath string)
RegisterRoutes mounts the core auth routes: login, logout, me, register.
type Credentials ¶
type Credentials struct {
Email string // email address
Password string // password (for email/password auth)
Provider string // OAuth provider name (e.g. "google", "github")
Token string // API key or bearer token
}
Credentials holds the information needed to authenticate a user.
type EmailSender ¶
EmailSender is the lightweight interface this package uses for any outbound transactional email (verification, password reset). Concrete implementations wrap SMTP, SES, Postmark, etc. The body is intentionally "anything you want" — these plugins build the URL and let the caller decide on templating.
type EmailVerificationConfig ¶
type EmailVerificationConfig struct {
// BaseURL is the application URL used to construct the verification link.
BaseURL string
// TokenTTL is the verification link's lifetime. Default 24h.
TokenTTL time.Duration
// EmailSender sends the verification message. If nil, DevMode must
// be set or send-verification fails closed (503).
EmailSender EmailSender
// BodyTemplate, when non-nil, transforms the verification URL into
// the full email body before EmailSender.Send is called. nil means
// "send the URL as the entire body" (the historical behavior).
BodyTemplate func(url string) string
// TokenStore persists pending verification tokens. Defaults to in-memory
// (does not survive restart / scale) — set a durable store
// (e.g. NewSQLMagicLinkTokenStore(db)) in production.
TokenStore MagicLinkTokenStore
// DevMode logs the verification URL when EmailSender is nil. NEVER
// enable in production — anyone with log read access then takes
// over arbitrary accounts.
DevMode bool
// RateLimit, when non-nil, applies a per-IP limit to send-verification.
RateLimit *RateLimiterConfig
}
EmailVerificationConfig configures the plugin.
type EmailVerificationPlugin ¶
type EmailVerificationPlugin struct {
// contains filtered or unexported fields
}
EmailVerificationPlugin wires:
- POST /auth/send-verification (authenticated; sends a token to the current user's email).
- GET /auth/verify-email?token=... (consumes a token, marks the user verified).
func NewEmailVerificationPlugin ¶
func NewEmailVerificationPlugin(cfg EmailVerificationConfig) *EmailVerificationPlugin
NewEmailVerificationPlugin builds the plugin with sensible defaults.
func (*EmailVerificationPlugin) Init ¶
func (p *EmailVerificationPlugin) Init(mgr *AuthManager) error
func (*EmailVerificationPlugin) Name ¶
func (p *EmailVerificationPlugin) Name() string
func (*EmailVerificationPlugin) RegisterRoutes ¶
func (p *EmailVerificationPlugin) RegisterRoutes(r *router.Router, basePath string)
type EmailVerifier ¶
EmailVerifier is the optional UserStore extension used by the EmailVerificationPlugin to mark a user's email as verified.
type EntitySessionStore ¶
type EntitySessionStore struct {
// contains filtered or unexported fields
}
EntitySessionStore adapts a database table to the SessionStore interface. The backing table must have: token (string, PK), user_id (string), created_at (timestamp), expires_at (timestamp).
Usage:
app.Entity("sessions", entity.EntityConfig{
Fields: auth.SessionEntityFields(),
})
mgr.SetSessionStore(auth.NewEntitySessionStore(db, "sessions"))
func NewEntitySessionStore ¶
func NewEntitySessionStore(db *sql.DB, table string) *EntitySessionStore
NewEntitySessionStore creates a SessionStore backed by a database table. Panics if the table name contains unsafe characters.
func (*EntitySessionStore) Cleanup ¶
func (s *EntitySessionStore) Cleanup(ctx context.Context) (int, error)
Cleanup removes all expired sessions and returns the count purged.
func (*EntitySessionStore) Create ¶
func (s *EntitySessionStore) Create(ctx context.Context, userID string, ttl time.Duration) (*Session, error)
Create generates a random token, inserts a session row, and returns it. Rejects ttl <= 0 — the caller must supply a positive lifetime.
func (*EntitySessionStore) Delete ¶
func (s *EntitySessionStore) Delete(ctx context.Context, token string) error
Delete removes a session by token. Idempotent.
func (*EntitySessionStore) DeleteByUser ¶
DeleteByUser removes every session belonging to userID and returns the count purged. Implements SessionUserPurger.
func (*EntitySessionStore) EnsureSchema ¶ added in v0.7.0
func (s *EntitySessionStore) EnsureSchema(ctx context.Context) error
EnsureSchema creates the session table if it does not already exist. Called by AuthManager.Init so hosts never hand-roll the DDL. Idempotent. The datetime and boolean column types are chosen per dialect (SQLite vs PostgreSQL) so the same battery boots on either.
func (*EntitySessionStore) MarkPendingTwoFactor ¶
func (s *EntitySessionStore) MarkPendingTwoFactor(ctx context.Context, token string) error
MarkPendingTwoFactor flips pending_two_factor=TRUE for the given session. Implements SessionPendingMarker.
func (*EntitySessionStore) MarkTwoFactorVerified ¶
func (s *EntitySessionStore) MarkTwoFactorVerified(ctx context.Context, token string) error
MarkTwoFactorVerified sets two_factor_verified=TRUE and clears pending_two_factor. Implements SessionTwoFAMarker.
type EntityTwoFAStore ¶ added in v0.21.0
type EntityTwoFAStore struct {
// contains filtered or unexported fields
}
EntityTwoFAStore adapts a database table to the TwoFAStore interface — the durable sibling of MemoryTwoFAStore, mirroring EntitySessionStore. Without it, a restart (or a second replica) silently reverts every enrolled 2FA account to password-only auth, because enrollment lives only in process memory.
Usage:
mgr.Use(auth.NewTwoFAPlugin(auth.TwoFAConfig{
Store: auth.NewEntityTwoFAStore(db, "auth_twofa"),
}))
The plugin calls EnsureSchema at Init, so hosts never hand-roll the DDL.
func NewEntityTwoFAStore ¶ added in v0.21.0
func NewEntityTwoFAStore(db *sql.DB, table string) *EntityTwoFAStore
NewEntityTwoFAStore creates a TwoFAStore backed by a database table. Panics if the table name contains unsafe characters.
func (*EntityTwoFAStore) ConsumeBackupCode ¶ added in v0.21.0
func (s *EntityTwoFAStore) ConsumeBackupCode(ctx context.Context, userID string, code string) (bool, error)
ConsumeBackupCode checks the given code against the stored bcrypt hashes and, on a match, removes that code atomically. Concurrency-safe across replicas via an optimistic compare-and-swap on an integer version column (NOT the JSON bytes — so a non-canonically-formatted row can't wedge consumption): if two replicas race to consume the SAME code, exactly one CAS wins; the loser re-reads, no longer finds the code, and returns false.
func (*EntityTwoFAStore) DeleteTwoFA ¶ added in v0.21.0
func (s *EntityTwoFAStore) DeleteTwoFA(ctx context.Context, userID string) error
DeleteTwoFA removes the 2FA state for a user. Deleting an absent row is not an error.
func (*EntityTwoFAStore) EnsureSchema ¶ added in v0.21.0
func (s *EntityTwoFAStore) EnsureSchema(ctx context.Context) error
EnsureSchema creates the 2FA table if it does not already exist. Called by TwoFAPlugin.Init so hosts never hand-roll the DDL. Idempotent. The boolean column type is chosen per dialect (SQLite vs PostgreSQL) so the same battery boots on either.
func (*EntityTwoFAStore) GetTwoFA ¶ added in v0.21.0
func (s *EntityTwoFAStore) GetTwoFA(ctx context.Context, userID string) (*TwoFAState, error)
GetTwoFA retrieves the 2FA state for a user. Returns nil (not an error) when the user is not enrolled, matching MemoryTwoFAStore.
func (*EntityTwoFAStore) SetTwoFA ¶ added in v0.21.0
func (s *EntityTwoFAStore) SetTwoFA(ctx context.Context, userID string, state *TwoFAState) error
SetTwoFA upserts the 2FA state for a user. A nil state deletes the row, matching the semantics callers get from DeleteTwoFA.
type EntityUserStore ¶
type EntityUserStore struct {
// contains filtered or unexported fields
}
EntityUserStore adapts GoFastr's entity/CRUD system to the UserStore interface. It uses raw SQL against the entity's table so auth doesn't need to import the full CRUD handler.
The backing entity must have at minimum: id (string/uuid), email (string, unique), password_hash (string), roles (string/json).
Usage:
app.Entity("users", entity.EntityConfig{
Fields: auth.UserEntityFields(),
})
mgr.SetUserStore(auth.NewEntityUserStore(db, "users"))
func NewEntityUserStore ¶
func NewEntityUserStore(db *sql.DB, table string, fieldMap ...UserFieldMap) *EntityUserStore
NewEntityUserStore creates a UserStore backed by a database table. The table must have the columns described by fieldMap. Panics if the table name or any field mapping contains unsafe characters.
func (*EntityUserStore) CreateUser ¶
func (s *EntityUserStore) CreateUser(ctx context.Context, email, hashedPassword string, roles []string) (User, error)
CreateUser inserts a new user and returns it. Returns ErrEmailTaken when the email already exists; other errors are returned verbatim.
CreateUser marks password_set=true. Callers that auto-create an account without a user-chosen password (OAuth, magic-link) should prefer CreateUserNoPassword so HasPassword reports the right answer later.
func (*EntityUserStore) CreateUserNoPassword ¶
func (s *EntityUserStore) CreateUserNoPassword(ctx context.Context, email string, roles []string) (User, error)
CreateUserNoPassword auto-creates a user using the package-level placeholder hash and records password_set=false. Used by the OAuth / magic-link auto-create flows. Implements OAuthUserCreator.
func (*EntityUserStore) EnsureOAuthLinksSchema ¶ added in v0.36.0
func (s *EntityUserStore) EnsureOAuthLinksSchema(ctx context.Context) error
EnsureOAuthLinksSchema creates the (provider, provider_id) → user_id link table if it does not already exist. Called from EnsureSchema so hosts never hand-roll the DDL. Idempotent. Timestamp type is chosen per dialect so the same battery boots on SQLite and Postgres.
func (*EntityUserStore) EnsureSchema ¶ added in v0.7.0
func (s *EntityUserStore) EnsureSchema(ctx context.Context) error
EnsureSchema creates the user table if it does not already exist. The auth battery owns its schema, so hosts never hand-roll the DDL — AuthManager.Init calls this. Idempotent. Boolean columns use the native PostgreSQL BOOLEAN type so CreateUser's Go bool bindings work on a fresh Postgres database.
func (*EntityUserStore) FindByEmail ¶
FindByEmail looks up a user by email. Returns ErrUserNotFound when no row matches; any other error is returned verbatim.
func (*EntityUserStore) FindByID ¶
FindByID looks up a user by their unique ID. Returns ErrUserNotFound when no row matches; any other error is returned verbatim.
func (*EntityUserStore) FindByOAuth ¶ added in v0.36.0
func (s *EntityUserStore) FindByOAuth(ctx context.Context, provider, providerID string) (User, error)
FindByOAuth returns the locally-linked user for a (provider, providerID) pair, or ErrUserNotFound when no link exists. Implements OAuthLinker.
The lookup is two-step — read the user_id from the link table, then read the user — so the link table stays narrow and the user row stays the single source of truth for profile/roles. A link pointing at a since-deleted user resolves to ErrUserNotFound (FindByID's sentinel).
func (*EntityUserStore) HasPassword ¶
HasPassword reports whether the user has a real password set vs the placeholder hash. Implements PasswordChecker.
func (*EntityUserStore) LinkOAuth ¶ added in v0.36.0
func (s *EntityUserStore) LinkOAuth(ctx context.Context, userID, provider, providerID string) error
LinkOAuth binds a (provider, providerID) pair to a user. Idempotent and race-safe via INSERT ... ON CONFLICT (provider, provider_id) DO UPDATE on the profile columns ONLY: the PRIMARY KEY is the serialization point, so two concurrent first-logins for the same external identity cannot create conflicting rows. The user_id of an existing binding is immutable from this path — rebinding an identity to a different local account is an admin operation, not an OAuth callback. Implements OAuthLinker.
func (*EntityUserStore) LinkOAuthEnriched ¶ added in v0.36.0
func (s *EntityUserStore) LinkOAuthEnriched(ctx context.Context, userID, provider, providerID string, profile OAuthAccountProfile) error
LinkOAuthEnriched is LinkOAuth plus the profile snapshot at link time. Implements OAuthEnrichedLinker. The profile fields are informational — the authoritative identity is still the (provider, provider_id) pair. On a pre-existing link the profile is refreshed in place (the email a user sees in /auth/accounts should match what the IdP says now, not what it said at the first link).
func (*EntityUserStore) ListAccounts ¶ added in v0.36.0
ListAccounts returns every OAuth identity linked to userID, ordered deterministically by provider so the /auth/accounts UI is stable across calls. Implements AccountLister.
func (*EntityUserStore) ListUsers ¶ added in v0.18.0
func (s *EntityUserStore) ListUsers(ctx context.Context, opts ListUsersOptions) ([]User, int, error)
ListUsers enumerates accounts ordered by the email column, returning one page plus the total row count. Implements UserLister.
Only id/email/roles are selected — never password_hash — so a paged listing never surfaces password material. Roles are parsed with the store's existing parseRoles helper, matching FindByEmail/FindByID. The caller (AuthManager.ListUsers) clamps opts, so Limit>=1 and Offset>=0 hold here.
func (*EntityUserStore) SetPassword ¶
func (s *EntityUserStore) SetPassword(ctx context.Context, userID, hashedPassword string) error
SetPassword updates a user's password hash and marks password_set=true. Implements PasswordSetter (used by PasswordResetPlugin).
func (*EntityUserStore) UnlinkOAuth ¶ added in v0.36.0
func (s *EntityUserStore) UnlinkOAuth(ctx context.Context, userID, provider string) error
UnlinkOAuth removes every link for (userID, provider). Deleting an absent link is not an error — the caller (AccountsPlugin) has already verified the link exists and that removing it leaves the user with a login method. Implements AccountUnlinker.
func (*EntityUserStore) UpdateRoles ¶ added in v0.20.0
UpdateRoles replaces the roles for an existing user. It writes the JSON roles column via a parameterized UPDATE with query.QuoteIdent on the column name. Returns ErrUserNotFound when no row matches.
Unlike CreateUser (which uses formatRoles and defaults empty roles to ["user"]), UpdateRoles serializes the slice directly via json.Marshal so an empty roles list clears the column (writes "null") rather than silently stamping the default. Clearing roles is a legitimate admin operation; defaulting would be a privilege bug.
The roles are OPERATOR input (from an admin screen), never request data — the same security posture as AuthConfig.DefaultRoles. The caller (AuthManager.SetUserRoles) is responsible for sourcing them from an admin-gated context.
type GitHubProvider ¶
type GitHubProvider struct {
// contains filtered or unexported fields
}
GitHubProvider implements OAuth2Provider for GitHub's OAuth2 endpoints.
func NewGitHubProvider ¶
func NewGitHubProvider(clientID, clientSecret, redirectURL string) *GitHubProvider
NewGitHubProvider creates a GitHub OAuth2 provider.
func (*GitHubProvider) AuthURL ¶
func (g *GitHubProvider) AuthURL(state string) string
func (*GitHubProvider) ExchangeCode ¶
func (g *GitHubProvider) ExchangeCode(ctx context.Context, code string) (*OAuth2Token, error)
func (*GitHubProvider) FetchUserInfo ¶
func (g *GitHubProvider) FetchUserInfo(ctx context.Context, token string) (*OAuth2UserInfo, error)
func (*GitHubProvider) Name ¶
func (g *GitHubProvider) Name() string
func (*GitHubProvider) RefreshToken ¶ added in v0.3.3
func (g *GitHubProvider) RefreshToken(ctx context.Context, refreshToken string) (*OAuth2Token, error)
RefreshToken exchanges a GitHub refresh token for a fresh access token. Only GitHub Apps (and OAuth apps with expiring tokens enabled) issue refresh tokens; classic OAuth-app tokens never expire and won't have one.
type GoogleProvider ¶
type GoogleProvider struct {
// contains filtered or unexported fields
}
GoogleProvider implements OAuth2Provider for Google's OAuth2 endpoints.
func NewGoogleProvider ¶
func NewGoogleProvider(clientID, clientSecret, redirectURL string) *GoogleProvider
NewGoogleProvider creates a Google OAuth2 provider.
func (*GoogleProvider) AuthURL ¶
func (g *GoogleProvider) AuthURL(state string) string
func (*GoogleProvider) ExchangeCode ¶
func (g *GoogleProvider) ExchangeCode(ctx context.Context, code string) (*OAuth2Token, error)
func (*GoogleProvider) FetchUserInfo ¶
func (g *GoogleProvider) FetchUserInfo(ctx context.Context, token string) (*OAuth2UserInfo, error)
func (*GoogleProvider) Name ¶
func (g *GoogleProvider) Name() string
func (*GoogleProvider) RefreshToken ¶ added in v0.3.3
func (g *GoogleProvider) RefreshToken(ctx context.Context, refreshToken string) (*OAuth2Token, error)
RefreshToken exchanges a Google refresh token for a fresh access token. Google does not re-issue the refresh token on this grant, so the returned OAuth2Token.RefreshToken is normally empty — callers keep the stored one.
type JWTAuth ¶
JWTAuth manages JWT token generation and validation.
func NewJWTAuth ¶
NewJWTAuth creates a new JWTAuth with the given secret and token expiry duration.
func (*JWTAuth) GenerateToken ¶
GenerateToken creates a signed JWT token for the given user. The token contains the user's ID, email, and roles.
type ListUsersOptions ¶ added in v0.18.0
ListUsersOptions controls paged user enumeration. Limit and Offset are clamped by AuthManager.ListUsers before the store sees them, so custom UserLister implementations can assume sane values.
type MagicLinkConfig ¶
type MagicLinkConfig struct {
// TokenLength is the number of random bytes in the token (default: 32).
TokenLength int
// TokenTTL is how long the magic link remains valid (default: 15 minutes).
TokenTTL time.Duration
// EmailSender sends the magic link email. If nil, the URL is logged instead (dev mode).
EmailSender MagicLinkEmailSender
// TokenStore persists pending tokens. Defaults to an in-memory store,
// which does NOT survive restarts or scale across replicas — set a
// durable store (e.g. NewSQLMagicLinkTokenStore(db)) in production.
TokenStore MagicLinkTokenStore
// BodyTemplate, when non-nil, transforms the magic-link URL into
// the full email body before EmailSender.SendMagicLink is called.
// nil means "send the URL as the entire body" (historical behavior).
// Note: MagicLinkEmailSender's interface still takes (to, link)
// — implementations decide whether to wrap the link in HTML; this
// hook is the bridge for ones that just blast the body verbatim.
BodyTemplate func(url string) string
// BaseURL is the application base URL used to construct magic links
// (e.g. "http://localhost:8080").
BaseURL string
// OnSuccessURL is the URL to redirect to after successful login (default: "/").
OnSuccessURL string
// RateLimit, when non-nil, applies a per-IP rate limit to
// /auth/magic-link/send. Without this, an attacker can spam any
// recipient address. nil disables (not recommended).
RateLimit *RateLimiterConfig
// DevMode permits the plugin to operate without an EmailSender by
// logging the magic-link URL to stdout. Without this flag, a nil
// EmailSender causes /auth/magic-link/send to return 503 — refusing
// to silently log live tokens to production logs (which would let
// anyone with log read access take over arbitrary accounts).
DevMode bool
}
MagicLinkConfig configures the MagicLinkPlugin.
type MagicLinkEmailSender ¶
type MagicLinkEmailSender interface {
SendMagicLink(ctx context.Context, email, magicLinkURL string) error
}
MagicLinkEmailSender sends the magic-link email to the user. Implementations handle templating, delivery, and retries.
type MagicLinkPlugin ¶
type MagicLinkPlugin struct {
// contains filtered or unexported fields
}
MagicLinkPlugin implements AuthPlugin and AuthPluginRoutes for passwordless magic-link authentication.
func NewMagicLinkPlugin ¶
func NewMagicLinkPlugin(config MagicLinkConfig) *MagicLinkPlugin
NewMagicLinkPlugin creates a new magic-link plugin with the given config.
func (*MagicLinkPlugin) Init ¶
func (p *MagicLinkPlugin) Init(mgr *AuthManager) error
Init stores a reference to the AuthManager.
func (*MagicLinkPlugin) Name ¶
func (p *MagicLinkPlugin) Name() string
Name returns the plugin identifier.
func (*MagicLinkPlugin) RegisterRoutes ¶
func (p *MagicLinkPlugin) RegisterRoutes(r *router.Router, basePath string)
RegisterRoutes mounts the magic-link send and verify endpoints.
type MagicLinkTokenStore ¶
type MagicLinkTokenStore interface {
// CreateToken generates a new token for the given email, valid for ttl.
CreateToken(ctx context.Context, email string, ttl time.Duration) (token string, err error)
// RedeemToken atomically consumes the token and returns the associated email.
// Must return an error if the token is unknown, already redeemed, or expired.
RedeemToken(ctx context.Context, token string) (email string, err error)
// Cleanup removes expired tokens and returns the count purged.
Cleanup(ctx context.Context) (int, error)
}
MagicLinkTokenStore is the interface for persisting and consuming magic-link tokens. Implementations must ensure RedeemToken is atomic: a token can only be consumed once.
type MemoryMagicLinkTokenStore ¶
type MemoryMagicLinkTokenStore struct {
// contains filtered or unexported fields
}
MemoryMagicLinkTokenStore is a goroutine-safe in-memory MagicLinkTokenStore.
func NewMemoryMagicLinkTokenStore ¶
func NewMemoryMagicLinkTokenStore() *MemoryMagicLinkTokenStore
NewMemoryMagicLinkTokenStore returns a fresh in-memory token store.
func (*MemoryMagicLinkTokenStore) Cleanup ¶
func (m *MemoryMagicLinkTokenStore) Cleanup(_ context.Context) (int, error)
Cleanup removes all expired tokens and returns the count purged.
func (*MemoryMagicLinkTokenStore) CreateToken ¶
func (m *MemoryMagicLinkTokenStore) CreateToken(_ context.Context, email string, ttl time.Duration) (string, error)
CreateToken generates a cryptographically random hex-encoded token, stores it with the given email and TTL, and returns the token string.
func (*MemoryMagicLinkTokenStore) RedeemToken ¶
RedeemToken atomically consumes a token. Returns the associated email. Returns ErrTokenNotFound if the token is unknown or expired.
type MemorySessionStore ¶
type MemorySessionStore struct {
// contains filtered or unexported fields
}
MemorySessionStore is a goroutine-safe in-memory SessionStore suitable for single-instance deployments and tests. Tokens are 32 cryptographically random bytes base64'd, mirroring what most cookie auth systems use.
func NewMemorySessionStore ¶
func NewMemorySessionStore() *MemorySessionStore
NewMemorySessionStore returns a fresh, empty MemorySessionStore.
func (*MemorySessionStore) Cleanup ¶
func (m *MemorySessionStore) Cleanup(_ context.Context) (int, error)
Cleanup removes all expired sessions and returns the count purged. Safe to call on a timer; concurrent with Get/Create/Delete.
func (*MemorySessionStore) Create ¶
func (m *MemorySessionStore) Create(_ context.Context, userID string, ttl time.Duration) (*Session, error)
Create generates a random token, persists a new Session keyed on it, and returns the result. ttl is the session lifetime; consumers passing 0 get a sensible default (one week).
func (*MemorySessionStore) Delete ¶
func (m *MemorySessionStore) Delete(_ context.Context, token string) error
Delete drops the session for token. Returns nil even if the token is unknown — idempotent logout.
func (*MemorySessionStore) DeleteByUser ¶
DeleteByUser removes every session belonging to userID and returns the count purged. Implements SessionUserPurger.
func (*MemorySessionStore) Get ¶
Get returns the session for the given token, or ErrSessionNotFound if unknown / expired.
func (*MemorySessionStore) MarkPendingTwoFactor ¶
func (m *MemorySessionStore) MarkPendingTwoFactor(_ context.Context, token string) error
MarkPendingTwoFactor flips PendingTwoFactor=true on the session. No-op if the session is unknown.
func (*MemorySessionStore) MarkTwoFactorVerified ¶
func (m *MemorySessionStore) MarkTwoFactorVerified(_ context.Context, token string) error
MarkTwoFactorVerified flips TwoFactorVerified=true and clears PendingTwoFactor on the session. No-op (and no error) if the session is unknown.
type MemoryTwoFAStore ¶
type MemoryTwoFAStore struct {
// contains filtered or unexported fields
}
MemoryTwoFAStore is a goroutine-safe in-memory TwoFAStore for dev/test.
func NewMemoryTwoFAStore ¶
func NewMemoryTwoFAStore() *MemoryTwoFAStore
NewMemoryTwoFAStore creates a fresh, empty MemoryTwoFAStore.
func (*MemoryTwoFAStore) ConsumeBackupCode ¶
func (*MemoryTwoFAStore) DeleteTwoFA ¶
func (m *MemoryTwoFAStore) DeleteTwoFA(_ context.Context, userID string) error
func (*MemoryTwoFAStore) GetTwoFA ¶
func (m *MemoryTwoFAStore) GetTwoFA(_ context.Context, userID string) (*TwoFAState, error)
func (*MemoryTwoFAStore) SetTwoFA ¶
func (m *MemoryTwoFAStore) SetTwoFA(_ context.Context, userID string, state *TwoFAState) error
type OAuth2Config ¶
type OAuth2Config struct {
// Providers is a map of provider name → provider implementation.
Providers map[string]OAuth2Provider
// RedirectURL is the base redirect URL for OAuth2 callbacks.
// The provider-specific path is appended automatically.
RedirectURL string
// StateSecret is the HMAC key used to sign OAuth2 state tokens.
// If empty, a random key is generated at startup (suitable for
// single-instance deployments only).
StateSecret string
// TokenStore, when set, persists each provider's access/refresh token
// at login so that calls made on the user's behalf can recover after
// the (short-lived) access token expires — see RefreshOAuthToken /
// ValidOAuthToken. Opt-in: when nil, OAuth login behaves exactly as
// before and the provider's refresh token is discarded.
TokenStore OAuthTokenStore
}
OAuth2Config holds the configuration for the OAuth2 plugin.
type OAuth2Plugin ¶
type OAuth2Plugin struct {
// contains filtered or unexported fields
}
OAuth2Plugin implements AuthPlugin and AuthPluginRoutes to provide OAuth2-based authentication via configurable providers.
State is stateless: the token itself carries (nonce, provider, expiryUnix, hmac). The plugin never persists per-redirect state, so server restarts mid-flow don't invalidate in-flight logins and the redirect endpoint can't be used as a memory-pressure DoS surface. Replay protection comes from a bounded usedNonces map that's only touched on the (much rarer) successful callback path.
func NewOAuth2Plugin ¶
func NewOAuth2Plugin(cfg OAuth2Config) *OAuth2Plugin
NewOAuth2Plugin creates an OAuth2 plugin with the given configuration.
func (*OAuth2Plugin) Init ¶
func (p *OAuth2Plugin) Init(mgr *AuthManager) error
Init stores a reference to the AuthManager and fails closed when the configured UserStore is not a durable OAuth link store.
A non-linker store means the OAuth callback cannot bind identity to (provider, provider_id) — it would have to fall back to email-only matching, which lets an IdP emitting an unverified email sign in as an existing local account. That fallback is removed; production must refuse to boot rather than silently expose the takeover.
DevMode and AllowInMemoryStores keep the no-linker path reachable so the rest of the OAuth plumbing (redirect, state token, callback errors) stays unit-testable without a SQLite/Postgres backing store. The path logs a WARN — `resolveOAuthUser` itself returns errOAuthNoLinker rather than silently downgrading, so a DevMode test that actually drives the callback gets a loud failure, not a silent email-trust login.
func (*OAuth2Plugin) Name ¶
func (p *OAuth2Plugin) Name() string
Name returns the plugin identifier.
func (*OAuth2Plugin) RegisterProvider ¶
func (p *OAuth2Plugin) RegisterProvider(name string, provider OAuth2Provider)
RegisterProvider adds a provider to the plugin's registry.
func (*OAuth2Plugin) RegisterRoutes ¶
func (p *OAuth2Plugin) RegisterRoutes(r *router.Router, basePath string)
RegisterRoutes mounts the OAuth2 auth and callback routes.
type OAuth2Provider ¶
type OAuth2Provider interface {
// Name returns the provider identifier (e.g. "google", "github").
Name() string
// AuthURL returns the URL to redirect the user to for authorisation.
// The state parameter is included for CSRF protection.
AuthURL(state string) string
// ExchangeCode exchanges the authorisation code for an access token.
ExchangeCode(ctx context.Context, code string) (*OAuth2Token, error)
// FetchUserInfo uses the access token to fetch the user's profile.
FetchUserInfo(ctx context.Context, token string) (*OAuth2UserInfo, error)
}
OAuth2Provider is the interface each OAuth2 provider (Google, GitHub, etc.) must implement. Providers are registered on the OAuth2Plugin via RegisterProvider or passed in through OAuth2Config.
type OAuth2Token ¶
OAuth2Token holds the token information returned by an OAuth2 provider.
type OAuth2UserInfo ¶
type OAuth2UserInfo struct {
ID string
Email string
Name string
AvatarURL string
Provider string
// EmailVerified reports whether the provider has VERIFIED the email.
// The OAuth callback uses this to decide whether an email match may
// bind to an existing local account: an unverified email must NEVER
// match. Defaults to false — a provider that cannot assert
// verification must not be treated as if it had.
EmailVerified bool
}
OAuth2UserInfo holds the user profile fetched from an OAuth2 provider.
type OAuthAccountProfile ¶
OAuthAccountProfile is the optional profile payload an OAuthEnrichedLinker receives at link time. Mirrors the subset of OAuth2UserInfo that's worth surfacing in a UI later.
type OAuthEnrichedLinker ¶
type OAuthEnrichedLinker interface {
LinkOAuthEnriched(ctx context.Context, userID, provider, providerID string, profile OAuthAccountProfile) error
}
OAuthEnrichedLinker is the optional UserStore extension that accepts profile metadata at link time so AccountLister can return it later. Stores that implement this interface get the OAuth handler's full FetchUserInfo payload; stores that only implement OAuthLinker get just (provider, providerID) and the rest is dropped.
type OAuthLinker ¶
type OAuthLinker interface {
FindByOAuth(ctx context.Context, provider, providerID string) (User, error)
LinkOAuth(ctx context.Context, userID, provider, providerID string) error
}
OAuthLinker is the REQUIRED UserStore extension for OAuth login: it binds a user to a durable (provider, provider_id) pair. OAuth2Plugin.Init fails closed in production when the configured store is not an OAuthLinker (only DevMode / AllowInMemoryStores relaxes it), so the legacy email-only path is gone. EntityUserStore now implements this by default.
The callback's resolveOAuthUser decision table (see oauth2.go):
- FindByOAuth(provider, providerID) matches → LOGIN as that user.
- No link, but the provider's email (email_verified) matches an existing local account: - account HAS a password → REFUSED (409): the user must log in with their password and add the provider via the authenticated link flow (GET /auth/oauth/{provider}/link) — protects the local credential from IdP-email takeover. - account is PASSWORDLESS → AUTO-LINK + login (safe migration: a prior OAuth-created account re-binds to the same verified identity).
- Email is UNVERIFIED, or no email match → CreateUser + LinkOAuth (a fresh distinct account; an unverified email never binds to an existing one). See framework/docs/content/auth.md for the full contract.
type OAuthTokenRecord ¶ added in v0.3.3
type OAuthTokenRecord struct {
// UserID is the local user/account id the token belongs to.
UserID string
// Provider is the OAuth2 provider name ("google", "github", …).
Provider string
// AccessToken is the current provider access token.
AccessToken string
// RefreshToken is the long-lived token used to mint new access tokens.
// May be empty when the provider did not issue one.
RefreshToken string
// Expiry is when AccessToken stops being valid. Zero means "unknown";
// callers treat a zero expiry as already-expired so a refresh is forced.
Expiry time.Time
}
OAuthTokenRecord is one provider token tied to a local user. The store persists it so that calls made on the user's behalf (e.g. reading a Google calendar) can recover after the short-lived access token expires by exchanging the refresh token for a fresh one.
func RefreshOAuthToken ¶ added in v0.3.3
func RefreshOAuthToken(ctx context.Context, store OAuthTokenStore, provider OAuth2Provider, userID string) (OAuthTokenRecord, error)
RefreshOAuthToken loads the stored token for userID, exchanges its refresh token for a fresh access token via the provider, and writes the result back to the store. It returns the updated record.
The provider must implement OAuthTokenRefresher (the built-in Google and GitHub providers do). If the stored token has no refresh token, refresh is impossible and an error is returned — the user must re-authenticate.
SECURITY: userID MUST be the authenticated principal's id (e.g. from the resolved session), never a value taken from request input. Passing a client-supplied id is an IDOR — it reads/refreshes another user's tokens.
type OAuthTokenRefresher ¶ added in v0.3.3
type OAuthTokenRefresher interface {
// RefreshToken exchanges refreshToken for a fresh access token. The
// returned token's RefreshToken may be empty when the provider does
// not re-issue one (Google's typical behavior); callers retain the
// previously stored refresh token in that case.
RefreshToken(ctx context.Context, refreshToken string) (*OAuth2Token, error)
}
OAuthTokenRefresher is implemented by providers that can exchange a refresh token for a fresh access token at the provider's token endpoint. The built-in GoogleProvider and GitHubProvider implement it. The refresh path is concrete per provider — there is deliberately no generic provider registry here.
type OAuthTokenStore ¶ added in v0.3.3
type OAuthTokenStore interface {
// Save inserts or replaces the token row for (UserID, Provider).
Save(ctx context.Context, rec OAuthTokenRecord) error
// Get returns the stored token for the pair, or ErrOAuthTokenNotFound.
Get(ctx context.Context, userID, provider string) (OAuthTokenRecord, error)
// Delete removes the stored token for the pair. Deleting a missing row
// is not an error.
Delete(ctx context.Context, userID, provider string) error
}
OAuthTokenStore persists provider tokens per (user, provider). It mirrors the durable-store shape auth already uses for magic-link/reset/verify tokens. The store is opt-in: OAuth login works without one, but no refresh is possible until a store is configured.
type OAuthUserCreator ¶
type OAuthUserCreator interface {
CreateUserNoPassword(ctx context.Context, email string, roles []string) (User, error)
}
OAuthUserCreator is the optional UserStore extension that creates a user known to have NO password (OAuth or magic-link auto-create). The store is responsible for recording that fact so a subsequent HasPassword call returns false. Stores that don't implement it get the legacy "CreateUser with placeholder hash" path, which is functional but cannot distinguish password-set from placeholder-only users.
type OIDCClaimsMapping ¶ added in v0.15.0
type OIDCClaimsMapping struct {
IDClaim string
EmailClaim string
NameClaim string
AvatarClaim string
// EmailVerifiedClaim names the boolean claim that asserts the email
// has been verified by the IdP. Defaults to "email_verified" per
// OIDC Core §5.1. Some IdPs emit the value as the string "true"
// rather than a JSON bool; both shapes are accepted (the string is
// matched case-insensitively). When the claim is absent or unparseable,
// EmailVerified is set to false — an unverifiable email must never
// bind to an existing local account.
EmailVerifiedClaim string
}
OIDCClaimsMapping maps id_token/userinfo claim names to the fields OAuth2UserInfo exposes. Zero-value fields fall back to the OIDC standard claims (sub, email, name, picture).
type OIDCConfig ¶ added in v0.15.0
type OIDCConfig struct {
// Issuer is the IdP issuer identifier, e.g.
// "https://keycloak.example/realms/myrealm". Required. Must be an https://
// URL; http:// is accepted only for localhost/127.0.0.1/::1 (local IdPs
// and tests).
Issuer string
// ClientID is the OAuth client_id registered at the IdP. Required.
ClientID string
// ClientSecret is the confidential-client secret. Required.
ClientSecret string
// RedirectURL is the app's callback URL. Required.
RedirectURL string
// ProviderName is returned by Name() and set as OAuth2UserInfo.Provider.
// Defaults to "oidc".
ProviderName string
// Scopes requested at the authorization endpoint. Defaults to
// ["openid","email","profile"].
Scopes []string
// Claims maps id_token/userinfo claims to OAuth2UserInfo fields.
Claims OIDCClaimsMapping
// HTTPClient overrides the default 10s-deadline client. If nil,
// defaultOAuthHTTPClient is used.
HTTPClient *http.Client
// JWKSCacheTTL is how long a fetched JWKS is trusted before refresh.
// Defaults to 1h.
JWKSCacheTTL time.Duration
}
OIDCConfig configures an OIDCProvider.
type OIDCProvider ¶ added in v0.15.0
type OIDCProvider struct {
// contains filtered or unexported fields
}
OIDCProvider implements OAuth2Provider for a generic OIDC IdP.
func NewOIDCProvider ¶ added in v0.15.0
func NewOIDCProvider(cfg OIDCConfig) (*OIDCProvider, error)
NewOIDCProvider validates cfg and returns a provider WITHOUT performing any network I/O — discovery runs lazily on first use.
func (*OIDCProvider) AuthURL ¶ added in v0.15.0
func (p *OIDCProvider) AuthURL(state string) string
AuthURL builds the authorization-endpoint redirect. No nonce is sent: this is the confidential-client authorization-code flow — the code is single-use and exchanged server-to-server with the client secret, and the OAuth2 plugin's HMAC state token already binds the callback to this redirect. A nonce is only mandatory for the implicit/hybrid flow, where no server-side code exchange happens.
AuthURL cannot return an error. If discovery has not yet run (or fails), it falls back to a best-effort "<issuer>/authorize?…" URL and lets the callback fail cleanly rather than send the user to a stale or wrong endpoint.
func (*OIDCProvider) ExchangeCode ¶ added in v0.15.0
func (p *OIDCProvider) ExchangeCode(ctx context.Context, code string) (*OAuth2Token, error)
ExchangeCode trades the authorization code for tokens at the token endpoint, then fully verifies the id_token BEFORE returning. Verified claims are cached by access token so FetchUserInfo can map them without re-fetching.
func (*OIDCProvider) FetchUserInfo ¶ added in v0.15.0
func (p *OIDCProvider) FetchUserInfo(ctx context.Context, token string) (*OAuth2UserInfo, error)
FetchUserInfo maps the verified id_token claims (cached at ExchangeCode) into OAuth2UserInfo. If the mapped email is empty and the IdP exposes a userinfo_endpoint, it is fetched with the bearer token and only the missing email/name/avatar fields are merged in (the userinfo sub MUST match the id_token sub). With no cached claims, the info is built purely from userinfo.
func (*OIDCProvider) Name ¶ added in v0.15.0
func (p *OIDCProvider) Name() string
Name returns the provider identifier.
type PasswordChecker ¶
PasswordChecker is the optional UserStore extension that reports whether a user has a real password set (vs the placeholder hash used by OAuth / magic-link auto-created accounts). AccountsPlugin.unlinkHandler uses it to refuse an unlink that would leave the user with no way to log in. Stores that do not implement it fall back to the conservative links-only check.
type PasswordResetConfig ¶
type PasswordResetConfig struct {
// BaseURL is the application URL used to construct the reset link
// emailed to the user (e.g. "https://app.example.com").
BaseURL string
// TokenTTL is the reset link's lifetime. Default: 1h. Short by design —
// reset tokens are a transient secret and short lifetimes limit the
// damage if logs / referer headers leak them.
TokenTTL time.Duration
// EmailSender sends the reset email. If nil, DevMode must be set or
// /auth/forgot-password fails closed (it still returns 200 to avoid
// account enumeration, but no email is sent).
EmailSender EmailSender
// TokenStore persists pending reset tokens. Defaults to in-memory (does
// not survive restart / scale across replicas) — set a durable store
// (e.g. NewSQLMagicLinkTokenStore(db)) in production.
TokenStore MagicLinkTokenStore
// BodyTemplate, when non-nil, transforms the reset URL into the
// full email body before EmailSender.Send is called. nil means
// "send the URL as the entire body" (the historical behavior).
BodyTemplate func(url string) string
// DevMode logs the reset URL when EmailSender is nil. NEVER enable in
// production — anyone with log read access then resets arbitrary
// passwords. The log entry uses hashed identifiers to limit exposure,
// but the URL itself is the secret.
DevMode bool
// RateLimit, when non-nil, applies a per-IP limit to both endpoints.
// Strongly recommended in production.
RateLimit *RateLimiterConfig
}
PasswordResetConfig configures the plugin.
type PasswordResetPlugin ¶
type PasswordResetPlugin struct {
// contains filtered or unexported fields
}
PasswordResetPlugin wires:
- POST /auth/forgot-password (unauthenticated; takes {email}; sends a reset link if the email exists; ALWAYS returns 200 to avoid leaking account existence).
- POST /auth/reset-password (takes {token, password}; verifies the token; updates the user's password).
func NewPasswordResetPlugin ¶
func NewPasswordResetPlugin(cfg PasswordResetConfig) *PasswordResetPlugin
NewPasswordResetPlugin builds the plugin with sensible defaults.
func (*PasswordResetPlugin) Init ¶
func (p *PasswordResetPlugin) Init(mgr *AuthManager) error
func (*PasswordResetPlugin) Name ¶
func (p *PasswordResetPlugin) Name() string
func (*PasswordResetPlugin) RegisterRoutes ¶
func (p *PasswordResetPlugin) RegisterRoutes(r *router.Router, basePath string)
type PasswordSetter ¶
type PasswordSetter interface {
SetPassword(ctx context.Context, userID, hashedPassword string) error
}
PasswordSetter is the optional UserStore extension used by the PasswordResetPlugin to persist a new bcrypt hash for an existing user. The store implementation is responsible for validating the user exists; a nil error means the password was updated.
type PolicyOption ¶
type PolicyOption func(*policyOpts)
PolicyOption tunes a policy constructor (SessionPolicy, RolePolicy). Options control what happens on policy failure: redirect, render an alternate component, or block with an HTTP status. The default for SessionPolicy is Redirect("/login?next=…"); the default for RolePolicy is Block(403).
func WithBlock ¶
func WithBlock(status int, msg string) PolicyOption
WithBlock sets the failure outcome to an HTTP status (e.g. 401, 403, 404). msg is optional human-readable text the host may include in the body. Default for SessionPolicy when no option is supplied is Redirect("/login"); for RolePolicy it is Block(403).
func WithRedirect ¶
func WithRedirect(url string, opts ...RedirectOpt) PolicyOption
WithRedirect sets the failure outcome to a 303 redirect to url. By default, the policy appends ?next=<encoded-request-path> so the post-login flow can return the user where they were going. Pass auth.NoNext() to suppress the next-path append for fixed redirects (e.g. "/" → marketing).
auth.WithRedirect("/login") // next-path appended
auth.WithRedirect("/", auth.NoNext()) // bare redirect, no next
func WithRenderAlt ¶
func WithRenderAlt(factory func() component.Component) PolicyOption
WithRenderAlt sets the failure outcome to rendering an alt component instead of the screen's own. factory MUST return a FRESH instance every call — the framework invokes it once per request and the returned component is then mutated by SetParams / Inject / Load under the screen lock. A shared singleton is a cross-user data leak.
For a stateless anon-landing page:
auth.WithRenderAlt(func() component.Component { return &Landing{} })
type RateLimitStore ¶ added in v0.21.0
type RateLimitStore interface {
Allow(ctx context.Context, key string, cfg RateLimiterConfig) (allowed bool, retryAfter time.Duration, err error)
}
RateLimitStore is the optional shared backend for RateLimiter. When RateLimiterConfig.Store is set, every replica consults the same attempt ledger, so the brute-force budget stays MaxAttempts total instead of MaxAttempts × replicas and a block on one replica holds on all of them.
The store receives the limiter's resolved config on every call so one store instance can serve limiters with different budgets (login, register, 2FA challenge) — implementations must derive per-limiter state from the key alone, which the RateLimiter already namespaces.
type RateLimiter ¶
type RateLimiter struct {
// contains filtered or unexported fields
}
RateLimiter is an in-memory sliding-window rate limiter keyed by an arbitrary string (typically the client IP).
func NewRateLimiter ¶
func NewRateLimiter(cfg RateLimiterConfig) *RateLimiter
NewRateLimiter constructs a RateLimiter with the given config. Zero fields fall back to the documented defaults.
func (*RateLimiter) Allow ¶
func (rl *RateLimiter) Allow(key string) (allowed bool, retryAfter time.Duration)
Allow records an attempt for key and returns whether it is allowed. If not allowed, retryAfter is the duration the caller should communicate in a Retry-After header. Equivalent to AllowContext with a background context — HTTP paths should prefer AllowContext(r.Context(), key).
func (*RateLimiter) AllowContext ¶ added in v0.21.0
func (rl *RateLimiter) AllowContext(ctx context.Context, key string) (allowed bool, retryAfter time.Duration)
AllowContext records an attempt for key against the configured backend: the shared Store when one is set (replica-wide budget), the in-process sliding window otherwise. A store failure DENIES the attempt — the limiter guards brute-force surfaces, so it must fail closed: degrading its backend must never lift the limit.
DevMode (see RateLimiterConfig) is an explicit, tested short-circuit: when set, every attempt is admitted without touching either backend. This is the dev-only relaxation that stops local tooling being locked out; production never sets it, so the fail-closed guarantee holds.
func (*RateLimiter) Middleware ¶
func (rl *RateLimiter) Middleware() func(http.Handler) http.Handler
Middleware returns an HTTP middleware that rate-limits by client IP. Blocked requests get 429 with a Retry-After header.
It deliberately emits ONLY Retry-After and never the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset budget headers that the generic token-bucket middleware (core/middleware.RateLimit) exposes: a live remaining-attempt count on login / password-reset endpoints would hand an attacker exact brute-force pacing information.
type RateLimiterConfig ¶
type RateLimiterConfig struct {
MaxAttempts int
Window time.Duration
BlockDuration time.Duration
TrustForwardedFor bool
Store RateLimitStore
Scope string
// DevMode relaxes the limiter: when true, AllowContext short-circuits
// and admits every attempt. Intended ONLY for non-production deploys
// so local screenshot / verification tooling that hammers an endpoint
// from one IP (localhost) is never locked out. The default (false)
// keeps the limiter fail-closed — production must NEVER set this.
//
// The framework auto-propagates AuthConfig.DevMode to the per-IP
// login limiter only; the per-ACCOUNT login limiter stays active in
// dev because it guards brute-force even there. Set DevMode
// explicitly on any other limiter you want relaxed in dev.
DevMode bool
}
RateLimiterConfig controls a per-key sliding-window limiter.
MaxAttempts requests are permitted within Window. The MaxAttempts+1th request triggers a block of BlockDuration during which every request gets 429.
Defaults (filled in by NewRateLimiter when zero): MaxAttempts=10, Window=15m, BlockDuration=30m. Same shape as framework/auth.Guard so the two implementations stay in sync.
TrustForwardedFor: when true, the leftmost X-Forwarded-For entry is used as the client IP. ONLY enable this if the server sits behind a trusted reverse proxy that strips client-supplied XFF headers — otherwise an attacker rotates the header per request and bypasses every per-IP limit. Default is false (use the connection's RemoteAddr).
Store: when non-nil, attempts are recorded in the shared backend (e.g. SQLRateLimitStore) instead of process memory, so the budget holds across replicas: MaxAttempts total, not MaxAttempts × N, and a block on one replica blocks on all. On a store error the limiter fails CLOSED (denies) — an attacker must never be able to lift the limit by degrading its backend. One store instance can back several limiters: keys are namespaced by Scope.
Scope namespaces this limiter's keys inside a shared Store (an IP tracked by the login limiter must not consume the 2FA challenge budget). Each built-in consumer defaults its own scope ("login_ip", "login_account", "register", "twofa", "magiclink", "password_reset", "email_verification"); set it explicitly only for custom limiters. Ignored when Store is nil.
type RedirectOpt ¶
type RedirectOpt func(*redirectCfg)
RedirectOpt tunes WithRedirect behaviour.
func NoNext ¶
func NoNext() RedirectOpt
NoNext suppresses the auto-appended ?next=<request-path> on a WithRedirect option. Use for fixed redirects where preserving the origin path would be incorrect (e.g. anon "/" → "/marketing").
type RequireSessionOption ¶
type RequireSessionOption func(*requireSessionOptions)
RequireSessionOption configures RequireSession.
func WithRedirectOnFail ¶
func WithRedirectOnFail(path string) RequireSessionOption
WithRedirectOnFail redirects unauthenticated HTML requests to the given path (e.g. "/login") instead of returning JSON 401. JSON requests still receive 401 — only browsers get the redirect.
type SQLAPITokenStore ¶ added in v0.15.0
type SQLAPITokenStore struct {
// contains filtered or unexported fields
}
SQLAPITokenStore is the database-backed APITokenStore. Tokens are stored as a sha256 hash (never the plaintext), with a display Prefix and JSON scopes. It works on both SQLite and PostgreSQL via the battery's dialect conventions ($N placeholders, portable column types).
Schema (table auth_api_tokens, created at construction):
id TEXT PRIMARY KEY name TEXT NOT NULL owner_kind TEXT NOT NULL -- "user" | "service" owner_id TEXT NOT NULL prefix TEXT NOT NULL -- first 12 chars of plaintext hash TEXT NOT NULL UNIQUE -- sha256hex(plaintext) scopes TEXT NOT NULL DEFAULT '[]' expires_at <ts> NULL last_used_at <ts> NULL revoked_at <ts> NULL created_at <ts> NOT NULL
func NewSQLAPITokenStore ¶ added in v0.15.0
func NewSQLAPITokenStore(db *sql.DB, opts ...SQLAPITokenStoreOption) (*SQLAPITokenStore, error)
NewSQLAPITokenStore creates the token table (IF NOT EXISTS) and returns the store. Table-name options are validated like the battery's other stores; schema is ensured at construction.
func (*SQLAPITokenStore) Create ¶ added in v0.15.0
Create inserts a token row. sha256Hash is the hash of the plaintext; the plaintext itself never reaches this layer.
func (*SQLAPITokenStore) FindByHash ¶ added in v0.15.0
FindByHash looks up a token by its sha256 hash. Returns (nil, nil) for unknown hashes.
func (*SQLAPITokenStore) List ¶ added in v0.15.0
List returns the tokens owned by (ownerKind, ownerID), newest first.
func (*SQLAPITokenStore) ListAll ¶ added in v0.28.0
func (s *SQLAPITokenStore) ListAll(ctx context.Context) ([]APIToken, error)
ListAll returns every token across all owners, newest first — the admin view. Deliberately NOT part of APITokenStore: the plugin's self-service routes must never reach it; hosts wire it into their own admin-gated surface from the concrete store.
func (*SQLAPITokenStore) Revoke ¶ added in v0.15.0
func (s *SQLAPITokenStore) Revoke(ctx context.Context, id, ownerKind, ownerID string) error
Revoke stamps RevokedAt on the token owned by (ownerKind, ownerID). It is idempotent (an already-revoked token is a no-op success) and owner-scoped (a foreign id returns ErrTokenNotFound so the handler can answer 404).
func (*SQLAPITokenStore) RevokeAny ¶ added in v0.28.0
func (s *SQLAPITokenStore) RevokeAny(ctx context.Context, id string) error
RevokeAny stamps RevokedAt regardless of owner — the admin action. Same idempotency contract as Revoke; ErrTokenNotFound for unknown ids. Like ListAll, it is host-wired only, never exposed via the plugin routes.
func (*SQLAPITokenStore) TouchLastUsed ¶ added in v0.15.0
TouchLastUsed stamps last_used_at. The middleware calls this throttled (≥60s since the previous stamp) and best-effort.
type SQLAPITokenStoreOption ¶ added in v0.15.0
type SQLAPITokenStoreOption func(*sqlAPITokenStoreConfig)
SQLAPITokenStoreOption tunes NewSQLAPITokenStore.
func WithAPITokenTable ¶ added in v0.15.0
func WithAPITokenTable(name string) SQLAPITokenStoreOption
WithAPITokenTable overrides the token table name (default "auth_api_tokens"). Validated like the battery's other stores.
type SQLMagicLinkTokenStore ¶
type SQLMagicLinkTokenStore struct {
// contains filtered or unexported fields
}
SQLMagicLinkTokenStore is a database-backed MagicLinkTokenStore. Unlike MemoryMagicLinkTokenStore, tokens survive process restarts and are shared across replicas, so magic-link / passwordless login works in a horizontally scaled deployment. Tokens are single-use and time-limited.
Schema (created on construction): a table with a TEXT primary-key token, the associated email, and an expiry stored as a unix timestamp (portable across SQLite and Postgres without time-format ambiguity).
func NewSQLMagicLinkTokenStore ¶
func NewSQLMagicLinkTokenStore(db *sql.DB, table ...string) (*SQLMagicLinkTokenStore, error)
NewSQLMagicLinkTokenStore creates the token table (IF NOT EXISTS) and returns the store. Pass an optional table name; defaults to "magic_link_tokens".
func (*SQLMagicLinkTokenStore) Cleanup ¶
func (s *SQLMagicLinkTokenStore) Cleanup(ctx context.Context) (int, error)
Cleanup deletes expired tokens and returns the count removed.
func (*SQLMagicLinkTokenStore) CreateToken ¶
func (s *SQLMagicLinkTokenStore) CreateToken(ctx context.Context, email string, ttl time.Duration) (string, error)
CreateToken generates a cryptographically random token, persists it with the email and TTL, and returns it.
func (*SQLMagicLinkTokenStore) RedeemToken ¶
RedeemToken atomically consumes a token (single-use) via DELETE … RETURNING, returning the associated email. Returns ErrTokenNotFound when the token is unknown, already redeemed, or expired — the row is removed regardless so a known-but-expired token can never be reused.
type SQLOAuthTokenStore ¶ added in v0.3.3
type SQLOAuthTokenStore struct {
// contains filtered or unexported fields
}
SQLOAuthTokenStore is a database-backed OAuthTokenStore. Tokens survive process restarts and are shared across replicas. Access and refresh tokens are sealed with AES-GCM before they touch the database so a raw table dump does not leak the live provider secrets.
Schema (created on construction): (user_id, provider) composite primary key, sealed access/refresh token columns, and an expiry stored as a unix timestamp (portable across SQLite and Postgres).
func NewSQLOAuthTokenStore ¶ added in v0.3.3
func NewSQLOAuthTokenStore(db *sql.DB, cfg ...SQLOAuthTokenStoreConfig) (*SQLOAuthTokenStore, error)
NewSQLOAuthTokenStore creates the token table (IF NOT EXISTS) and returns the store. A non-empty EncryptionKey is REQUIRED — stored refresh tokens are password-equivalent, so the store fails closed rather than sealing them with a default key (which would be reversible obfuscation, not encryption).
func (*SQLOAuthTokenStore) Delete ¶ added in v0.3.3
func (s *SQLOAuthTokenStore) Delete(ctx context.Context, userID, provider string) error
Delete removes the stored token; deleting a missing row is not an error.
func (*SQLOAuthTokenStore) Get ¶ added in v0.3.3
func (s *SQLOAuthTokenStore) Get(ctx context.Context, userID, provider string) (OAuthTokenRecord, error)
Get returns the stored token, or ErrOAuthTokenNotFound.
func (*SQLOAuthTokenStore) Save ¶ added in v0.3.3
func (s *SQLOAuthTokenStore) Save(ctx context.Context, rec OAuthTokenRecord) error
Save upserts the token row for (UserID, Provider).
type SQLOAuthTokenStoreConfig ¶ added in v0.3.3
type SQLOAuthTokenStoreConfig struct {
// Table is the table name; defaults to "oauth_tokens".
Table string
// EncryptionKey seals the access/refresh tokens at rest with AES-GCM.
// REQUIRED and non-empty — stored refresh tokens are password-equivalent,
// so there is no default key. Any length is accepted (it is SHA-256-folded
// to a 32-byte key). Source it from a secret manager, not source code.
EncryptionKey []byte
}
SQLOAuthTokenStoreConfig tunes the SQL-backed store. EncryptionKey is required (NewSQLOAuthTokenStore fails closed without it).
type SQLRateLimitStore ¶ added in v0.21.0
type SQLRateLimitStore struct {
// contains filtered or unexported fields
}
SQLRateLimitStore is a RateLimitStore backed by two database tables (SQLite or PostgreSQL): one row per attempt plus a per-key block row. Timestamps are stored as unix milliseconds so window comparisons are plain integer arithmetic on both dialects.
Usage:
shared := auth.NewSQLRateLimitStore(db, "auth_rate_limits")
mgr := auth.New(auth.AuthConfig{
LoginRateLimit: &auth.RateLimiterConfig{Store: shared},
...
})
The schema is created lazily on first use — hosts never hand-roll the DDL. Concurrent replicas may overshoot MaxAttempts by at most the number of simultaneously in-flight requests (the count-then-insert is not serialized); that error is bounded and far smaller than the MaxAttempts × replicas budget the in-process limiter degrades to.
func NewSQLRateLimitStore ¶ added in v0.21.0
func NewSQLRateLimitStore(db *sql.DB, table string) *SQLRateLimitStore
NewSQLRateLimitStore creates a shared rate-limit store on the given table base name (an "_attempts" sibling table is derived from it). Panics if the table name contains unsafe characters.
func (*SQLRateLimitStore) Allow ¶ added in v0.21.0
func (s *SQLRateLimitStore) Allow(ctx context.Context, key string, cfg RateLimiterConfig) (bool, time.Duration, error)
Allow implements RateLimitStore with the same sliding-window + block semantics as the in-process limiter.
func (*SQLRateLimitStore) EnsureSchema ¶ added in v0.21.0
func (s *SQLRateLimitStore) EnsureSchema(ctx context.Context) error
EnsureSchema creates both tables if absent. Idempotent; invoked lazily by Allow, exported for hosts that migrate eagerly at boot.
type SQLServiceAccountStore ¶ added in v0.15.0
type SQLServiceAccountStore struct {
// contains filtered or unexported fields
}
SQLServiceAccountStore is the database-backed ServiceAccountStore.
Schema (table auth_service_accounts, created at construction):
id TEXT PRIMARY KEY name TEXT NOT NULL UNIQUE roles TEXT NOT NULL DEFAULT '[]' -- JSON array disabled <bool> NOT NULL DEFAULT false created_at <ts> NOT NULL
func NewSQLServiceAccountStore ¶ added in v0.15.0
func NewSQLServiceAccountStore(db *sql.DB, opts ...SQLServiceAccountStoreOption) (*SQLServiceAccountStore, error)
NewSQLServiceAccountStore creates the service-account table (IF NOT EXISTS) and returns the store.
func (*SQLServiceAccountStore) Create ¶ added in v0.15.0
func (s *SQLServiceAccountStore) Create(ctx context.Context, sa ServiceAccount) error
func (*SQLServiceAccountStore) Get ¶ added in v0.15.0
func (s *SQLServiceAccountStore) Get(ctx context.Context, id string) (*ServiceAccount, error)
func (*SQLServiceAccountStore) List ¶ added in v0.15.0
func (s *SQLServiceAccountStore) List(ctx context.Context) ([]ServiceAccount, error)
func (*SQLServiceAccountStore) SetDisabled ¶ added in v0.15.0
type SQLServiceAccountStoreOption ¶ added in v0.15.0
type SQLServiceAccountStoreOption func(*sqlServiceAccountStoreConfig)
SQLServiceAccountStoreOption tunes NewSQLServiceAccountStore.
func WithServiceAccountTable ¶ added in v0.15.0
func WithServiceAccountTable(name string) SQLServiceAccountStoreOption
WithServiceAccountTable overrides the service-account table name (default "auth_service_accounts").
type SecurityEvent ¶ added in v0.14.0
type SecurityEvent struct {
// Kind is the fixed taxonomy identifier (e.g. "login.succeeded",
// "2fa.enrolled"). Callers MUST use one of the documented kinds so
// downstream consumers can match on a closed vocabulary.
Kind string
// UserID is the resolved principal. Empty when unknown — e.g. a failed
// login for a nonexistent account, or a password-reset request for an
// unregistered email. The SQL sink substitutes "-" so the NOT NULL
// record_id column still accepts the row.
UserID string
// Email is the submitted address where relevant. It is the ONLY
// user-controlled string permitted in any event field; every other
// Meta value is a fixed-vocabulary token or a count. Useful for
// forensics; never a credential.
Email string
// Remote is the client IP (host part of r.RemoteAddr). XFF is NOT
// trusted — see the auth threat-model doc. Empty when the request
// carries no remote address.
Remote string
// Meta carries small, fixed-vocabulary details (a "reason" code, a
// session "count", a provider name). NEVER put secrets, tokens, codes,
// or any value derived from user input other than Email here.
Meta map[string]string
// At is stamped by the emitter when zero, so callers can omit it.
At time.Time
}
SecurityEvent is one security-relevant auth occurrence: a login, a 2FA lifecycle change, a password reset, an OAuth link, a magic-link issuance. It is the unit the audit trail records for non-CRUD auth activity — the CRUD hooks already cover user/session row writes, so this struct carries only the events those hooks can't see.
type ServiceAccount ¶ added in v0.15.0
type ServiceAccount struct {
ID string
Name string // unique, required
Roles []string
Disabled bool
CreatedAt time.Time
}
ServiceAccount is a non-human identity that authenticates ONLY via API tokens (there is deliberately no interactive login path). Its Roles feed access.Can / RequireRole through the User interface, exactly like a human user's roles.
func NewServiceAccount ¶ added in v0.15.0
func NewServiceAccount(name string, roles []string) ServiceAccount
NewServiceAccount builds a ServiceAccount with a fresh ID and CreatedAt, ready for ServiceAccountStore.Create. Service-account management is programmatic-only in v1 (no HTTP surface) — hosts call this then Create.
type ServiceAccountStore ¶ added in v0.15.0
type ServiceAccountStore interface {
Create(ctx context.Context, sa ServiceAccount) error
Get(ctx context.Context, id string) (*ServiceAccount, error) // (nil,nil) unknown
List(ctx context.Context) ([]ServiceAccount, error)
SetDisabled(ctx context.Context, id string, disabled bool) error
}
ServiceAccountStore persists service accounts.
type Session ¶
type Session struct {
Token string
UserID string
CreatedAt time.Time
ExpiresAt time.Time
TwoFactorVerified bool
PendingTwoFactor bool
}
Session represents an authenticated cookie-bound session. UserID is the stable identifier the application's User repo uses to look up a row; Token is the opaque cookie value the browser sends back.
TwoFactorVerified is set by the 2FA challenge handler after a successful TOTP / backup-code verification. The RequireTwoFA middleware uses it to gate access for users who have enrolled in 2FA.
PendingTwoFactor is set when the login flow detects an enrolled user and means the session is ONLY valid for /auth/2fa/challenge. Every other handler refuses it. The successful challenge clears Pending and sets Verified.
type SessionMiddlewareOption ¶
type SessionMiddlewareOption func(*sessionMiddlewareOpts)
SessionMiddlewareOption tunes SessionMiddleware. Today only logging is configurable; future options can land here without breaking the SessionMiddleware(mgr) call shape.
func WithSessionLogger ¶
func WithSessionLogger(l *slog.Logger) SessionMiddlewareOption
WithSessionLogger overrides the logger SessionMiddleware uses for observability (store errors, misconfiguration warnings). When unset, slog.Default() is used. Passing nil disables all logging.
type SessionPendingMarker ¶
type SessionPendingMarker interface {
MarkPendingTwoFactor(ctx context.Context, token string) error
}
SessionPendingMarker is the SessionStore extension that lets CorePlugin's login handler mark a freshly-minted session as awaiting a 2FA challenge. It is optional only for deployments without 2FA: if any registered TwoFactorChecker reports a user enrolled and the store does NOT implement this interface, login fails closed (the session is destroyed and the login rejected) rather than silently downgrading the account to password-only auth.
type SessionStore ¶
type SessionStore interface {
Create(ctx context.Context, userID string, ttl time.Duration) (*Session, error)
Get(ctx context.Context, token string) (*Session, error)
Delete(ctx context.Context, token string) error
Cleanup(ctx context.Context) (int, error) // returns count removed
}
SessionStore is the storage backend for cookie sessions. Production deployments swap in Redis / Postgres / etc. behind the same interface.
All methods take a Context for cancellation; in-memory implementations can ignore it.
type SessionTwoFAMarker ¶
type SessionTwoFAMarker interface {
MarkTwoFactorVerified(ctx context.Context, token string) error
}
SessionTwoFAMarker is the optional SessionStore extension that lets the 2FA challenge handler mark a session as having completed the second factor. The implementation MUST set TwoFactorVerified=true AND clear PendingTwoFactor — the two are inverses in the post-login state model. RequireTwoFA refuses access if a 2FA-enabled user's session is not marked. Stores that don't implement this method effectively cannot participate in 2FA enforcement (RequireTwoFA will fail-closed).
type SessionUserPurger ¶
SessionUserPurger is the optional SessionStore extension that revokes every session belonging to a user in one call. Credential-changing flows (password reset / change, email change) call it so a credential that was compromised before the change cannot retain access through an already-issued cookie. Both built-in stores implement it; a store that does not is treated as fail-open for this purpose and the caller logs that revocation was a no-op. Returns the number of sessions removed.
type TokenMiddlewareOption ¶ added in v0.15.0
type TokenMiddlewareOption func(*tokenMiddlewareOpts)
TokenMiddlewareOption tunes TokenMiddleware.
func WithTokenAudit ¶ added in v0.15.0
func WithTokenAudit(sink AuditSink) TokenMiddlewareOption
WithTokenAudit routes token auth events to sink. TokenMiddleware has no AuthManager, so it cannot use mgr.emitSecurity; this option is the seam. A nil sink (the default) disables auditing entirely and never panics.
func WithTokenLogger ¶ added in v0.15.0
func WithTokenLogger(l *slog.Logger) TokenMiddlewareOption
WithTokenLogger overrides the logger TokenMiddleware uses for store-error observability. When unset, slog.Default() is used; pass nil to silence.
func WithTokenPrefix ¶ added in v0.28.0
func WithTokenPrefix(prefix string) TokenMiddlewareOption
WithTokenPrefix sets the plaintext marker this middleware recognizes (default TokenPrefix, "gfsk_"). Pair it with the same TokenSpec.Prefix at issue time — hosts that brand their credentials must brand both sides or every authenticated request passes through unrecognized.
type TokenSpec ¶ added in v0.15.0
type TokenSpec struct {
Name string
OwnerKind string // "user" | "service"
OwnerID string
Scopes []string
TTL time.Duration // 0 = no expiry
// Prefix overrides the plaintext marker for this token (default
// TokenPrefix, "gfsk_"). Hosts brand their credentials — a leaked token's
// prefix then identifies WHICH product leaked it, and per-product secret
// scanners can grep for it. Must match tokenPrefixPattern (lowercase
// alnum, 2–16 chars, trailing underscore). Wire the SAME prefix into
// TokenMiddleware via WithTokenPrefix or authenticated requests will pass
// through unrecognized.
Prefix string
}
TokenSpec describes a token to mint.
type TokensPlugin ¶ added in v0.15.0
type TokensPlugin struct {
// contains filtered or unexported fields
}
TokensPlugin is the AuthPlugin + AuthPluginRoutes implementation that exposes self-service API-token management to logged-in users:
POST {base}/tokens — create a token for the caller (plaintext shown once)
GET {base}/tokens — list the caller's tokens (prefix only, no plaintext)
DELETE {base}/tokens/{id} — revoke one of the caller's tokens
Every endpoint resolves the owner from the authenticated session user — OwnerKind is forced to "user" and OwnerID to the current user's ID, so a caller can never mint for or revoke another user's token. Service-account management is programmatic-only in v1 (no HTTP surface).
func NewTokensPlugin ¶ added in v0.15.0
func NewTokensPlugin(tokens APITokenStore) *TokensPlugin
NewTokensPlugin constructs the plugin. The token store is supplied here; the manager arrives via Init (so emitSecurity is available after wiring).
func (*TokensPlugin) Init ¶ added in v0.15.0
func (p *TokensPlugin) Init(mgr *AuthManager) error
func (*TokensPlugin) Name ¶ added in v0.15.0
func (p *TokensPlugin) Name() string
func (*TokensPlugin) RegisterRoutes ¶ added in v0.15.0
func (p *TokensPlugin) RegisterRoutes(r *router.Router, basePath string)
func (*TokensPlugin) WithPrefix ¶ added in v0.28.0
func (p *TokensPlugin) WithPrefix(prefix string) *TokensPlugin
WithPrefix brands tokens this plugin mints (default TokenPrefix, "gfsk_") so a host's credentials are greppable as ITS credentials. Pair with TokenMiddleware's WithTokenPrefix so the branded tokens authenticate.
type TwoFAConfig ¶
type TwoFAConfig struct {
// Issuer is the name shown in authenticator apps. Defaults to "GoFastr".
Issuer string
// Period is the TOTP time-step period in seconds. Defaults to 30.
Period uint
// Digits is the number of digits in generated TOTP codes. Defaults to 6.
Digits uint
// Skew is the number of time-steps allowed before/after the current step.
// Defaults to 1 (allows ±1 period window).
Skew uint
// BackupCodeCount is how many backup codes to generate. Defaults to 10.
BackupCodeCount int
// Store is the persistence backend for 2FA state. If nil, an in-memory
// store is used (dev/test only).
Store TwoFAStore
// RateLimit, when non-nil, applies a per-IP rate limit to
// /2fa/challenge and /2fa/verify. Without this, an attacker who has
// stolen a session can brute-force the 6-digit TOTP (~333k expected
// attempts at skew=1).
RateLimit *RateLimiterConfig
}
TwoFAConfig holds optional settings for the TwoFAPlugin.
type TwoFAPlugin ¶
type TwoFAPlugin struct {
// contains filtered or unexported fields
}
TwoFAPlugin implements AuthPlugin and AuthPluginRoutes for TOTP-based two-factor authentication.
func NewTwoFAPlugin ¶
func NewTwoFAPlugin(config TwoFAConfig) *TwoFAPlugin
NewTwoFAPlugin creates a new 2FA plugin with the given (optional) config.
func (*TwoFAPlugin) HasTwoFactorEnabled ¶
HasTwoFactorEnabled implements TwoFactorChecker. Returns true when the user has 2FA enrolled and enabled. CorePlugin's loginHandler queries this to decide whether to mint a PendingTwoFactor session.
func (*TwoFAPlugin) Init ¶
func (p *TwoFAPlugin) Init(mgr *AuthManager) error
Init stores a reference to the AuthManager, selects the store, and self-migrates its schema when the store supports it.
Init fails closed when DevMode=false and no durable store is configured: in-memory 2FA state in production is worse than a scaling gap — a restart wipes enrollment, silently reverting every 2FA account to password-only auth. A security control that quietly stops applying is not a warning-grade condition, so the app refuses to boot unless the host acknowledges a deliberate single-node deployment via AuthConfig.AllowInMemoryStores.
func (*TwoFAPlugin) RegisterRoutes ¶
func (p *TwoFAPlugin) RegisterRoutes(r *router.Router, basePath string)
RegisterRoutes mounts the 2FA HTTP endpoints.
func (*TwoFAPlugin) RequireTwoFA ¶
func (p *TwoFAPlugin) RequireTwoFA() func(http.Handler) http.Handler
RequireTwoFA returns middleware that:
- Lets requests through if the user has not enrolled in 2FA.
- Lets requests through if the session has TwoFactorVerified=true.
- Returns 403 in all other cases (enrolled but not verified, or no session).
Install this on every route that requires step-up authentication. Note that it relies on the SessionStore implementing SessionTwoFAMarker — otherwise RequireTwoFA fails closed (always 403 for enrolled users).
type TwoFAState ¶
type TwoFAState struct {
Enabled bool // true after successful verify step
Secret string // base32 TOTP secret (plaintext, stored encrypted at rest in production)
BackupCodes []string // bcrypt-hashed backup codes
Verified bool // true once the user has proven they can generate a valid code
}
TwoFAState holds the per-user 2FA enrollment and verification status.
type TwoFAStore ¶
type TwoFAStore interface {
// GetTwoFA retrieves the 2FA state for a user. Returns nil if not enrolled.
GetTwoFA(ctx context.Context, userID string) (*TwoFAState, error)
// SetTwoFA persists the 2FA state for a user.
SetTwoFA(ctx context.Context, userID string, state *TwoFAState) error
// DeleteTwoFA removes the 2FA state for a user.
DeleteTwoFA(ctx context.Context, userID string) error
// ConsumeBackupCode checks if the given code matches any stored (hashed)
// backup code for the user. If it matches, that code is removed and true
// is returned. Otherwise returns false.
ConsumeBackupCode(ctx context.Context, userID string, code string) (bool, error)
}
TwoFAStore is the interface for persisting 2FA state per user.
type TwoFactorChecker ¶
type TwoFactorChecker interface {
HasTwoFactorEnabled(ctx context.Context, userID string) (bool, error)
}
TwoFactorChecker is the optional plugin extension that lets CorePlugin know whether to mint a PendingTwoFactor session at login time. The 2FA plugin implements this. Other plugins (WebAuthn, SMS) can implement it too to participate in the same gating.
type User ¶
User represents an authenticated user.
func GetCurrentUser ¶
GetCurrentUser extracts the authenticated User from the context. Returns nil if no user is present.
func SessionFrom ¶
SessionFrom returns the user loaded into ctx by SessionMiddleware (or RequireAuth). ok is false when the request is anonymous — no session, expired session, store outage, etc. Safe to call from any Render or RenderCtx path; never panics, never touches the DB.
Use this inside ContextComponent.RenderCtx for in-page gating:
func (s *Dashboard) RenderCtx(ctx context.Context) render.HTML {
if sess, ok := auth.SessionFrom(ctx); ok {
return UserSidebar(sess)
}
return AnonSidebar()
}
type UserFieldMap ¶
type UserFieldMap struct {
ID string // default: "id"
Email string // default: "email"
PasswordHash string // default: "password_hash"
Roles string // default: "roles"
// PasswordSet flags whether the user has a real password vs the
// placeholder hash used by OAuth / magic-link auto-create. Defaults
// to "password_set". The column is optional — if missing from the
// physical table, HasPassword returns ErrPasswordSetNotTracked and
// AccountsPlugin falls back to the conservative links-only rule.
PasswordSet string // default: "password_set"
}
UserFieldMap maps logical auth fields to actual DB column names. Defaults to snake_case standard names.
func DefaultUserFieldMap ¶
func DefaultUserFieldMap() UserFieldMap
DefaultUserFieldMap returns the standard field mapping.
type UserFields ¶
UserFields is the canonical user-entity field list returned by UserEntityFields. Its underlying type is []schema.Field so it remains assignable to entity.EntityConfig.Fields without any cast. The named type exists solely to attach the fluent With method.
func UserEntityFields ¶
func UserEntityFields() UserFields
UserEntityFields returns the standard field definitions for a user entity. Apps call this when defining their user entity:
app.Entity("users", entity.EntityConfig{
Fields: auth.UserEntityFields(),
})
For most apps, prefer UserEntityConfig() which also disables the dangerous-by-default auto-CRUD on user rows.
func (UserFields) With ¶
func (uf UserFields) With(extra ...schema.Field) UserFields
With returns a new UserFields containing the canonical fields plus any additional fields the host wants to attach (typically domain columns like username, disabled_at, or display_name).
The returned slice is independent — repeated calls don't mutate the receiver or the canonical list.
Example:
app.Entity("users", entity.EntityConfig{
Fields: auth.UserEntityFields().With(
schema.Field{Name: "username", Type: schema.String, Unique: true},
schema.Field{Name: "disabled_at", Type: schema.Timestamp},
),
})
type UserLister ¶ added in v0.18.0
type UserLister interface {
ListUsers(ctx context.Context, opts ListUsersOptions) (users []User, total int, err error)
}
UserLister is the optional UserStore extension that enumerates accounts in a stable order. Back-offices use it instead of raw SQL against the auth_users table.
Stores that do not implement it are unsupported: AuthManager.ListUsers returns ErrListUsersUnsupported rather than silently returning empty results, so a misconfigured deployment fails loudly instead of rendering a blank user list.
type UserStore ¶
type UserStore interface {
// FindByEmail looks up a user by email. Returns the user and their
// hashed password, or ErrUserNotFound if no row matches. Other errors
// (e.g. transport failures) are returned verbatim so handlers can
// distinguish "user doesn't exist" from "DB unavailable".
FindByEmail(ctx context.Context, email string) (User, string, error)
// FindByID looks up a user by their unique ID. Returns ErrUserNotFound
// if no row matches.
FindByID(ctx context.Context, id string) (User, error)
// CreateUser creates a new user account. Returns ErrEmailTaken if the
// email is already registered.
CreateUser(ctx context.Context, email, hashedPassword string, roles []string) (User, error)
// UpdateRoles replaces the roles for an existing user. Returns
// ErrUserNotFound if the user does not exist. The roles slice is
// OPERATOR input (from an admin screen), never request data — the
// same security posture as AuthConfig.DefaultRoles.
UpdateRoles(ctx context.Context, userID string, roles []string) error
}
UserStore is the interface auth needs to look up and manage users. This decouples auth from any specific storage backend.
Source Files
¶
- accounts.go
- agents.go
- apitoken.go
- apitoken_middleware.go
- apitoken_routes.go
- apitoken_sql.go
- audit.go
- auth.go
- bff.go
- core.go
- csrf.go
- devlog.go
- doc.go
- email_verification.go
- entity_oauth_links.go
- entity_store.go
- entity_twofa_store.go
- export.go
- form_decode.go
- json_limit.go
- jwt.go
- magiclink.go
- magiclink_sql.go
- manager.go
- mcp_gate.go
- middleware.go
- oauth2.go
- oauth_token_store.go
- oidc.go
- oidc_jwks.go
- owner.go
- password.go
- password_reset.go
- policy.go
- ratelimit.go
- session.go
- session_middleware.go
- sql_ratelimit_store.go
- token.go
- twofa.go
- update_roles.go
- users.go
- verify_private.go