Documentation
¶
Overview ¶
Package authx is a self-contained authentication library for Go web apps: passwords, passkeys (WebAuthn), email magic-links, OIDC, social login, and 2FA behind one signed HttpOnly session cookie — the SPA never handles tokens. It runs a backend-for-frontend Authorization Code + PKCE flow and is opt-in: with no OIDC_ISSUER/OIDC_CLIENT_ID and no local methods enabled everything no-ops and the service behaves as before (open), so the demo and tests are unaffected.
Index ¶
- Constants
- Variables
- func GroupsFromRequest(r *http.Request) []string
- func KeyHasScope(info *APIKeyInfo, scope string) bool
- func NewHIBPBreachChecker() *hibpChecker
- func SessionFromRequest(r *http.Request) (sub, email string, ok bool)
- func TrustedProxies() []string
- type APIKeyInfo
- type AuthUser
- type Authenticator
- func (a *Authenticator) AccountChangeEmailRequest(c *reqCtx)
- func (a *Authenticator) AccountDelete(c *reqCtx)
- func (a *Authenticator) AccountInfo(c *reqCtx)
- func (a *Authenticator) AccountSetPassword(c *reqCtx)
- func (a *Authenticator) AuthConfig(c *reqCtx)
- func (a *Authenticator) CSRFHTTP(next http.Handler) http.Handler
- func (a *Authenticator) Callback(c *reqCtx)
- func (a *Authenticator) Close() error
- func (a *Authenticator) DirectoryEnabled() bool
- func (a *Authenticator) EmailChangeConfirm(c *reqCtx)
- func (a *Authenticator) EmailLogin(c *reqCtx)
- func (a *Authenticator) EmailOTPSend(c *reqCtx)
- func (a *Authenticator) EmailOTPVerify(c *reqCtx)
- func (a *Authenticator) EmailRequest(c *reqCtx)
- func (a *Authenticator) EmailVerify(c *reqCtx)
- func (a *Authenticator) Enabled() bool
- func (a *Authenticator) GateHTTP(next http.Handler) http.Handler
- func (a *Authenticator) Handler() http.Handler
- func (a *Authenticator) LocalEnabled() bool
- func (a *Authenticator) Login(c *reqCtx)
- func (a *Authenticator) Logout(c *reqCtx)
- func (a *Authenticator) Me(c *reqCtx)
- func (a *Authenticator) OAuthUnlink(c *reqCtx)
- func (a *Authenticator) OrgInviteAccept(c *reqCtx)
- func (a *Authenticator) OrgInviteCreate(c *reqCtx)
- func (a *Authenticator) OrgInviteList(c *reqCtx)
- func (a *Authenticator) OrgInviteRevoke(c *reqCtx)
- func (a *Authenticator) OrgList(c *reqCtx)
- func (a *Authenticator) OrgMemberList(c *reqCtx)
- func (a *Authenticator) OrgMemberRemove(c *reqCtx)
- func (a *Authenticator) OrgMemberSetRole(c *reqCtx)
- func (a *Authenticator) OrgSwitch(c *reqCtx)
- func (a *Authenticator) OrgsEnabled() bool
- func (a *Authenticator) PasskeyRemove(c *reqCtx)
- func (a *Authenticator) PasskeyRename(c *reqCtx)
- func (a *Authenticator) PasswordLogin(c *reqCtx)
- func (a *Authenticator) PasswordResetConfirm(c *reqCtx)
- func (a *Authenticator) PasswordResetRequest(c *reqCtx)
- func (a *Authenticator) PasswordSignup(c *reqCtx)
- func (a *Authenticator) ReAuth(c *reqCtx)
- func (a *Authenticator) RequireGroupsHTTP(groups ...string) func(http.Handler) http.Handler
- func (a *Authenticator) RequireOrgGroupsHTTP(groups ...string) func(http.Handler) http.Handler
- func (a *Authenticator) RequireOrgHTTP(roles ...string) func(http.Handler) http.Handler
- func (a *Authenticator) RequireStepUpHTTP(maxAge time.Duration) func(http.Handler) http.Handler
- func (a *Authenticator) SessionList(c *reqCtx)
- func (a *Authenticator) SessionRevoke(c *reqCtx)
- func (a *Authenticator) SessionRevokeOthers(c *reqCtx)
- func (a *Authenticator) SessionsEnabled() bool
- func (a *Authenticator) SetAuthorizer(z Authorizer)
- func (a *Authenticator) SetBreachChecker(b BreachChecker)
- func (a *Authenticator) SetCredentialStore(cs CredentialStore)
- func (a *Authenticator) SetDirectoryStore(d DirectoryStore)
- func (a *Authenticator) SetLocalEnabled(on bool)
- func (a *Authenticator) SetMailer(m Mailer)
- func (a *Authenticator) SetOrgStore(o OrgStore)
- func (a *Authenticator) SetRateLimiters(perIP, perAccount RateLimiter)
- func (a *Authenticator) SetSessionStore(s SessionStore)
- func (a *Authenticator) SetTwoFactorStore(s TwoFactorStore)
- func (a *Authenticator) SocialCallback(c *reqCtx)
- func (a *Authenticator) SocialLogin(c *reqCtx)
- func (a *Authenticator) StepUpFresh(r *http.Request, maxAge time.Duration) bool
- func (a *Authenticator) StopImpersonating(c *reqCtx)
- func (a *Authenticator) TOTPBegin(c *reqCtx)
- func (a *Authenticator) TOTPConfirm(c *reqCtx)
- func (a *Authenticator) TOTPDisable(c *reqCtx)
- func (a *Authenticator) TwoFactorEnabled() bool
- func (a *Authenticator) TwoFactorPending(c *reqCtx)
- func (a *Authenticator) TwoFactorVerify(c *reqCtx)
- func (a *Authenticator) TwoFactorWebauthnBegin(c *reqCtx)
- func (a *Authenticator) TwoFactorWebauthnFinish(c *reqCtx)
- func (a *Authenticator) ValidateAPIKey(raw string) (*APIKeyInfo, bool)
- func (a *Authenticator) ValidateAPIKeyScope(raw, scope string) bool
- func (a *Authenticator) ValidateOrgAPIKeyScope(raw, scope, orgID string) bool
- func (a *Authenticator) WebauthnLoginBegin(c *reqCtx)
- func (a *Authenticator) WebauthnLoginFinish(c *reqCtx)
- func (a *Authenticator) WebauthnRegisterBegin(c *reqCtx)
- func (a *Authenticator) WebauthnRegisterFinish(c *reqCtx)
- type Authorizer
- type BreachChecker
- type Config
- type CredentialStore
- type DirectoryStore
- type Group
- type H
- type Identity
- type Mailer
- type Org
- type OrgDirectoryStore
- type OrgInvite
- type OrgMember
- type OrgStore
- type Passkey
- type RateLimiter
- type SessionClaims
- type SessionRecord
- type SessionStore
- type SocialOIDCProvider
- type TOTPInfo
- type TokenClaim
- type TwoFactorStore
- type UserOrg
Constants ¶
const ( OrgRoleOwner = "owner" OrgRoleAdmin = "admin" OrgRoleMember = "member" )
Reserved org roles. Owner and admin gate the self-service org endpoints (member management, invites); stores accept any validOrgRole token, so apps may define additional roles and gate on them with RequireOrgHTTP.
Variables ¶
var ( ErrNoUser = errors.New("authx: no such user") ErrNoCredential = errors.New("authx: no credential") ErrTokenInvalid = errors.New("authx: token invalid or expired") // ErrEmailConflict is returned by a reference Authorizer's identity upsert when a login's // email already belongs to a different, unproven (unverified, non-bootstrap) account row. // Refusing rather than merging is what prevents account takeover; surface it as a 409. ErrEmailConflict = errors.New("authx: an account already exists for this email") )
Sentinels returned by a CredentialStore so the auth package can branch without knowing the storage layer (GORM lives in the tenant package).
var ( // ErrNoOrg is returned by OrgStore lookups when an organization doesn't exist. ErrNoOrg = errors.New("authx: no such organization") // ErrNotOrgMember is returned by OrgRole when the user has no membership in the org. ErrNotOrgMember = errors.New("authx: not a member of this organization") )
Sentinels returned by an OrgStore.
var ErrAccessDenied = errors.New("access denied")
ErrAccessDenied refuses a login from an Authorizer.
var ErrNoGroup = errors.New("authx: no such group")
ErrNoGroup is returned by DirectoryStore lookups when a group doesn't exist.
var ErrOrgOwnerDeprovision = errors.New("authx: cannot deprovision an organization owner via SCIM — manage owners in-app")
ErrOrgOwnerDeprovision is returned by an org-scoped directory when a customer IdP tries to deprovision an org OWNER via SCIM. Exported so the SCIM server can render it as a 403 policy refusal instead of a 500 (a 5xx an IdP would retry forever) — owners are managed in-app.
Functions ¶
func GroupsFromRequest ¶
GroupsFromRequest returns the groups of the session attached by GateHTTP.
func KeyHasScope ¶
func KeyHasScope(info *APIKeyInfo, scope string) bool
KeyHasScope reports whether an API key may use a given scope. Least privilege: an EMPTY Scopes list grants NOTHING. "*" grants everything; otherwise the scope must be listed explicitly.
func NewHIBPBreachChecker ¶ added in v0.5.0
func NewHIBPBreachChecker() *hibpChecker
NewHIBPBreachChecker returns a fail-open HIBP checker (a HIBP outage degrades to the embedded list rather than blocking signups). It sends the Add-Padding header to blunt response-size analysis and times out quickly so a slow upstream can't stall the signup/reset path.
func SessionFromRequest ¶
SessionFromRequest returns the authenticated subject + email from a request gated by GateHTTP.
func TrustedProxies ¶
func TrustedProxies() []string
TrustedProxies returns the proxy IPs/CIDRs to trust when extracting the client IP from X-Forwarded-For, read from TRUSTED_PROXIES (comma-separated) or a private-range default that matches the usual reverse-proxy-on-a-private-network deployment. Without it XFF is honored from any peer, letting a client spoof it to defeat per-IP rate limits.
Types ¶
type APIKeyInfo ¶
type APIKeyInfo struct {
ID string `json:"id"` // opaque; see AuthUser.ID
Name string `json:"name"`
Prefix string `json:"prefix"` // leading chars of the key, for identification in lists
Groups []string `json:"groups"`
Scopes []string `json:"scopes,omitempty"` // deny-by-default; e.g. ["scim"], ["admin"], ["*"]
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
// OrgID binds the key to one organization ("" = a global key, the default). An org-bound key
// is refused by every GLOBAL surface — adminGuard, ValidateAPIKeyScope, group merging — and
// only satisfies ValidateOrgAPIKeyScope for its own org (e.g. that org's SCIM mount).
OrgID string `json:"orgId,omitempty"`
}
APIKeyInfo is the non-secret metadata of an API key. The raw key is shown ONCE at creation; only its sha256 is stored. Calls made with the key act with the key's Groups for access control, and are limited to the key's Scopes (e.g. "admin", "scim"). Scopes are DENY-BY-DEFAULT: an empty list grants nothing; grant ["*"] for an unrestricted ("root") key.
type AuthUser ¶
type AuthUser struct {
ID string
Sub string
Email string
Name string
EmailVerified bool
Disabled bool
// Ban is a time-boxed login block with a reason, distinct from Disabled (an operator off-switch).
// Banned + zero BannedUntil = permanent; Banned + a future BannedUntil = until that instant.
Banned bool
BannedUntil time.Time
BanReason string
// Timestamps for SCIM meta.created / meta.lastModified. Best-effort — a store that doesn't
// track them leaves them zero and SCIM omits the field.
CreatedAt time.Time
UpdatedAt time.Time
}
AuthUser is the auth package's storage-agnostic view of a control-plane user.
ID is an OPAQUE string: the auth package never parses it or does arithmetic on it, it only passes it back to the store. A store keyed by uuid/ULID/KSUID returns those verbatim; the reference stores keep numeric PKs and convert to/from decimal strings at their boundary (no DB migration). Empty ("") is the reserved "no user" sentinel — a store must never mint it as a real ID.
type Authenticator ¶
type Authenticator struct {
// contains filtered or unexported fields
}
Authenticator holds the OIDC client wiring. When auth is disabled it is a valid no-op value (Enabled() == false) so callers never special-case nil.
func New ¶
func New(ctx context.Context, cfg Config) (*Authenticator, error)
New builds the Authenticator. If auth is not configured it returns a disabled (no-op) instance. If it IS configured it performs OIDC discovery against the issuer and fails on error — we'd rather refuse to boot than run unprotected when auth was intended (fail closed).
func (*Authenticator) AccountChangeEmailRequest ¶ added in v0.5.0
func (a *Authenticator) AccountChangeEmailRequest(c *reqCtx)
AccountChangeEmailRequest (POST /auth/api/account/email) starts a verified email change after a fresh step-up: it emails a confirmation link to the NEW address and never writes the new email until that link is redeemed (preserving the verified-email invariant), and notifies the OLD address.
func (*Authenticator) AccountDelete ¶ added in v0.5.0
func (a *Authenticator) AccountDelete(c *reqCtx)
AccountDelete (DELETE /auth/api/account) hard-deletes the signed-in user's account after a fresh step-up. Irreversible: it cascades credentials/passkeys/tokens/OAuth/2FA, anonymizes the audit trail, revokes all sessions, and clears the caller's cookies. Data your app keyed on the user's Sub is your responsibility to remove — that boundary ends at this library's tables.
func (*Authenticator) AccountInfo ¶
func (a *Authenticator) AccountInfo(c *reqCtx)
AccountInfo (GET /auth/api/account) returns the signed-in user's identity + sign-in methods.
func (*Authenticator) AccountSetPassword ¶
func (a *Authenticator) AccountSetPassword(c *reqCtx)
AccountSetPassword (POST /auth/api/account/password) sets or changes the signed-in user's password. If one already exists, the current password is required.
func (*Authenticator) AuthConfig ¶
func (a *Authenticator) AuthConfig(c *reqCtx)
AuthConfig (GET /auth/config) is public and tells the SPA which sign-in methods are available, so the login UI renders only what's wired this phase.
func (*Authenticator) CSRFHTTP ¶
func (a *Authenticator) CSRFHTTP(next http.Handler) http.Handler
CSRFHTTP is net/http middleware that enforces the double-submit CSRF token on state-changing /api and /auth requests, skipping the unauthenticated entry points and social callbacks.
func (*Authenticator) Callback ¶
func (a *Authenticator) Callback(c *reqCtx)
Callback completes the flow: validate state, exchange the code, verify the ID token, enforce group access, then set the session cookie.
func (*Authenticator) Close ¶ added in v0.4.0
func (a *Authenticator) Close() error
Close stops the background rate-limiter GC goroutine started for the built-in in-memory limiters. Optional: call it when discarding an Authenticator in a long-lived process (e.g. tests spinning up many instances) to avoid leaking the ticker goroutine. Safe to call once; a no-op if no built-in limiters were installed.
func (*Authenticator) DirectoryEnabled ¶
func (a *Authenticator) DirectoryEnabled() bool
DirectoryEnabled reports whether the directory features are wired.
func (*Authenticator) EmailChangeConfirm ¶ added in v0.5.0
func (a *Authenticator) EmailChangeConfirm(c *reqCtx)
EmailChangeConfirm (GET /auth/email/change?token=) redeems the new-address confirmation token and rebinds the email. Redeeming proves control of the new address, so it lands verified.
func (*Authenticator) EmailLogin ¶
func (a *Authenticator) EmailLogin(c *reqCtx)
EmailLogin (GET /auth/email/login?token=) redeems a magic-login token and signs the user in. Redeeming the emailed link proves email control, so it also confirms the address.
func (*Authenticator) EmailOTPSend ¶ added in v0.5.0
func (a *Authenticator) EmailOTPSend(c *reqCtx)
EmailOTPSend (POST /auth/email-otp/send) emails a 6-digit sign-in code. Always 200 (anti-enumeration); the lookup + send happen off the request path so timing doesn't reveal whether the account exists.
func (*Authenticator) EmailOTPVerify ¶ added in v0.5.0
func (a *Authenticator) EmailOTPVerify(c *reqCtx)
EmailOTPVerify (POST /auth/email-otp/verify) checks a code and, on success, signs the user in through the shared completeLogin funnel. The store enforces the per-code attempt cap; we add the per-IP guard.
func (*Authenticator) EmailRequest ¶
func (a *Authenticator) EmailRequest(c *reqCtx)
EmailRequest (POST /auth/email/request) emails a one-time sign-in (magic) link. Always 200 (anti-enumeration).
func (*Authenticator) EmailVerify ¶
func (a *Authenticator) EmailVerify(c *reqCtx)
EmailVerify (GET /auth/email/verify?token=) redeems a verify-email token, marks the email verified, and signs the user in.
func (*Authenticator) Enabled ¶
func (a *Authenticator) Enabled() bool
Enabled reports whether auth is enforced.
func (*Authenticator) GateHTTP ¶
func (a *Authenticator) GateHTTP(next http.Handler) http.Handler
GateHTTP is net/http middleware that enforces a valid session: public paths pass; missing/invalid sessions get a 401 JSON for /api and /ws, else a redirect to the branded landing. A valid API-key Bearer is also admitted (for RequireGroupsHTTP downstream). On success it stashes the session in the request context (see SessionFromRequest) and ensures a CSRF cookie. No-op when auth isn't enforced.
func (*Authenticator) Handler ¶
func (a *Authenticator) Handler() http.Handler
Handler returns the /auth/* endpoints as a standard http.Handler (a net/http ServeMux wrapped in CSRF enforcement). Mount it in any router — no framework dependency:
mux.Handle("/auth/", authn.Handler()) // net/http, chi, echo, ...
func (*Authenticator) LocalEnabled ¶
func (a *Authenticator) LocalEnabled() bool
LocalEnabled reports whether in-app auth methods are active (flag on + a credential store).
func (*Authenticator) Login ¶
func (a *Authenticator) Login(c *reqCtx)
Login starts the Authorization Code + PKCE flow: stash state/nonce/verifier/next in a short-lived signed cookie and redirect to the IdP.
func (*Authenticator) Logout ¶
func (a *Authenticator) Logout(c *reqCtx)
Logout clears the session and, if the IdP advertises one, redirects to its RP-initiated logout endpoint.
func (*Authenticator) Me ¶
func (a *Authenticator) Me(c *reqCtx)
Me reports auth status + the current identity for the SPA. Always 200. authEnabled follows enforcing() — true when ANY auth method gates requests (OIDC OR in-app local auth) — so a password/passkey-only deployment still reports signed-in state.
func (*Authenticator) OAuthUnlink ¶ added in v0.5.0
func (a *Authenticator) OAuthUnlink(c *reqCtx)
OAuthUnlink (DELETE /auth/api/identities/{provider}) removes a linked social/OIDC identity, refusing to strip the user's last sign-in method. Not step-up gated (parity with PasskeyRemove, and so an OAuth-only user with two providers can still unlink one).
func (*Authenticator) OrgInviteAccept ¶ added in v0.6.0
func (a *Authenticator) OrgInviteAccept(c *reqCtx)
OrgInviteAccept (GET /auth/org/invite/accept?token=) redeems an invite from the emailed link. The org and role come from the stored RECORD — nothing in the URL can be tampered with. The signed-in user's email must MATCH the invited address — the token proves control of that mailbox, so acceptance also marks the email verified (same rule as the magic link). Unauthenticated clicks bounce to the login page with the accept URL as next, so the invitee can sign in or register first. These are browser link-clicks, so failures redirect with a message rather than returning JSON (mirrors redeemAndLogin).
func (*Authenticator) OrgInviteCreate ¶ added in v0.6.0
func (a *Authenticator) OrgInviteCreate(c *reqCtx)
OrgInviteCreate (POST /auth/org/invites, body {"email","role"}) emails a single-use invite to join the ACTIVE org, stored as a first-class record (list with GET, revoke with DELETE /{id}); re-inviting an address replaces its pending invite. Owner/admin only; only an owner may invite an owner. The link is ONLY ever emailed — possession of the token is the invitee's proof of mailbox control, so handing it to the inviter would let them mint memberships for addresses they don't own.
func (*Authenticator) OrgInviteList ¶ added in v0.7.0
func (a *Authenticator) OrgInviteList(c *reqCtx)
OrgInviteList (GET /auth/org/invites) lists the ACTIVE org's PENDING invitations (owner/admin) — the management surface a bare token could never offer.
func (*Authenticator) OrgInviteRevoke ¶ added in v0.7.0
func (a *Authenticator) OrgInviteRevoke(c *reqCtx)
OrgInviteRevoke (DELETE /auth/org/invites/{id}) cancels a pending invitation before it is redeemed (owner/admin). Idempotent — revoking an already-gone invite is not an error.
func (*Authenticator) OrgList ¶ added in v0.6.0
func (a *Authenticator) OrgList(c *reqCtx)
OrgList (GET /auth/orgs) lists the signed-in user's org memberships plus the active org, for the app's org picker.
func (*Authenticator) OrgMemberList ¶ added in v0.6.0
func (a *Authenticator) OrgMemberList(c *reqCtx)
OrgMemberList (GET /auth/org/members) lists the ACTIVE org's members to any live member.
func (*Authenticator) OrgMemberRemove ¶ added in v0.6.0
func (a *Authenticator) OrgMemberRemove(c *reqCtx)
OrgMemberRemove (DELETE /auth/org/members/{userId}) removes a member from the ACTIVE org. Owner/admin only; removing an owner requires an owner; the last owner can't be removed. A member may remove THEMSELF (leave) under the same guards — their cookie is re-minted with no active org.
func (*Authenticator) OrgMemberSetRole ¶ added in v0.6.0
func (a *Authenticator) OrgMemberSetRole(c *reqCtx)
OrgMemberSetRole (POST /auth/org/members/{userId}, body {"role": "..."}) changes a member's role in the ACTIVE org. Owner/admin only; anything touching the owner role (granting it, or changing an existing owner's role) requires an owner, and the last owner can't be demoted.
func (*Authenticator) OrgSwitch ¶ added in v0.6.0
func (a *Authenticator) OrgSwitch(c *reqCtx)
OrgSwitch (POST /auth/org/switch) changes the session's ACTIVE org after verifying LIVE membership, re-minting the cookie with its remaining lifetime. Body: {"slug": "<slug>"} resolves by slug ONLY; {"org": "<id or slug>"} resolves as an ID first, slug fallback — pass the explicit slug field when a slug could collide with another org's opaque ID (e.g. an all-digit slug against the reference stores' decimal IDs). An empty body clears the active org. Membership and existence failures are the same 403, so the endpoint is not an org-ID existence oracle.
func (*Authenticator) OrgsEnabled ¶ added in v0.6.0
func (a *Authenticator) OrgsEnabled() bool
OrgsEnabled reports whether the org layer is active: an OrgStore AND a CredentialStore are wired (memberships reference credential-store user IDs, so orgs cannot operate without one).
func (*Authenticator) PasskeyRemove ¶
func (a *Authenticator) PasskeyRemove(c *reqCtx)
PasskeyRemove (DELETE /auth/api/passkeys/:id) deletes one of the user's passkeys, refusing to remove their last remaining sign-in method.
func (*Authenticator) PasskeyRename ¶ added in v0.5.0
func (a *Authenticator) PasskeyRename(c *reqCtx)
PasskeyRename (POST /auth/api/passkeys/{id}) relabels one of the user's passkeys.
func (*Authenticator) PasswordLogin ¶
func (a *Authenticator) PasswordLogin(c *reqCtx)
PasswordLogin (POST /auth/password/login) verifies email+password and starts a session.
func (*Authenticator) PasswordResetConfirm ¶
func (a *Authenticator) PasswordResetConfirm(c *reqCtx)
PasswordResetConfirm (POST /auth/password/reset/confirm) redeems a reset token and sets a new password. The token proves email control, so we also mark the email verified.
func (*Authenticator) PasswordResetRequest ¶
func (a *Authenticator) PasswordResetRequest(c *reqCtx)
PasswordResetRequest (POST /auth/password/reset/request) emails a reset link. Always 200.
func (*Authenticator) PasswordSignup ¶
func (a *Authenticator) PasswordSignup(c *reqCtx)
PasswordSignup (POST /auth/password/signup) creates a local, unverified account and emails a verification link. Always responds 200 with a generic message (anti-enumeration); it never overwrites the password of an existing VERIFIED account.
func (*Authenticator) ReAuth ¶ added in v0.2.0
func (a *Authenticator) ReAuth(c *reqCtx)
ReAuth (POST /auth/reauth, session-gated) re-verifies the signed-in user with a password OR a 2FA code, and on success sets the step-up cookie.
func (*Authenticator) RequireGroupsHTTP ¶
RequireGroupsHTTP is net/http middleware permitting only principals (a session user gated by GateHTTP, OR a valid API key) in at least one of the named groups. Empty names = any AUTHENTICATED principal (a session or a valid key) — NOT anonymous access; an unauthenticated request is refused.
func (*Authenticator) RequireOrgGroupsHTTP ¶ added in v0.7.0
RequireOrgGroupsHTTP is net/http middleware permitting only a session user (gated by GateHTTP) who is a LIVE member of their ACTIVE org and belongs to at least one of the named ORG-SCOPED groups there. Like RequireOrgHTTP, everything is re-verified against the stores per request — org groups never ride in the cookie. Empty names = any live org member. API-key principals are refused (org keys authorize surfaces, not group gates).
func (*Authenticator) RequireOrgHTTP ¶ added in v0.6.0
RequireOrgHTTP is net/http middleware permitting only a session user (gated by GateHTTP) whose LIVE role in their ACTIVE org is one of the named roles. Empty roles = any current member. Membership is re-verified against the OrgStore per request (one indexed lookup) — see liveOrgRole. API-key principals carry no org, so they are refused; org-scoped keys are a later phase. NOTE: this gates "who is acting in which org" — row-level isolation of the app's own data remains the app's responsibility.
func (*Authenticator) RequireStepUpHTTP ¶ added in v0.2.0
RequireStepUpHTTP wraps a handler so it requires a re-authentication within maxAge. On a stale/absent step-up it responds 403 {"error":"reauth_required"} — the client prompts for POST /auth/reauth, then retries.
func (*Authenticator) SessionList ¶ added in v0.5.0
func (a *Authenticator) SessionList(c *reqCtx)
SessionList (GET /auth/api/sessions) lists the signed-in user's active sessions, flagging the one making this request. Requires a session store.
func (*Authenticator) SessionRevoke ¶ added in v0.5.0
func (a *Authenticator) SessionRevoke(c *reqCtx)
SessionRevoke (DELETE /auth/api/sessions/{sid}) revokes one of the user's own sessions. It verifies the SID belongs to the caller before revoking, so a user can't revoke another account's session.
func (*Authenticator) SessionRevokeOthers ¶ added in v0.5.0
func (a *Authenticator) SessionRevokeOthers(c *reqCtx)
SessionRevokeOthers (POST /auth/api/sessions/revoke-others) revokes every session except the current one — "sign out my other devices".
func (*Authenticator) SessionsEnabled ¶ added in v0.5.0
func (a *Authenticator) SessionsEnabled() bool
SessionsEnabled reports whether server-side session tracking/revocation is wired.
func (*Authenticator) SetAuthorizer ¶
func (a *Authenticator) SetAuthorizer(z Authorizer)
SetAuthorizer installs an optional post-login authorization hook.
func (*Authenticator) SetBreachChecker ¶ added in v0.5.0
func (a *Authenticator) SetBreachChecker(b BreachChecker)
SetBreachChecker installs an optional password-breach checker (e.g. the HIBP k-anonymity checker). Pass nil to disable. Also enabled at boot via HIBP_BREACH_CHECK=true (see NewHIBPBreachChecker).
func (*Authenticator) SetCredentialStore ¶
func (a *Authenticator) SetCredentialStore(cs CredentialStore)
SetCredentialStore installs the in-app credential persistence (enables local methods).
func (*Authenticator) SetDirectoryStore ¶
func (a *Authenticator) SetDirectoryStore(d DirectoryStore)
SetDirectoryStore enables groups, API keys, per-group access control, and the admin REST API.
func (*Authenticator) SetLocalEnabled ¶
func (a *Authenticator) SetLocalEnabled(on bool)
SetLocalEnabled toggles whether the in-app auth methods (password / passkey / email magic-link + OTP) are wired (mirrors the AUTH_LOCAL env flag). OIDC and social login are independent of this flag — each social provider activates on its own credentials, and OIDC on its issuer.
func (*Authenticator) SetMailer ¶
func (a *Authenticator) SetMailer(m Mailer)
SetMailer installs the email sender for magic-link/verify/reset mails.
func (*Authenticator) SetOrgStore ¶ added in v0.6.0
func (a *Authenticator) SetOrgStore(o OrgStore)
SetOrgStore installs the optional organization persistence, enabling the org layer (active-org session claim, /auth/org/* + /auth/orgs endpoints, invites, RequireOrgHTTP, admin org verbs). Org features also need a CredentialStore — memberships are keyed by its user IDs.
func (*Authenticator) SetRateLimiters ¶
func (a *Authenticator) SetRateLimiters(perIP, perAccount RateLimiter)
SetRateLimiters overrides the per-IP and per-account limiters with custom (e.g. Redis-backed) backends. Call it during setup, BEFORE auth is enabled — once set, the built-in in-memory defaults (and their GC ticker) are not installed. Both arguments are required.
func (*Authenticator) SetSessionStore ¶ added in v0.5.0
func (a *Authenticator) SetSessionStore(s SessionStore)
SetSessionStore enables optional server-side session revocation + device listing.
func (*Authenticator) SetTwoFactorStore ¶ added in v0.2.0
func (a *Authenticator) SetTwoFactorStore(s TwoFactorStore)
SetTwoFactorStore enables optional TOTP two-factor auth + recovery codes. It also installs the default rate limiters if none exist yet: the 2FA verify / reauth endpoints throttle a brute-forceable 6-digit code space, and those guards would silently no-op in an OIDC/social-only deployment that never called SetLocalEnabled. No-op if the consumer already supplied limiters via SetRateLimiters.
func (*Authenticator) SocialCallback ¶
func (a *Authenticator) SocialCallback(c *reqCtx)
SocialCallback (GET /auth/social/:provider/callback) verifies state, exchanges the code, reads the provider-verified identity, links/creates the local user, and signs them in.
func (*Authenticator) SocialLogin ¶
func (a *Authenticator) SocialLogin(c *reqCtx)
SocialLogin (GET /auth/social/:provider/login) starts the provider's Authorization Code + PKCE flow, stashing state/nonce/verifier/next in the signed flow cookie (same machinery as OIDC).
func (*Authenticator) StepUpFresh ¶ added in v0.2.0
StepUpFresh reports whether the request carries a valid step-up proof for the CURRENT session user, issued within maxAge. (Bound to the session subject so a step-up from one account can't elevate another.)
func (*Authenticator) StopImpersonating ¶ added in v0.5.0
func (a *Authenticator) StopImpersonating(c *reqCtx)
StopImpersonating (POST /auth/api/stop-impersonating) ends an impersonation session. It revokes the impersonation session and clears the cookies; the admin then signs back in with their own account (clean, and it avoids resurrecting a dropped OIDC id_token from the original admin session).
func (*Authenticator) TOTPBegin ¶ added in v0.2.0
func (a *Authenticator) TOTPBegin(c *reqCtx)
TOTPBegin (POST /auth/2fa/totp/begin, session-gated) starts enrollment: generate a fresh secret (pending, not yet enabled) and return the otpauth:// URI + base32 secret. The consumer renders the QR; enrollment completes at /confirm with a valid code.
func (*Authenticator) TOTPConfirm ¶ added in v0.2.0
func (a *Authenticator) TOTPConfirm(c *reqCtx)
TOTPConfirm (POST /auth/2fa/totp/confirm, session-gated) finishes enrollment: validate a code against the pending secret, enable TOTP, and return single-use recovery codes ONCE.
func (*Authenticator) TOTPDisable ¶ added in v0.2.0
func (a *Authenticator) TOTPDisable(c *reqCtx)
TOTPDisable (POST /auth/2fa/disable, session-gated) turns 2FA off, requiring a current TOTP or recovery code so a walked-up session can't silently disable it.
func (*Authenticator) TwoFactorEnabled ¶ added in v0.2.0
func (a *Authenticator) TwoFactorEnabled() bool
TwoFactorEnabled reports whether 2FA is wired at all (a store is set).
func (*Authenticator) TwoFactorPending ¶ added in v0.2.0
func (a *Authenticator) TwoFactorPending(c *reqCtx)
TwoFactorPending (GET /auth/2fa/pending) reports whether a login is awaiting a second factor — the SPA uses it after a social/OIDC redirect to decide whether to show the 2FA prompt.
func (*Authenticator) TwoFactorVerify ¶ added in v0.2.0
func (a *Authenticator) TwoFactorVerify(c *reqCtx)
TwoFactorVerify (POST /auth/2fa/verify) completes a login halted for a second factor: read the short-lived 2fa-pending cookie, validate a TOTP code OR consume a recovery code, then mint the real session.
func (*Authenticator) TwoFactorWebauthnBegin ¶ added in v0.2.0
func (a *Authenticator) TwoFactorWebauthnBegin(c *reqCtx)
TwoFactorWebauthnBegin (POST /auth/2fa/webauthn/begin) starts a passkey assertion for the pending user's registered credentials.
func (*Authenticator) TwoFactorWebauthnFinish ¶ added in v0.2.0
func (a *Authenticator) TwoFactorWebauthnFinish(c *reqCtx)
TwoFactorWebauthnFinish (POST /auth/2fa/webauthn/finish) verifies the passkey assertion and, on success, mints the real session — completing the login halted for a second factor.
func (*Authenticator) ValidateAPIKey ¶
func (a *Authenticator) ValidateAPIKey(raw string) (*APIKeyInfo, bool)
ValidateAPIKey resolves a raw API key to its info if it exists and is not expired (and stamps last-used). Useful for wiring API-key auth into other surfaces (e.g. the SCIM server).
func (*Authenticator) ValidateAPIKeyScope ¶
func (a *Authenticator) ValidateAPIKeyScope(raw, scope string) bool
ValidateAPIKeyScope is ValidateAPIKey plus a scope check — convenient for gating a GLOBAL surface (e.g. the global SCIM server) to keys that carry a specific scope:
scim.NewServer(store, func(t string) bool { return authn.ValidateAPIKeyScope(t, "scim") })
Org-bound keys (OrgID != "") are REFUSED here — a key bound to one org grants nothing outside it; gate per-org surfaces with ValidateOrgAPIKeyScope instead.
func (*Authenticator) ValidateOrgAPIKeyScope ¶ added in v0.7.0
func (a *Authenticator) ValidateOrgAPIKeyScope(raw, scope, orgID string) bool
ValidateOrgAPIKeyScope is ValidateAPIKey plus a scope check that honors org binding: a GLOBAL key with the scope passes everywhere; an ORG-BOUND key passes only for its own org. This is the gate to hand an org's SCIM mount:
scim.NewServer(view, func(t string) bool { return authn.ValidateOrgAPIKeyScope(t, "scim", org.ID) })
func (*Authenticator) WebauthnLoginBegin ¶
func (a *Authenticator) WebauthnLoginBegin(c *reqCtx)
WebauthnLoginBegin (POST /auth/webauthn/login/begin) starts a discoverable passkey login. Discoverable (usernameless) login sends an EMPTY allowCredentials list, which is exactly what makes the browser offer "use a phone or tablet" — the FIDO2 hybrid transport that shows a QR code so the user can sign in with a passkey on another device. No server-side QR handling is needed; the platform negotiates it as long as we stay discoverable and the rpID is fixed.
func (*Authenticator) WebauthnLoginFinish ¶
func (a *Authenticator) WebauthnLoginFinish(c *reqCtx)
WebauthnLoginFinish (POST /auth/webauthn/login/finish?next=) verifies the assertion and signs the user in. The assertion is the body; the redirect target comes via ?next=.
func (*Authenticator) WebauthnRegisterBegin ¶
func (a *Authenticator) WebauthnRegisterBegin(c *reqCtx)
WebauthnRegisterBegin (POST /auth/webauthn/register/begin) starts passkey enrolment for the signed-in user.
func (*Authenticator) WebauthnRegisterFinish ¶
func (a *Authenticator) WebauthnRegisterFinish(c *reqCtx)
WebauthnRegisterFinish (POST /auth/webauthn/register/finish?name=) stores the new passkey. The WebAuthn attestation is the request body; the optional label comes via ?name=.
type Authorizer ¶
Authorizer is an optional post-login hook (e.g. multi-tenant provisioning). It may run side effects (create the user's account, accept invites) and returns the role to embed in the session, or ErrAccessDenied to refuse the login.
type BreachChecker ¶ added in v0.5.0
BreachChecker reports whether a candidate password appears in a known-breach corpus. It is consulted on signup, password reset, and password change, in ADDITION to the embedded common-password list.
A non-nil error means the check could not be COMPLETED (e.g. the upstream service was unreachable). The checker itself decides fail-open vs fail-closed: a fail-open checker swallows transport errors and returns (false, nil); a fail-closed checker returns the error, and the engine then refuses the password rather than let a possibly-breached one through.
type Config ¶
type Config struct {
Issuer string // OIDC_ISSUER, e.g. https://id.example.com
ClientID string // OIDC_CLIENT_ID
ClientSecret string // OIDC_CLIENT_SECRET (confidential client)
RedirectURL string // OIDC_REDIRECT_URL, e.g. https://app.example.com/auth/callback
AllowedGroups []string // OIDC_ALLOWED_GROUPS (comma-sep); empty = any authenticated user
SessionSecret []byte // SESSION_SECRET — HMAC key for the session/flow cookies
// PreviousSessionSecrets are retired signing keys still accepted for VERIFICATION (never signing),
// so SESSION_SECRET can be rotated without logging every user out at once. Populate from
// SESSION_SECRET_PREVIOUS (comma-separated) or programmatically; keep an old key only for one
// session TTL after rotation, then drop it (each entry widens the trusted-signer set). Each must
// also be at least 32 bytes.
PreviousSessionSecrets [][]byte
CookieSecure bool // COOKIE_SECURE — force the Secure flag (also auto-on under TLS)
// TrustedOrigins is an optional allow-list of exact origins (scheme://host[:port]) accepted on
// state-changing requests as defense-in-depth ON TOP OF the double-submit CSRF token. AppURL's
// origin is always allowed. Empty (and an absent Origin/Referer) fails open — the token remains
// the primary check — so this never rejects a legitimate same-origin request.
TrustedOrigins []string
// BreachCheckHIBP (HIBP_BREACH_CHECK=true) wires the default Have I Been Pwned k-anonymity password
// checker at boot. It can also be set programmatically via SetBreachChecker with a custom checker.
BreachCheckHIBP bool
// AppURL (APP_URL) is the public origin of the app, used to build email links and
// post-login redirects (e.g. https://trader.savin.nyc). Falls back to RedirectURL's origin.
AppURL string
// OwnerEmail (OWNER_EMAIL) is exempt from hard per-account lockout so the operator can't
// be locked out mid-migration (recovery via email link stays open regardless).
OwnerEmail string
// BrandName (BRAND_NAME) labels the auth emails and is the default WebAuthn relying-party
// display name. Falls back to "go-auth-x" when unset.
BrandName string
// OIDCAssumeVerified (OIDC_ASSUME_VERIFIED) trusts the issuer's email as verified even when the
// id_token omits an email_verified claim. Leave false (default) to require the claim — the
// correct posture for multi-IdP setups; set true only for a single, fully-trusted issuer that
// doesn't emit the claim.
OIDCAssumeVerified bool
// PublicPath, if set, is consulted (in addition to the always-public /auth, /_next, health
// routes) to decide which paths the gate lets through unauthenticated — e.g. your SPA's login
// pages. nil = the built-in reference defaults (/login, /register, /welcome, …).
PublicPath func(path string) bool
// Social login (OAuth). Each provider turns on only when its client id + secret are set.
// Callback URLs are AppURL + /auth/social/<provider>/callback — one per enabled provider
// (google, github, facebook, apple, microsoft, discord, and any SocialOIDC slug) — register
// each upstream.
GoogleClientID string // GOOGLE_CLIENT_ID
GoogleClientSecret string // GOOGLE_CLIENT_SECRET
GitHubClientID string // GITHUB_CLIENT_ID
GitHubClientSecret string // GITHUB_CLIENT_SECRET
FacebookClientID string // FACEBOOK_CLIENT_ID
FacebookClientSecret string // FACEBOOK_CLIENT_SECRET
// Sign in with Apple. The "client secret" is an ES256 JWT the library signs from your private
// key, so Apple needs four values: the Services ID (client id), your Team ID, the Key ID, and the
// .p8 EC private key (PEM; literal or with \n-escaped newlines). Apple posts its callback
// (form_post), so its flow cookie is set SameSite=None — HTTPS is required.
AppleClientID string // APPLE_CLIENT_ID (Services ID)
AppleTeamID string // APPLE_TEAM_ID
AppleKeyID string // APPLE_KEY_ID
ApplePrivateKey string // APPLE_PRIVATE_KEY (.p8 PEM)
// Microsoft / Entra ID (OIDC). Tenant defaults to "common" (any work/school or personal Microsoft
// account) — a MULTI-TENANT endpoint whose token email is NOT trustworthy: any Entra tenant can mint
// a token asserting any address (nOAuth). Set MICROSOFT_TENANT to a single tenant GUID / verified
// custom domain to restrict sign-in to that organization, which is the ONLY configuration in which the
// token email is trusted. With a multi-tenant endpoint the email is treated as unverified, so Entra's
// (which omits email_verified) sign-ins are refused until a tenant is pinned. MicrosoftStrictEmailVerified
// additionally requires an explicit email_verified even for a pinned single tenant.
MicrosoftClientID string // MICROSOFT_CLIENT_ID
MicrosoftClientSecret string // MICROSOFT_CLIENT_SECRET
MicrosoftTenant string // MICROSOFT_TENANT (default "common" — pin a single tenant to enable email sign-in)
MicrosoftStrictEmailVerified bool // MICROSOFT_STRICT_EMAIL_VERIFIED
// Discord (OAuth2 + REST; requires a verified email).
DiscordClientID string // DISCORD_CLIENT_ID
DiscordClientSecret string // DISCORD_CLIENT_SECRET
// SocialOIDC registers arbitrary standards-compliant OIDC identity providers as social logins
// (GitLab, Okta, Auth0, Keycloak, …), each mounted at /auth/social/<Name>/{login,callback}.
// Programmatic only (set on the Config) — there is no env form.
SocialOIDC []SocialOIDCProvider
}
Config is read from the environment at boot. Auth turns on only when an issuer and client id are present.
func ConfigFromEnv ¶
func ConfigFromEnv() Config
ConfigFromEnv loads the OIDC configuration from the environment.
type CredentialStore ¶
type CredentialStore interface {
// Identity
UserByEmail(email string) (*AuthUser, error)
UserBySub(sub string) (*AuthUser, error)
CreateLocalUser(email, name string) (*AuthUser, error) // Sub="local:<uuid>", EmailVerified=false
SetEmailVerified(userID string, verified bool) error
// SetEmail changes a user's email (used by the verified change-email flow AFTER the new address is
// proven). It must uphold the one-user-per-email invariant, returning ErrEmailConflict if another
// user already owns newEmail.
SetEmail(userID, newEmail string) error
// DeleteUser hard-deletes a user and everything keyed to it (credentials, passkeys, tokens, OAuth
// links, 2FA, group memberships). Audit rows are retained but any PII (email/IP) is anonymized, so
// the forensic count survives an erasure. Idempotent: deleting an absent user is not an error.
DeleteUser(userID string) error
// Passkeys (WebAuthn)
EnsureWebauthnHandle(userID string) ([]byte, error) // get-or-create the stable user handle
UserByWebauthnHandle(handle []byte) (*AuthUser, error) // resolve a discoverable login
Passkeys(userID string) ([]Passkey, error)
AddPasskey(userID string, p Passkey) error
TouchPasskey(credentialID []byte, signCount uint32) error // update sign count + last-used
RemovePasskey(userID, id string) error
RenamePasskey(userID, id, name string) error // relabel a passkey; no-op if not the user's
// Password
PasswordHash(userID string) (hash, algo string, err error) // ErrNoCredential if unset
SetPasswordHash(userID, hash, algo string) error
// Social (OAuth) identities — natural key (provider, subject), NOT email, so an upstream
// email change doesn't fork the account.
UserByOAuth(provider, subject string) (*AuthUser, error) // ErrNoUser if unlinked
LinkOAuth(userID, provider, subject, email string) error // idempotent per (provider,subject)
UnlinkOAuth(userID, provider string) error // remove the user's link to a provider
OAuthIdentities(userID string) ([]string, error) // provider slugs the user has linked
// Single-use, hashed-at-rest email tokens (magic-link, verify-email, password-reset, invite)
CreateToken(purpose, userID, email string, tokenHash []byte, expiresAt time.Time) error
ConsumeToken(purpose string, tokenHash []byte) (*TokenClaim, error) // marks consumed atomically; ErrTokenInvalid
// PeekToken validates a token (exists, unconsumed, unexpired) and returns its claim WITHOUT consuming
// it, so a caller can validate downstream input (e.g. the new password) before burning a single-use
// link. ErrTokenInvalid on a miss/expiry.
PeekToken(purpose string, tokenHash []byte) (*TokenClaim, error)
// Email OTP (short numeric codes). Unlike the 256-bit tokens above these are brute-forceable, so
// they are scoped by (purpose,email) — NOT matched globally by hash — and carry an attempt counter.
// CreateEmailOTP replaces any existing OTP for (purpose,email). VerifyEmailOTP atomically compares
// (constant-time), increments the attempt count, and invalidates the code on success OR once
// maxAttempts is exceeded; it returns ErrTokenInvalid on any miss/expiry/exhaustion.
CreateEmailOTP(purpose, email string, codeHash []byte, expiresAt time.Time, maxAttempts int) error
VerifyEmailOTP(purpose, email string, codeHash []byte) (*TokenClaim, error)
// Audit + lockout. A userID of "" means "no user" (e.g. a failed login before resolution) —
// the store records the row without keying it to an account.
RecordAudit(userID, email, ip, method, event string, success bool, detail string)
RecentFailures(email string, since time.Time) (int, error)
}
CredentialStore is the persistence the in-app auth methods need. It is implemented by the control plane (tenant package), so the auth package stays storage-agnostic — the same boundary the Authorizer hook uses. Lookups return ErrNoUser / ErrNoCredential when absent. The passkey and social-identity methods are added in later phases.
type DirectoryStore ¶
type DirectoryStore interface {
// Groups
CreateGroup(name, description string) (*Group, error)
Groups() ([]Group, error)
GroupByName(name string) (*Group, error) // ErrNoGroup if absent
DeleteGroup(id string) error
AddUserToGroup(userID, groupID string) error
RemoveUserFromGroup(userID, groupID string) error
UserGroups(userID string) ([]Group, error)
GroupMembers(groupID string) ([]AuthUser, error)
// Admin user management
ListUsers() ([]AuthUser, error)
UserByID(id string) (*AuthUser, error) // ErrNoUser if absent
UserByEmail(email string) (*AuthUser, error) // ErrNoUser if absent — used by SCIM to enforce create-uniqueness
SetUserDisabled(userID string, disabled bool) error
// SetUserBan sets or clears a time-boxed ban with a reason. banned=false clears it; a nil `until`
// with banned=true is a permanent ban, else the ban lifts at *until.
SetUserBan(userID string, banned bool, until *time.Time, reason string) error
// UpsertExternalUser provisions/updates a user from an external directory (LDAP/SCIM) keyed by
// an external Sub (e.g. "ldap:<uid>" / "scim:<id>"). Applies the same safe email-linking rule.
UpsertExternalUser(sub, email, name string, emailVerified bool) (*AuthUser, error)
// API keys (sha256-at-rest). APIKeyByHash returns ErrNoCredential if absent and does NOT
// itself check expiry (the caller does, so an expired key can still be listed/revoked).
CreateAPIKey(name string, groups, scopes []string, prefix string, hash []byte, expiresAt *time.Time) (*APIKeyInfo, error)
APIKeyByHash(hash []byte) (*APIKeyInfo, error)
ListAPIKeys() ([]APIKeyInfo, error)
RevokeAPIKey(id string) error
TouchAPIKey(id string) error // best-effort last-used stamp
}
DirectoryStore is the OPTIONAL persistence for groups, group membership, API keys, and admin user management. Wire it with SetDirectoryStore to enable groups, per-group access control (RequireGroups), API-key auth, and the admin REST API. It is intentionally separate from CredentialStore, so consumers that only need authentication don't have to implement it.
func NewOrgScopedDirectory ¶ added in v0.7.0
func NewOrgScopedDirectory(dir DirectoryStore, orgs OrgStore, orgID string) (DirectoryStore, error)
NewOrgScopedDirectory wraps a DirectoryStore in a view confined to orgID, satisfying scim.NewServer so each customer's IdP provisions ONLY its own organization:
view, _ := authx.NewOrgScopedDirectory(store, store, org.ID)
scim.NewServer(view, func(t string) bool { return authn.ValidateOrgAPIKeyScope(t, "scim", org.ID) })
Org-scoped semantics differ from the global SCIM mount in three deliberate ways:
- Provisioned subjects are NAMESPACED per org ("scim:org<id>:<userName>"), so the same userName pushed by two customers yields two accounts, not one shared identity.
- An existing account whose email a customer's IdP asserts is NEVER adopted or rebound (ErrEmailConflict → SCIM 409): users stay global, and bringing an existing account into an org requires the consent-based invite flow. Only accounts this org's IdP provisioned (or that already belong to the org) can be updated through the view.
- Deprovisioning (SCIM active=false / DELETE) removes the user FROM THE ORG (membership + its org groups via the RemoveOrgMember contract) — never the global account, which may belong to other orgs. Org owners cannot be deprovisioned via SCIM (manage owners in-app).
type Group ¶
type Group struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt,omitempty"` // for SCIM meta.lastModified; zero if the store doesn't track it
// OrgID binds an org-scoped group to its organization ("" = a global group, the default).
// Names are unique per (org, name). Org-scoped groups are invisible to the global verbs
// (Groups / GroupByName) and never ride in the session cookie — they are managed through
// OrgDirectoryStore and checked live by RequireOrgGroupsHTTP.
OrgID string `json:"orgId,omitempty"`
}
Group is a named collection of users used for access control. ID is opaque (see AuthUser.ID).
type Identity ¶
type Identity struct {
Subject string
Email string
Name string
Groups []string
// EmailVerified reports whether THIS login proved the email — the OIDC email_verified claim,
// a provider-verified social email, or a redeemed in-app token. The reference Authorizer feeds
// it to the safe account-linking rule (only a proven email may reclaim/adopt another row).
EmailVerified bool
}
Identity is the authenticated identity handed to an Authorizer after login.
type Mailer ¶
Mailer sends the in-app auth emails. Implemented by internal/email; the auth package owns the message templates and calls Send. nil/!Configured() disables email methods.
type Org ¶ added in v0.6.0
type Org struct {
ID string `json:"id"`
Slug string `json:"slug"`
Name string `json:"name"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt,omitempty"` // zero if the store doesn't track it
}
Org is a tenant/organization: a named collection of users with per-org roles, layered ABOVE authentication. Users stay global (one account, many orgs) — an org never owns a user, so the one-user-per-email invariant and the safe account-linking rule are unaffected.
ID is an opaque string (uuid/ULID-friendly, see roadmap #2); the reference stores use their numeric PKs and convert at the boundary. Slug is the unique, URL-safe handle.
type OrgDirectoryStore ¶ added in v0.7.0
type OrgDirectoryStore interface {
// Org-scoped groups. CreateOrgGroup enforces (org, name) uniqueness; OrgGroupByName returns
// ErrNoGroup when absent. OrgOfGroup resolves which org owns a group ID ("" = global,
// ErrNoGroup if the group doesn't exist) — the check every by-ID group operation gates on.
CreateOrgGroup(orgID, name, description string) (*Group, error)
OrgGroups(orgID string) ([]Group, error)
OrgGroupByName(orgID, name string) (*Group, error)
OrgOfGroup(groupID string) (string, error)
UserOrgGroups(orgID, userID string) ([]Group, error)
// Org-bound API keys. An org-bound key (OrgID != "") is refused by every GLOBAL surface
// (adminGuard, ValidateAPIKeyScope, group merging) and only satisfies ValidateOrgAPIKeyScope
// for ITS org. ListAPIKeys (global) still lists all keys — the OrgID field says which are bound.
CreateOrgAPIKey(orgID, name string, groups, scopes []string, prefix string, hash []byte, expiresAt *time.Time) (*APIKeyInfo, error)
ListOrgAPIKeys(orgID string) ([]APIKeyInfo, error)
}
OrgDirectoryStore is an OPTIONAL upgrade interface a DirectoryStore may implement to support org-scoped resources: groups and API keys bound to one organization (the v0.7 phase). It is detected via type assertion — a DirectoryStore that doesn't implement it simply has the org-scoped features off (the handlers 501), so existing custom stores keep compiling unchanged.
Org-scoped groups live in the same namespace as global groups but are invisible to the global verbs: Groups()/GroupByName() return ONLY global groups, and group names are unique per (org, name) — two orgs may both have "engineering". Org-scoped groups never ride in the session cookie (see RequireOrgGroupsHTTP, which checks them live).
type OrgInvite ¶ added in v0.7.0
type OrgInvite struct {
ID string `json:"id"` // opaque; see AuthUser.ID
OrgID string `json:"orgId"`
Email string `json:"email"`
Role string `json:"role"`
InvitedBy string `json:"invitedBy,omitempty"` // inviter's email, for the pending-invites UI
ExpiresAt time.Time `json:"expiresAt"`
CreatedAt time.Time `json:"createdAt"`
}
OrgInvite is a pending, first-class invitation record: listable and revocable, unlike a bare token. The raw invite token is emailed once and only its hash is stored; the org + role bind to the RECORD, so nothing security-relevant rides in the accept URL.
type OrgMember ¶ added in v0.6.0
OrgMember is one of an org's members (user-joined, for member lists).
type OrgStore ¶ added in v0.6.0
type OrgStore interface {
// Orgs. CreateOrg must enforce slug uniqueness (the handlers pre-check and 409, the store's
// constraint is the fail-closed backstop under races). DeleteOrg cascades memberships and is
// idempotent. Lookups return ErrNoOrg when absent.
CreateOrg(slug, name string) (*Org, error)
OrgByID(id string) (*Org, error)
OrgBySlug(slug string) (*Org, error)
Orgs() ([]Org, error)
RenameOrg(id, name string) error
DeleteOrg(id string) error
// Membership. SetOrgMember UPSERTS (adds the user or updates their role) — role-change policy
// (who may grant what, the last-owner guard) is enforced by the callers, not the store. It
// returns ErrNoOrg when the org is absent and ErrNoUser when the user doesn't exist (a
// dangling row would be invisible in OrgMembers yet inherited by a future user assigned that
// ID). RemoveOrgMember is idempotent and ALSO removes the user from the org's org-scoped
// groups when the store supports them — org-group membership must not outlive org membership.
// OrgRole returns ErrNotOrgMember when there is no membership (ErrNoOrg when the org itself
// is absent).
SetOrgMember(orgID, userID, role string) error
RemoveOrgMember(orgID, userID string) error
OrgRole(orgID, userID string) (string, error)
UserOrgs(userID string) ([]UserOrg, error)
OrgMembers(orgID string) ([]OrgMember, error)
// Invitations — first-class records (listable + revocable), hashed-at-rest like every other
// token. CreateOrgInvite REPLACES any pending invite for (org, email), so re-inviting updates
// the role/expiry instead of stacking rows. OrgInvites returns the PENDING set (unconsumed,
// unexpired, unrevoked). RevokeOrgInvite is idempotent. PeekOrgInvite validates a token
// WITHOUT consuming it (so the wrong account can't burn a valid invite); ConsumeOrgInvite
// atomically marks it used — both return ErrTokenInvalid on any miss/expiry/revocation.
CreateOrgInvite(orgID, email, role, invitedBy string, tokenHash []byte, expiresAt time.Time) (*OrgInvite, error)
OrgInvites(orgID string) ([]OrgInvite, error)
RevokeOrgInvite(orgID, id string) error
PeekOrgInvite(tokenHash []byte) (*OrgInvite, error)
ConsumeOrgInvite(tokenHash []byte) (*OrgInvite, error)
}
OrgStore is the OPTIONAL persistence for organizations and their memberships. Wire it with SetOrgStore to enable the org layer: an active-org session claim, org switching, member management, email invites, RequireOrgHTTP, and the admin org verbs. Like the other capability stores, nil = the feature is off with zero behavior change.
Memberships are keyed by the CredentialStore's user ID, so org features also require a CredentialStore (see OrgsEnabled).
type Passkey ¶
type Passkey struct {
ID string // opaque; see AuthUser.ID
CredentialID []byte
PublicKey []byte
AttestationType string
AAGUID []byte
SignCount uint32
Transports string // CSV of authenticator transports
BackupEligible bool
BackupState bool
Name string
CreatedAt time.Time
LastUsedAt time.Time
}
Passkey is the auth package's storage-agnostic view of a registered WebAuthn credential.
type RateLimiter ¶
RateLimiter throttles auth attempts keyed by an arbitrary string (a client IP or an account email). Allow records ONE attempt for key and reports whether it remains within budget (false = throttle). Implementations MUST be safe for concurrent use. The built-in default is in-memory and per-process; for a multi-replica deployment supply a shared-store (e.g. Redis) implementation via SetRateLimiters so the budget is coherent across replicas.
type SessionClaims ¶
type SessionClaims struct {
Email string `json:"email,omitempty"`
Name string `json:"name,omitempty"`
Groups []string `json:"groups,omitempty"`
Role string `json:"role,omitempty"` // optional role from an Authorizer
IDToken string `json:"idt,omitempty"` // raw OIDC id_token — used as id_token_hint at RP-initiated logout
// SID is a random per-session id, present only when an optional SessionStore is wired. It lets the
// gate consult server-side revocation (sign-out-everywhere, list/revoke devices). Empty = the
// default stateless behavior (no server-side session record).
SID string `json:"sid,omitempty"`
// ImpersonatedBy is the admin subject when this session was minted by admin impersonation. Surfaced
// at /auth/me so the app can show a banner, and refused by adminGuard so an impersonated session
// can't perform further admin actions.
ImpersonatedBy string `json:"imp,omitempty"`
// Org is the ACTIVE organization's ID when an OrgStore is wired — the sole membership at login,
// or whatever POST /auth/org/switch re-minted. Only the active org rides in the cookie (the full
// membership list is served by /auth/me); empty = no active org.
Org string `json:"org,omitempty"`
// OrgRole is the user's role in Org AT MINT TIME — a display hint. Enforcement (RequireOrgHTTP,
// the org endpoints) re-reads the live role from the store, so a demotion doesn't ride out the TTL.
OrgRole string `json:"orgRole,omitempty"`
jwt.RegisteredClaims
}
SessionClaims is the signed session-cookie payload — the authenticated identity.
type SessionRecord ¶ added in v0.5.0
type SessionRecord struct {
SID string `json:"id"`
Subject string `json:"-"`
UserID string `json:"-"` // opaque; see AuthUser.ID ("" when the session isn't tied to a stored user)
UserAgent string `json:"userAgent,omitempty"`
IP string `json:"ip,omitempty"`
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
Current bool `json:"current"` // set by the engine when listing; not persisted
}
SessionRecord is a server-side record of an issued session, used for revocation and device listing. It is written at login and read back for the "your sessions" list; only non-secret metadata.
type SessionStore ¶ added in v0.5.0
type SessionStore interface {
// RecordSession persists a freshly minted session (best-effort; a failure must not block login).
RecordSession(rec SessionRecord) error
// IsRevoked reports whether the session with this SID must be rejected. It returns true for an
// explicitly-revoked session AND for an UNKNOWN sid: because RecordSession runs at mint and rows are
// only ever tombstoned (never deleted), a missing record means a lost write, and honoring it would
// leave an un-revocable session. Return (false, err) on a genuine store error so the caller can fail
// open (a transient outage must not mass-logout). Only sessions with a non-empty SID are checked.
IsRevoked(sid string) (bool, error)
RevokeSession(sid string) error
RevokeAllForUser(subject string) error
ListSessionsForUser(subject string) ([]SessionRecord, error)
}
SessionStore is OPTIONAL persistence that layers server-side revocation over the stateless signed-cookie default. Wire it with SetSessionStore to enable sign-out-everywhere, per-device list/revoke, and ban-revokes-all. Sessions minted before a store is configured carry no SID and are simply not tracked (they remain valid until they expire) — enable the store from the start for full coverage. It mirrors the pluggable RateLimiter seam: nil = today's pure-stateless behavior.
type SocialOIDCProvider ¶ added in v0.3.0
type SocialOIDCProvider struct {
Name string // URL slug (lowercased), e.g. "gitlab", "okta", "auth0"
Issuer string // OIDC issuer URL
ClientID string // OAuth client id
ClientSecret string // OAuth client secret (confidential client)
Scopes []string // optional; defaults to openid, email, profile
// AssumeVerified trusts the token's email when the provider omits email_verified (e.g. a
// tenant that owns its addresses). Off by default — an absent/false email_verified is refused.
AssumeVerified bool
}
SocialOIDCProvider registers any OIDC-compliant identity provider as a social login. Discovery uses <Issuer>/.well-known/openid-configuration; sign-in follows the same Authorization Code + PKCE + nonce + azp + email_verified path as Google.
type TOTPInfo ¶ added in v0.2.0
TOTPInfo is a user's stored TOTP state. Secret is the base32 shared secret; Enabled is false while enrollment is pending (secret set but not yet confirmed with a valid code); LastStep is the most recently consumed time-step, used to reject a replayed code.
type TokenClaim ¶
TokenClaim is what a redeemed single-use token resolves to.
type TwoFactorStore ¶ added in v0.2.0
type TwoFactorStore interface {
TOTP(userID string) (*TOTPInfo, error) // ErrNoCredential if the user has no TOTP
SetTOTPSecret(userID, secret string) error // (re)enroll: store secret, Enabled=false, LastStep=0
EnableTOTP(userID string) error // confirm enrollment
DisableTOTP(userID string) error // remove TOTP + all recovery codes
SetTOTPLastStep(userID string, step uint64) error // set the enrollment step (no concurrency concern)
// ClaimTOTPStep atomically records a just-verified time-step ONLY if it advances past LastStep,
// returning true iff it did. This is the replay guard for login verification: the compare and the
// write must be one atomic step so two concurrent requests can't both accept the same code.
ClaimTOTPStep(userID string, step uint64) (bool, error)
ReplaceRecoveryCodes(userID string, hashes [][]byte) error // set at enrollment (replaces any existing)
ConsumeRecoveryCode(userID string, hash []byte) (bool, error) // true if it existed and was removed
RecoveryCodesRemaining(userID string) (int, error)
}
TwoFactorStore is OPTIONAL persistence for TOTP + recovery codes. Wire it with SetTwoFactorStore to enable two-factor auth. It's separate from CredentialStore so consumers that don't want 2FA are unaffected. User ids match CredentialStore. Recovery codes are stored as sha256 hashes; consuming one removes it. Secrets should be encrypted at rest by the implementation.
Source Files
¶
- account.go
- account_lifecycle.go
- admin.go
- admin_orgs.go
- admin_users.go
- authenticator.go
- breach.go
- config.go
- credstore.go
- csrf.go
- directory.go
- emailotp.go
- emails.go
- handlers.go
- httpadapter.go
- login.go
- magic.go
- middleware.go
- orgdirectory.go
- orgs.go
- orgs_handlers.go
- password.go
- ratelimit.go
- session.go
- sessions.go
- social.go
- social_apple.go
- social_oidc.go
- stepup.go
- tokens.go
- totp.go
- twofactor.go
- twofactor_handlers.go
- twofactor_webauthn.go
- webauthn.go
- webctx.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package ldapsync syncs users and groups from an LDAP/Active Directory server into an authx.DirectoryStore (and the user records behind it).
|
Package ldapsync syncs users and groups from an LDAP/Active Directory server into an authx.DirectoryStore (and the user records behind it). |
|
Package email is a minimal SMTP sender for the in-app auth flows (magic-link, email verification, password reset, invites).
|
Package email is a minimal SMTP sender for the in-app auth flows (magic-link, email verification, password reset, invites). |
|
Package scim is a SCIM 2.0 provisioning server (Users + Groups) over an authx.DirectoryStore, so an upstream IdP (Okta, Entra/Azure AD, JumpCloud) can push and deprovision users and groups.
|
Package scim is a SCIM 2.0 provisioning server (Users + Groups) over an authx.DirectoryStore, so an upstream IdP (Okta, Entra/Azure AD, JumpCloud) can push and deprovision users and groups. |
|
store
|
|
|
gormstore
Package gormstore is a reference GORM-backed implementation of authx.CredentialStore plus a safe reference Authorizer (identity upsert with verified-email account-linking).
|
Package gormstore is a reference GORM-backed implementation of authx.CredentialStore plus a safe reference Authorizer (identity upsert with verified-email account-linking). |
|
memory
Package memory is a zero-dependency, in-memory reference implementation of authx.CredentialStore (+ the safe verified-email linking rule).
|
Package memory is a zero-dependency, in-memory reference implementation of authx.CredentialStore (+ the safe verified-email linking rule). |