Documentation
¶
Overview ¶
Package authhandlers provides shared types and utilities for the server's authentication handler cluster: login, WebAuthn, OIDC, admin user management, and security management (password change, API keys, passkeys).
Index ¶
- Constants
- Variables
- func Audit(r *http.Request, level slog.Level, event AuditEvent, success bool, user string, ...)
- func Base64URLEncode(data []byte) string
- func ClientIP(r *http.Request) string
- func GenerateCeremonyToken() (string, error)
- func SanitizeForwardedProto(next http.Handler) http.Handler
- func SetTrustedProxies(nets []*net.IPNet)
- func UnauthorizedResponse(w http.ResponseWriter, r *http.Request)
- func ValidateAndHashPassword(ctx context.Context, password, username string, passwordOnly, checkBreach bool, ...) (hash, userMsg string, err error)
- type APIKeyInfo
- type AuditEvent
- type AuthAdminStore
- type AuthConfig
- type CeremonyStore
- type Handler
- func (h *Handler) HandleAuthMe(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleChangePassword(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleCreateUser(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleDeletePasskey(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleDeleteUser(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleGenerateAPIKey(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleListAPIKeys(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleListPasskeys(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleListUsers(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleLogin(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleLogout(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleOIDCCallback(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleOIDCLink(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleOIDCRedirect(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleOIDCUnlink(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleRenamePasskey(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleRevokeAPIKey(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleSetupCreate(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleSetupStatus(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleWebAuthnLoginBegin(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleWebAuthnLoginFinish(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleWebAuthnRegisterBegin(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleWebAuthnRegisterFinish(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleWebAuthnSignalData(w http.ResponseWriter, r *http.Request)
- type OIDCStore
- type PasskeyInfo
- type PendingLink
- type SecurityStore
- type ShardedCeremonyMap
- type UserInfo
- type WebAuthnLoginBeginResponse
- type WebAuthnRegisterBeginResponse
- type WebAuthnSession
Constants ¶
const ( CookieNameHTTP = "sfx_session" CookieNameSecure = "__Host-" + CookieNameHTTP )
Session cookie names as they appear on the wire: the bare base name over plain HTTP (LAN, ip:port) and the __Host--prefixed Secure form over HTTPS. These are the two names SessionCookie's per-request posture alternates between; tests assert against them as subflux's observable cookie contract.
const ( // CeremonyTTL is the maximum age for pending WebAuthn sessions. CeremonyTTL = authwebauthn.CeremonyTimeout // MaxCeremonySessions caps the in-memory ceremony maps to prevent OOM // from unauthenticated flooding of /api/auth/login or /api/auth/webauthn/login/begin. MaxCeremonySessions = 10000 // CeremonyShards is the number of shards for ceremony maps. CeremonyShards = 16 // HeaderWebAuthnSession is the HTTP header carrying the WebAuthn session token. HeaderWebAuthnSession = "X-WebAuthn-Session" )
const AuditEventKind = "auth"
AuditEventKind is the fixed attribute value used to mark auth audit records. Filter on `event_kind="auth"` in log queries.
Variables ¶
var SessionCookie = auth.CookieConfig{ Posture: auth.PosturePerRequest, Name: CookieNameHTTP, TrustForwardedHeaders: true, }
SessionCookie is subflux's session-cookie configuration. Subflux serves both HTTP (LAN, ip:port) and HTTPS (behind a reverse proxy) from a single instance, so PosturePerRequest selects CookieNameSecure with the Secure flag over HTTPS and CookieNameHTTP without it over plain HTTP, per request. TrustForwardedHeaders honors X-Forwarded-Proto for the HTTPS decision; it is safe only because SanitizeForwardedProto strips that header from any request whose direct peer is not a configured trusted proxy.
Functions ¶
func Audit ¶
func Audit(r *http.Request, level slog.Level, event AuditEvent, success bool, user string, kvs ...any)
Audit emits a structured auth audit record at the specified slog level. Failures should emit at WARN; successes at INFO (so operator dashboards still see failures distinct from routine success traffic while auditors filter the full trail by event_kind).
`user` is the username when known; pass "" for failures on unknown usernames (still useful as the audit record will carry the IP and the failure reason).
Extra attributes are appended verbatim. Use `slog.String("reason", "invalid_password")` or `slog.String("key_id", id)` to keep the structured shape consistent.
func Base64URLEncode ¶
Base64URLEncode encodes bytes as base64url without padding.
func ClientIP ¶
ClientIP extracts the client IP address from the request using the current trusted-proxy set. With no trusted proxies configured it returns the unspoofable socket-peer host and ignores X-Forwarded-For; when the direct peer is a trusted proxy it resolves the real client from a trusted X-Forwarded-For chain (spoof-safe, walked right-to-left). It delegates to webhttp.ClientIP, the shared spoof-aware resolver — passing no trusted ranges is byte-identical to the previous unconfigured behavior.
func GenerateCeremonyToken ¶
GenerateCeremonyToken generates a random hex token for ephemeral ceremony state.
func SanitizeForwardedProto ¶ added in v0.1.148
SanitizeForwardedProto strips the X-Forwarded-Proto header from any request whose direct peer is not in the configured trusted-proxy set (the same hot-reloadable set ClientIP consults). A trusted reverse proxy always overwrites the header for the requests it forwards, so the only surviving spoofing path is a direct connection — and for those the header must not influence the per-request cookie posture (a forged "https" over plain LAN HTTP would flip the session cookie to the __Host-/Secure form the browser then refuses to store or send). With no trusted proxies configured the header is stripped from every request: scheme detection falls back to the unspoofable r.TLS, and HTTPS-terminating deployments declare their proxy via trusted_proxies exactly as they already must for client-IP resolution.
func SetTrustedProxies ¶ added in v0.1.92
SetTrustedProxies replaces the trusted reverse-proxy CIDR set used by ClientIP. Passing nil or an empty slice restores the trust-nothing default. It is safe for concurrent use and the new set takes effect on the next request, so a config hot-reload updates client-IP resolution without a restart.
func UnauthorizedResponse ¶ added in v0.1.148
func UnauthorizedResponse(w http.ResponseWriter, r *http.Request)
UnauthorizedResponse writes subflux's unauthorized response: a 302 to /login for browsers and subflux's typed 401 JSON envelope otherwise. Installed on the library Authenticator via auth.WithUnauthorizedResponse so RequireAuth speaks subflux's error vocabulary.
func ValidateAndHashPassword ¶
func ValidateAndHashPassword(ctx context.Context, password, username string, passwordOnly, checkBreach bool, client *http.Client) (hash, userMsg string, err error)
ValidateAndHashPassword validates password length and context (rejecting passwords that contain the username or app name), checks against breach databases (if checkBreach is true), and returns the Argon2id hash.
Types ¶
type APIKeyInfo ¶ added in v0.1.148
type APIKeyInfo struct {
CreatedAt time.Time `json:"created_at"`
KeyPrefix string `json:"key_prefix"`
KeySuffix string `json:"key_suffix"`
Label string `json:"label"`
ID int64 `json:"id"`
}
APIKeyInfo is one entry of the GET /api/auth/apikeys response.
type AuditEvent ¶
type AuditEvent string
AuditEvent enumerates the security-relevant events captured in the audit trail. Add new events here when introducing new auth flows.
const ( AuditLoginSuccess AuditEvent = "login.success" AuditLoginFailure AuditEvent = "login.failure" AuditLoginRateLimited AuditEvent = "login.rate_limited" AuditLogout AuditEvent = "logout" AuditPasswordChange AuditEvent = "password.change" AuditPasskeyAdd AuditEvent = "passkey.add" AuditPasskeyDelete AuditEvent = "passkey.delete" AuditPasskeyRename AuditEvent = "passkey.rename" AuditAPIKeyCreate AuditEvent = "apikey.create" AuditAPIKeyRevoke AuditEvent = "apikey.revoke" AuditOIDCCallback AuditEvent = "oidc.callback" )
AuditEvent constants enumerate the security-relevant events captured in the audit trail.
type AuthAdminStore ¶
type AuthAdminStore interface {
ListUsers(ctx context.Context) ([]auth.User, error)
CreateUser(ctx context.Context, user *auth.User) error
DeleteUser(ctx context.Context, id int64) error
}
AuthAdminStore is the narrow interface consumed by admin user management handlers.
type AuthConfig ¶
type AuthConfig interface {
BasicAuthEnabled() bool
CheckBreachedPasswords() bool
OIDCEnabled() bool
}
AuthConfig is the narrow interface consumed by auth handlers for configuration access. Mirrors the auth-related subset of api.ConfigProvider.
type CeremonyStore ¶
type CeremonyStore struct {
WebAuthn *ShardedCeremonyMap[*WebAuthnSession]
Link *ShardedCeremonyMap[*PendingLink]
}
CeremonyStore holds ephemeral ceremony state for auth flows. Owned by the Server struct to enable per-instance isolation in tests.
func NewCeremonyStore ¶
func NewCeremonyStore() *CeremonyStore
NewCeremonyStore creates a new ceremony store.
func (*CeremonyStore) Cleanup ¶
func (cs *CeremonyStore) Cleanup()
Cleanup removes expired pending WebAuthn sessions. Called periodically by the server.
func (*CeremonyStore) ConsumeWebAuthnSession ¶
func (cs *CeremonyStore) ConsumeWebAuthnSession(token string) *webauthn.SessionData
ConsumeWebAuthnSession atomically retrieves and removes a WebAuthn session. Returns nil if the session is missing or expired.
type Handler ¶
type Handler struct {
Store authstore.AuthStore
AdminDB AuthAdminStore
SecDB SecurityStore
OidcDB OIDCStore
RateLimiter ratelimit.Checker
// WebAuthnResolver resolves the current WebAuthn instance from the live
// snapshot per request (may resolve nil: RP ID unset or construction
// degraded). A direct field would freeze the boot-time instance across
// hot config edits; the resolver is the same seam OIDCResolver and
// Config already use.
WebAuthnResolver func() *webauthn.WebAuthn
OIDCResolver func() *authoidc.Provider
Ceremonies *CeremonyStore
Config func() AuthConfig // returns current config (hot-reloadable)
Configured func() bool // returns whether server has valid config
HTTPClient *http.Client // shared client for outbound requests (HIBP, etc.)
// contains filtered or unexported fields
}
Handler holds all dependencies for the auth handler family. Constructed by the server package and stored on the Server struct.
func (*Handler) HandleAuthMe ¶
func (h *Handler) HandleAuthMe(w http.ResponseWriter, r *http.Request)
HandleAuthMe handles GET /api/auth/me — returns profile information for the current user.
func (*Handler) HandleChangePassword ¶
func (h *Handler) HandleChangePassword(w http.ResponseWriter, r *http.Request)
HandleChangePassword handles PUT /api/auth/password — changes the current user's password after verifying the existing one, then invalidates all other sessions.
func (*Handler) HandleCreateUser ¶
func (h *Handler) HandleCreateUser(w http.ResponseWriter, r *http.Request)
HandleCreateUser handles POST /api/auth/users — creates a new user account.
func (*Handler) HandleDeletePasskey ¶
func (h *Handler) HandleDeletePasskey(w http.ResponseWriter, r *http.Request)
HandleDeletePasskey handles DELETE /api/auth/passkeys/{id} — removes a passkey. Refuses when deleting would leave the account with no authentication method.
func (*Handler) HandleDeleteUser ¶
func (h *Handler) HandleDeleteUser(w http.ResponseWriter, r *http.Request)
HandleDeleteUser handles DELETE /api/auth/users/{id} — deletes a user account. Refuses to delete the caller's own account or the last admin.
func (*Handler) HandleGenerateAPIKey ¶
func (h *Handler) HandleGenerateAPIKey(w http.ResponseWriter, r *http.Request)
HandleGenerateAPIKey handles POST /api/auth/apikeys — generates a new API key for the current user.
func (*Handler) HandleListAPIKeys ¶
func (h *Handler) HandleListAPIKeys(w http.ResponseWriter, r *http.Request)
HandleListAPIKeys handles GET /api/auth/apikeys — lists API keys for the current user.
func (*Handler) HandleListPasskeys ¶
func (h *Handler) HandleListPasskeys(w http.ResponseWriter, r *http.Request)
HandleListPasskeys handles GET /api/auth/passkeys — lists passkeys for the current user.
func (*Handler) HandleListUsers ¶
func (h *Handler) HandleListUsers(w http.ResponseWriter, r *http.Request)
HandleListUsers handles GET /api/auth/users — lists all user accounts.
func (*Handler) HandleLogin ¶
func (h *Handler) HandleLogin(w http.ResponseWriter, r *http.Request)
HandleLogin handles POST /api/auth/login — authenticates with username and password.
func (*Handler) HandleLogout ¶
func (h *Handler) HandleLogout(w http.ResponseWriter, r *http.Request)
HandleLogout handles POST /api/auth/logout — invalidates the current session cookie.
func (*Handler) HandleOIDCCallback ¶
func (h *Handler) HandleOIDCCallback(w http.ResponseWriter, r *http.Request)
HandleOIDCCallback handles GET /api/auth/oidc/callback — completes the OIDC authorization code exchange, resolves or JIT-provisions the user, and creates a session. Redirects to /login.html when a link ceremony is required.
func (*Handler) HandleOIDCLink ¶
func (h *Handler) HandleOIDCLink(w http.ResponseWriter, r *http.Request)
HandleOIDCLink completes link-on-login: the user proves ownership of the existing local account by password, and the pending OIDC identity is linked to it. The link token is single-use and TTL-bounded.
func (*Handler) HandleOIDCRedirect ¶
func (h *Handler) HandleOIDCRedirect(w http.ResponseWriter, r *http.Request)
HandleOIDCRedirect handles GET /api/auth/oidc — initiates the OIDC authorization code flow by generating state, nonce, and PKCE and redirecting to the provider.
func (*Handler) HandleOIDCUnlink ¶
func (h *Handler) HandleOIDCUnlink(w http.ResponseWriter, r *http.Request)
HandleOIDCUnlink removes the OIDC binding from the current user's account. It refuses if doing so would leave the account with no way to log in (no password and no passkey), mirroring the disable-password lockout guard.
func (*Handler) HandleRenamePasskey ¶
func (h *Handler) HandleRenamePasskey(w http.ResponseWriter, r *http.Request)
HandleRenamePasskey handles PUT /api/auth/passkeys/{id} — renames a passkey.
func (*Handler) HandleRevokeAPIKey ¶
func (h *Handler) HandleRevokeAPIKey(w http.ResponseWriter, r *http.Request)
HandleRevokeAPIKey handles DELETE /api/auth/apikeys/{id} — revokes an API key owned by the current user.
func (*Handler) HandleSetupCreate ¶
func (h *Handler) HandleSetupCreate(w http.ResponseWriter, r *http.Request)
HandleSetupCreate handles POST /api/auth/setup — creates the initial admin account. Only succeeds when no users exist; subsequent calls return 409.
func (*Handler) HandleSetupStatus ¶
func (h *Handler) HandleSetupStatus(w http.ResponseWriter, r *http.Request)
HandleSetupStatus handles GET /api/auth/setup — returns whether initial setup is required.
func (*Handler) HandleWebAuthnLoginBegin ¶
func (h *Handler) HandleWebAuthnLoginBegin(w http.ResponseWriter, r *http.Request)
HandleWebAuthnLoginBegin handles POST /api/auth/webauthn/login/begin — issues a WebAuthn assertion challenge. Supports both standard and conditional (passkey autofill) mediation modes.
func (*Handler) HandleWebAuthnLoginFinish ¶
func (h *Handler) HandleWebAuthnLoginFinish(w http.ResponseWriter, r *http.Request)
HandleWebAuthnLoginFinish handles POST /api/auth/webauthn/login/finish — verifies the assertion response, updates the credential sign count, and creates a session for the authenticated user.
func (*Handler) HandleWebAuthnRegisterBegin ¶
func (h *Handler) HandleWebAuthnRegisterBegin(w http.ResponseWriter, r *http.Request)
HandleWebAuthnRegisterBegin handles POST /api/auth/webauthn/register/begin — initiates passkey registration. Requires password verification before issuing the creation challenge to prevent unauthorized credential provisioning.
func (*Handler) HandleWebAuthnRegisterFinish ¶
func (h *Handler) HandleWebAuthnRegisterFinish(w http.ResponseWriter, r *http.Request)
HandleWebAuthnRegisterFinish handles POST /api/auth/webauthn/register/finish — completes passkey registration, stores the new credential, and emits an audit record.
func (*Handler) HandleWebAuthnSignalData ¶
func (h *Handler) HandleWebAuthnSignalData(w http.ResponseWriter, r *http.Request)
HandleWebAuthnSignalData handles GET /api/auth/webauthn/signal-data — returns the WebAuthn signal data needed by the browser for credential management.
type OIDCStore ¶
type OIDCStore interface {
CreateOIDCState(ctx context.Context, state, nonce, codeVerifier, redirectURI string) error
ConsumeOIDCState(ctx context.Context, state string) (nonce, codeVerifier, redirectURI string, err error)
GetUserByOIDCSub(ctx context.Context, issuer, sub string) (*auth.User, error)
GetUserByEmail(ctx context.Context, email string) (*auth.User, error)
GetUserByUsername(ctx context.Context, username string) (*auth.User, error)
CreateUser(ctx context.Context, user *auth.User) error
UpdateUser(ctx context.Context, user *auth.User) error
}
OIDCStore is the narrow interface consumed by OIDC authentication handlers.
type PasskeyInfo ¶ added in v0.1.148
type PasskeyInfo struct {
CreatedAt time.Time `json:"created_at"`
Name string `json:"name"`
Transport string `json:"transport,omitempty"`
ID int64 `json:"id"`
BackupEligible bool `json:"backup_eligible"`
}
PasskeyInfo is one entry of the GET /api/auth/passkeys response.
type PendingLink ¶
PendingLink holds state for an OIDC login that matched an existing local account by username but not by (issuer, sub). The user must prove ownership of the local account with its password before the OIDC identity is linked (link-on-login). Stored under a random token, single-use, TTL-bounded.
type SecurityStore ¶
type SecurityStore interface {
UpdateUser(ctx context.Context, user *auth.User) error
DeleteUserSessions(ctx context.Context, userID int64, exceptHash string) error
PasskeyCountForUser(ctx context.Context, userID int64) (int, error)
GetPasskeysByUserID(ctx context.Context, userID int64) ([]auth.PasskeyCredential, error)
CreatePasskey(ctx context.Context, cred *auth.PasskeyCredential) error
DeletePasskey(ctx context.Context, id, userID int64) error
RenamePasskey(ctx context.Context, id, userID int64, name string) error
CreateAPIKey(ctx context.Context, key *auth.Key) error
DeleteAPIKey(ctx context.Context, id, userID int64) error
ListAPIKeysByUserID(ctx context.Context, userID int64) ([]auth.Key, error)
}
SecurityStore is the narrow interface consumed by security management handlers.
type ShardedCeremonyMap ¶
type ShardedCeremonyMap[V any] struct { // contains filtered or unexported fields }
ShardedCeremonyMap is a sharded map for ephemeral ceremony state. Sharding reduces lock contention under concurrent auth requests.
func NewShardedCeremonyMap ¶
func NewShardedCeremonyMap[V any]() *ShardedCeremonyMap[V]
NewShardedCeremonyMap creates a new sharded ceremony map.
func (*ShardedCeremonyMap[V]) Cleanup ¶
func (sm *ShardedCeremonyMap[V]) Cleanup(isExpired func(V) bool)
Cleanup removes entries matching the isExpired predicate.
func (*ShardedCeremonyMap[V]) Load ¶
func (sm *ShardedCeremonyMap[V]) Load(key string) (V, bool)
Load retrieves a value from the sharded ceremony map by key without removing it.
func (*ShardedCeremonyMap[V]) LoadAndDelete ¶
func (sm *ShardedCeremonyMap[V]) LoadAndDelete(key string) (V, bool)
LoadAndDelete atomically retrieves and removes a value from the map.
func (*ShardedCeremonyMap[V]) Store ¶
func (sm *ShardedCeremonyMap[V]) Store(key string, val V) bool
Store adds a value to the map. Returns false if the session limit is reached.
type UserInfo ¶ added in v0.1.148
type UserInfo struct {
CreatedAt time.Time `json:"created_at"`
Username string `json:"username"`
Email string `json:"email"`
Role auth.Role `json:"role"`
ID int64 `json:"id"`
Enabled bool `json:"enabled"`
}
UserInfo is one entry of the GET /api/auth/users response.
type WebAuthnLoginBeginResponse ¶
type WebAuthnLoginBeginResponse struct {
PublicKey *protocol.CredentialAssertion `json:"publicKey"`
SessionToken string `json:"session_token"`
}
WebAuthnLoginBeginResponse wraps WebAuthn assertion options with a session token. Used for login ceremonies.
type WebAuthnRegisterBeginResponse ¶
type WebAuthnRegisterBeginResponse struct {
PublicKey *protocol.CredentialCreation `json:"publicKey"`
SessionToken string `json:"session_token"`
}
WebAuthnRegisterBeginResponse wraps WebAuthn creation options with a session token. Used for passkey registration ceremonies.
type WebAuthnSession ¶
type WebAuthnSession struct {
Data *webauthn.SessionData
CreatedAt time.Time
}
WebAuthnSession holds ephemeral WebAuthn ceremony data.