howdy/auth
Email-token, password and built-in provider authentication. This package owns its tables; applications
keep small facts in howdy/auth/field and anything relational in their own tables.
Authorization lives in howdy/authorization.
This module is the public face. Each flow is implemented under
howdy/auth/internal: config holds the configuration record and its one
constructor, email_flow, password_flow, provider_flow, sso_flow,
passkey_flow, mfa_flow, account_flow and session_flow hold the
operations, labels the stored method and ceremony names, and common
what several flows share. The types here are the ones applications see;
the flows return their internal/types twins, converted at this boundary.
Types
The configured authentication runtime. Construct with new or
new_without_email, then the with_* builders. The record itself lives in
howdy/auth/internal/config, where every flow reads it.
pub opaque type Auth
Deliver the token privately. Never log it or expose it in a public response.
The token expires after policy.challenge_seconds (ten minutes by default)
and must be exchanged via POST.
pub type Delivery {
Delivery(
email: String,
token: secret.Secret,
purpose: Purpose,
link: option.Option(secret.Secret),
code: option.Option(secret.Secret),
locale: option.Option(String),
)
}
Constructors
-
Delivery( email: String, token: secret.Secret, purpose: Purpose, link: option.Option(secret.Secret), code: option.Option(secret.Secret), locale: option.Option(String), )Arguments
- link
-
With
with_email_links, a page URL that fills intokenfor its reader to confirm.Nonefor notices, which carry no token. - code
-
With
with_email_codes, a six-digit code that signs in liketokenwhen entered with this address. Only forSignIn,RegistrationandAlreadyRegistered, and only until three wrong guesses. - locale
-
The locale to write the email in, as a BCP 47 tag: the recipient’s own if they chose or registered with one, otherwise the locale of the request that caused the email;
Nonewhen neither is known. Seewith_locales.
pub type LoginStep {
SignedIn(Session)
SecondFactor(MfaChallenge)
}
Constructors
-
SignedIn(Session) -
SecondFactor(MfaChallenge)
How a session was authenticated.
pub type Method {
EmailToken
Password
Provider(id: String)
Passkey
Impersonation
}
Constructors
-
EmailToken -
Password -
Provider(id: String) -
Passkey -
ImpersonationIssued by
impersonatefor an operator, never by a credential.
pub type MfaChallenge {
MfaChallenge(token: secret.Secret)
}
Constructors
-
MfaChallenge(token: secret.Secret)
pub type MfaMethod {
Totp
DeliveredCode
RecoveryCode
}
Constructors
-
Totp -
DeliveredCode -
RecoveryCode
pub type MfaSession {
MfaSession(
session: Session,
trusted_device: option.Option(secret.Secret),
)
}
Constructors
-
MfaSession( session: Session, trusted_device: option.Option(secret.Secret), )
pub type MfaSetup {
MfaSetup(
challenge: secret.Secret,
key: option.Option(secret.Secret),
uri: option.Option(secret.Secret),
qr_code: option.Option(secret.Secret),
)
}
Constructors
-
MfaSetup( challenge: secret.Secret, key: option.Option(secret.Secret), uri: option.Option(secret.Secret), qr_code: option.Option(secret.Secret), )Arguments
- qr_code
-
urias a standalone SVG QR code for an authenticator app to scan. It contains the key: show it only to the user enrolling, never cache it.
pub type PasskeyChallenge {
PasskeyChallenge(challenge: secret.Secret, options: json.Json)
}
Constructors
-
PasskeyChallenge(challenge: secret.Secret, options: json.Json)
pub type ProviderOutcome {
ProviderSession(Session)
ProviderLinked
ProviderSecondFactor(MfaChallenge)
}
Constructors
-
ProviderSession(Session) -
ProviderLinked -
ProviderSecondFactor(MfaChallenge)
Store browser_token in a Secure, HttpOnly, SameSite=Lax cookie before
redirecting to url. Prefer routes.providers, which owns these details.
pub type ProviderStart {
ProviderStart(url: String, browser_token: secret.Secret)
}
Constructors
-
ProviderStart(url: String, browser_token: secret.Secret)
What the email should tell its reader. Derived from the request and from whether the address already has an account; the HTTP response is the same either way, so this reveals nothing to anyone but the inbox owner.
pub type Purpose {
SignIn
Registration
AlreadyRegistered
EmailChange
EmailChangeApproval
EmailChanged
PasswordChanged
}
Constructors
-
SignInSign in to the account that already owns this address.
-
RegistrationCreate the account that redeeming this token will verify.
-
AlreadyRegisteredSomeone asked to register an address that already has an account. Say so rather than inviting them to register again: redeeming the token signs that existing account in, and no password from the request was kept.
-
EmailChangeConfirm a change of the signed-in account’s email, not a login token.
-
EmailChangeApprovalSent to the CURRENT address when
with_email_change_approvalis on: someone signed in asked to move the account to another address. The token approves that on the account page; it cannot sign in. Tell a reader who did not ask to ignore it and secure the account. -
EmailChangedA notice to the OLD address: the account now signs in with a different one.
tokenis empty. Tell a reader who did not do this to contact support, because this mailbox can no longer recover the account. -
PasswordChangedA notice, not a request: the account’s password was just replaced using its current password.
tokenis empty. Tell the reader to reset the password from a fresh email login if this was not them.
Returned by a successful sign-in. Keep the token secret. Browser routes put it in an HttpOnly cookie; native API clients use Bearer auth.
pub type Session {
Session(user: user.User, token: secret.Secret, expires_at: Int)
}
Constructors
-
Session(user: user.User, token: secret.Secret, expires_at: Int)
One of a user’s live sessions. id identifies it for revoke_session;
it is a digest and cannot be used to authenticate.
pub type SessionInfo {
SessionInfo(
id: String,
method: Method,
created_at: Int,
last_seen_at: Int,
expires_at: Int,
current: Bool,
client: String,
)
}
Constructors
-
SessionInfo( id: String, method: Method, created_at: Int, last_seen_at: Int, expires_at: Int, current: Bool, client: String, )Arguments
- client
-
Where the session was created from, as the transport reported it. Empty when it did not report one.
Values
pub fn account_deletion_enabled(auth: Auth) -> Bool
pub fn accounts_cookie_name(auth: Auth) -> String
The second cookie of a multi-session browser: every session token it holds,
the active one included, joined by .. It is as sensitive as the session
cookie and takes the same options.
pub fn add_device_session(
auth: Auth,
tokens: List(String),
session: Session,
client: String,
) -> List(String)
The tokens a browser should hold after signing in to session: the live
ones it had, then the new one. An earlier session of the same user is
revoked and replaced, and so are the oldest beyond the configured maximum.
pub fn allow_registration(auth: Auth) -> Auth
Public registration is disabled unless explicitly enabled.
pub fn approve_email_change(
auth: Auth,
principal: user.Principal,
secret: String,
) -> Result(Nil, service.Error)
Redeem the token sent to the current address, from the requesting session,
then email the confirmation token to the new one. Single-use even on a later
failure. Forbidden unless with_email_change_approval is configured.
pub fn authenticate(
auth: Auth,
secret: String,
) -> Result(user.Principal, service.Error)
Verify a session token. The returned principal records no client, so audit
events it causes do not say where the request came from; prefer
authenticate_from, or required, which reads it from the request.
pub fn authenticate_from(
auth: Auth,
secret: String,
client: String,
) -> Result(user.Principal, service.Error)
As authenticate, recording the requesting client on the principal so the
operations it performs are audited with it.
pub fn begin_mfa(
auth: Auth,
principal: user.Principal,
method: MfaMethod,
) -> Result(MfaSetup, service.Error)
Begin enrollment; it is NOT enabled until the code is confirmed. The key and URI are secrets; render locally, never through a third-party QR service.
pub fn begin_passkey_login(
auth: Auth,
) -> Result(PasskeyChallenge, service.Error)
pub fn begin_passkey_registration(
auth: Auth,
principal: user.Principal,
name: String,
) -> Result(PasskeyChallenge, service.Error)
pub fn begin_passkey_signup(
auth: Auth,
email: String,
name: String,
) -> Result(PasskeyChallenge, service.Error)
Register a new account with a passkey and no password. The ceremony runs
first, signed out; finish_passkey_signup then emails a Registration
token, and the account and its passkey are created together when that token
is exchanged. As everywhere else, no account exists before its address is
verified, and the reply never says whether the address already has one.
Nothing is excluded from the ceremony, for the same reason.
pub fn begin_provider(
auth: Auth,
id: String,
callback_path: String,
client: String,
) -> Result(ProviderStart, service.Error)
Begin browser sign-in. callback_path is trusted application configuration,
never a request parameter. The attempt captures the current group selection.
pub fn begin_provider_link(
auth: Auth,
principal: user.Principal,
id: String,
callback_path: String,
) -> Result(ProviderStart, service.Error)
Linking requires a live, recently created session. The callback must present that same session as well as the browser token and Google’s response.
pub fn begin_sso(
auth: Auth,
connection_id: String,
callback_path: String,
client: String,
) -> Result(ProviderStart, service.Error)
Begin browser sign-in through an SSO connection, as begin_provider does
for a built-in provider. The user signs in to the connection’s group.
pub fn begin_sso_link(
auth: Auth,
principal: user.Principal,
connection_id: String,
callback_path: String,
) -> Result(ProviderStart, service.Error)
As begin_provider_link. Only a member of the connection’s group can link
to it.
pub fn change_password(
auth: Auth,
principal: user.Principal,
current current: String,
new new: String,
) -> Result(Nil, service.Error)
Replace the caller’s password by proving the current one. Unlike
set_password this needs no fresh email login, so it also serves
deployments without email tokens. A wrong current password is Invalid, not
Unauthorized: the session itself is still good. Guesses spend the same
per-client and per-address budget as password logins, so a borrowed session
cannot search for the password faster than the login form could. An account
without a password receives Forbidden and must use set_password.
Every other session and remembered device of the user is revoked, and the
address is sent a PasswordChanged notice on a best-effort basis.
pub fn change_password_from(
auth: Auth,
principal: user.Principal,
current current: String,
new new: String,
client client: String,
) -> Result(Nil, service.Error)
As change_password, isolating back-off by a trusted client identity; see
login_password_from.
pub fn check_origin(
auth: Auth,
ctx: context.Context(a),
) -> Result(Nil, service.Error)
Require an exact, single Origin. The configured origin is never inferred from Host or forwarded headers.
pub fn confirm_email_change(
auth: Auth,
principal: user.Principal,
secret: String,
) -> Result(Nil, service.Error)
Confirm the new mailbox from the requesting session. Revalidates freshness,
account/group state and uniqueness, then signs out every session, including
this one. A correctly bound token is single-use even on a later failure.
The old address is sent an EmailChanged notice on a best-effort basis.
pub fn confirm_mfa(
auth: Auth,
principal: user.Principal,
challenge: String,
code: String,
) -> Result(List(secret.Secret), service.Error)
Successful enrollment revokes all sessions. Save the returned recovery codes once, then sign in again using the new second factor.
pub fn cookie_name(auth: Auth) -> String
pub fn delete_account(
auth: Auth,
principal: user.Principal,
confirm_email confirm_email: String,
) -> Result(Nil, service.Error)
Delete the caller after a recent sign-in and explicit confirmation of their
current address. Disabled until with_account_deletion is configured.
Credentials, fields, provider links, sessions and authz assignments cascade;
pending challenges are removed, and audit records follow their own retention.
pub fn delete_passkey(
auth: Auth,
principal: user.Principal,
id: String,
) -> Result(Nil, service.Error)
pub fn delete_user(
auth: Auth,
user_id: String,
by actor: user.Actor,
) -> Result(Nil, service.Error)
Delete a user’s account as an operator: an erasure request, or a test
account in development. Privileged: authorize the caller first. Needs
with_account_deletion, whose callback cleans up application-owned rows
in the same transaction; without it, Forbidden. Suspended accounts can
be deleted. Every session and pending challenge ends with the account,
and the audit trail keeps user.deleted with the actor.
pub fn device_sessions(
auth: Auth,
tokens: List(String),
client: String,
) -> List(#(String, user.Principal))
The session tokens that still authenticate, in the order given, each with its principal. Anything else is dropped without error: expired, revoked, suspended or malformed entries are what a long-lived cookie accumulates.
pub fn disable_mfa(
auth: Auth,
principal: user.Principal,
) -> Result(Nil, service.Error)
pub fn email_change_approval_enabled(auth: Auth) -> Bool
pub fn email_codes_enabled(auth: Auth) -> Bool
pub fn email_tokens_enabled(auth: Auth) -> Bool
pub fn exchange(
auth: Auth,
secret: String,
) -> Result(Session, service.Error)
Consume a challenge, verify/create the account and issue a session. The token is spent first, in its own transaction: it can create at most one session even under concurrency, and an exchange that then fails (account exists, suspended, method disabled) does not leave it usable.
pub fn exchange_code_step(
auth: Auth,
email: String,
code: String,
client: String,
) -> Result(LoginStep, service.Error)
As exchange_step, redeeming the six-digit code from Delivery.code
with the address it was sent to. Forbidden unless with_email_codes.
A wrong code, or one already retired by three wrong guesses, is
Unauthorized; too many guesses at an address or from a client are
TooManyRequests, as for passwords. The emailed token still works.
pub fn exchange_from(
auth: Auth,
secret: String,
client: String,
) -> Result(Session, service.Error)
As exchange, recording the requesting client on the new session and on
the audit events the exchange writes.
pub fn exchange_step(
auth: Auth,
token: String,
client: String,
) -> Result(LoginStep, service.Error)
MFA-aware replacements for transports that previously called exchange_from and login_password_from. A SecondFactor token is NOT a session credential.
pub fn finish_passkey_login(
auth: Auth,
challenge: String,
credential: String,
client: String,
) -> Result(LoginStep, service.Error)
pub fn finish_passkey_registration(
auth: Auth,
principal: user.Principal,
challenge: String,
credential: String,
) -> Result(Nil, service.Error)
pub fn finish_passkey_signup(
auth: Auth,
challenge: String,
credential: String,
client: String,
) -> Result(Nil, service.Error)
Verify the new credential, then send the registration token. The passkey
works only after that token is exchanged; for an address that already has an
account it is discarded and the email says AlreadyRegistered. Subject to
the same per-address cooldown as password registration.
pub fn finish_provider(
auth: Auth,
id: String,
callback_path: String,
state: String,
browser_token: String,
code: option.Option(String),
principal: option.Option(user.Principal),
) -> Result(ProviderOutcome, service.Error)
Consume a browser-bound attempt, verify the external identity, and apply
local policy. None code means cancelled/denied consent and still spends the
attempt. Custom transports must enforce request limits. No Google token is
returned. Linking never changes the current session.
pub fn finish_sso(
auth: Auth,
connection_id: String,
callback_path: String,
state: String,
browser_token: String,
code: option.Option(String),
principal: option.Option(user.Principal),
) -> Result(ProviderOutcome, service.Error)
As finish_provider. An identity the connection has not seen before
becomes a new account in the connection’s group when its address is in the
connection’s domains, whether or not public registration is enabled. It is
never attached to an existing account: that takes begin_sso_link.
pub fn group_mode(auth: Auth) -> group.Mode
pub fn impersonate(
auth: Auth,
user_id: String,
by actor: user.Actor,
) -> Result(Session, service.Error)
A session for user_id without a credential, for operators and
development tooling that act as a user. Privileged: authorize the caller
before invoking it, and never expose it over an unauthenticated route.
The session’s method is Impersonation, and the audit trail records
session.impersonated with the actor. It skips second factors and SSO
enforcement, which is why it must not be reachable by the user. Suspended
users cannot be impersonated.
pub fn in_group(auth: Auth, group_id: String) -> Auth
The same configuration, acting in one group. Token requests, registration
and password login made through the returned value apply to that group,
and it authenticates only that group’s users, so
auth.required(auth.in_group(identity, id)) guards a group’s routes.
AccountPerGroup needs this for every sign-in and registration, because
an address alone does not name an account. OneGroupPerUser needs it to
register, which is how a new user’s group is chosen; signing in works
without it. Single never needs it. It costs nothing: call it per request.
pub fn in_locale(auth: Auth, locale: String) -> Auth
The same configuration, acting for a request in locale, a BCP 47 tag.
An account registered through the returned value remembers the locale,
and emails sent through it use the locale unless their recipient has one
of their own. The bundled routes call this for every request; call it
per request in routes of your own.
pub fn linked_providers(
auth: Auth,
principal: user.Principal,
) -> Result(List(#(String, String)), service.Error)
The caller’s linked providers as #(provider id, issuer). Subjects and provider tokens are not disclosed. Also includes disabled providers so obsolete links can be removed using a different enabled sign-in method.
pub fn locale_source(
auth: Auth,
) -> fn(request.Request(context.Body)) -> option.Option(String)
A source for locale.from that answers the signed-in user’s own
locale, from their session cookie. Bearer tokens are not read: API
clients say what they want with accept-language.
pub fn login_password(
auth: Auth,
email: String,
password: String,
) -> Result(Session, service.Error)
Authenticate by email and password. Wrong, unknown, email-only and suspended
accounts all receive Unauthorized. This compatibility entry point uses a
shared headless client bucket. Custom transports should isolate clients with
login_password_from and enforce their own overall client/load limits.
pub fn login_password_from(
auth: Auth,
email: String,
password: String,
client: String,
) -> Result(Session, service.Error)
Use a trusted, stable client identity (normally the socket IP or an ingress header) to isolate password back-off. The compatibility wrapper shares a headless bucket; custom transports should call this operation instead.
pub fn login_password_step(
auth: Auth,
email: String,
password: String,
client: String,
) -> Result(LoginStep, service.Error)
pub fn logout(
auth: Auth,
principal: user.Principal,
) -> Result(Nil, service.Error)
pub fn mfa_delivery_enabled(auth: Auth) -> Bool
pub fn mfa_enabled(auth: Auth) -> Bool
pub fn mfa_status(
auth: Auth,
principal: user.Principal,
) -> Result(option.Option(String), service.Error)
pub fn mfa_trust_renewal(auth: Auth) -> Bool
Whether using a remembered device restarts its lifetime; a transport then
re-sets the device cookie with a fresh mfa_trust_seconds max-age.
pub fn mfa_trust_seconds(auth: Auth) -> Int
Remembered-device lifetime in seconds, for transports setting the cookie.
pub fn multi_session(auth: Auth) -> option.Option(Int)
The most accounts one browser may hold, when multi-session is on.
pub fn new(
repo repo: repo.Repo,
origin origin: String,
deliver deliver: fn(Delivery) -> Result(Nil, Nil),
) -> Result(Auth, service.Error)
Accept an already-configured Gloo Repo; the application owns its lifecycle. Construct once after running migrations. HTTPS is required except for loopback development origins. Origin must contain only scheme/host/port.
pub fn new_without_email(
repo repo: repo.Repo,
origin origin: String,
) -> Result(Auth, service.Error)
Construct auth without email tokens. Add built-in providers with
with_provider. Registration remains disabled until explicitly enabled.
pub fn passkey_signup_enabled(auth: Auth) -> Bool
Whether begin_passkey_signup is available: passkeys, registration and
email tokens must all be enabled.
pub fn passkeys(
auth: Auth,
principal: user.Principal,
) -> Result(List(passkey.Passkey), service.Error)
pub fn passkeys_enabled(auth: Auth) -> Bool
pub fn passwords_enabled(auth: Auth) -> Bool
pub fn policy(auth: Auth) -> policy.Policy
pub fn providers(auth: Auth) -> List(#(String, String))
The enabled provider IDs and display names, for custom sign-in pages.
pub fn provision(
auth: Auth,
email: String,
by actor: user.Actor,
) -> Result(user.User, service.Error)
Privileged operation: create a user without asking them, for an
invitation, an import or a directory sync. Authorize the caller first. It
works whether or not public registration is enabled, and sends nothing:
the user signs in with a login token when they are ready, which is also
what proves the address is theirs. The group is chosen as for
registration, so outside Single use auth.in_group:
auth.provision(auth.in_group(identity, "acme"), email, by: user.System)
Conflict when the address already has an account where it must be unique.
pub fn provision_with(
auth: Auth,
email: String,
fields changes: List(field.Change),
by actor: user.Actor,
) -> Result(user.User, service.Error)
As provision, setting fields on the new user in the same transaction;
see howdy/auth/field. Conflict when a unique value is taken.
pub fn prune_events(
auth: Auth,
before before: Int,
) -> Result(Nil, service.Error)
Apply an audit retention period: delete events that occurred before the given Unix time in seconds. Export them first if they must be kept.
pub fn prune_expired(auth: Auth) -> Result(Nil, service.Error)
Housekeeping for a scheduled job. Expired rows are already removed as a side effect of normal traffic; this covers quiet installations.
pub fn rate_limit_store(auth: Auth) -> rate_limit.Store
The auth database as a rate_limit.Store, for sharing the application’s
own limiters between nodes too. Give each rate_limit.shared limiter a
name not starting howdy_auth..
pub fn regenerate_recovery_codes(
auth: Auth,
principal: user.Principal,
) -> Result(List(secret.Secret), service.Error)
pub fn register_password(
auth: Auth,
email: String,
password: String,
) -> Result(Nil, service.Error)
Register with an email address and password. The email challenge must be
exchanged before the account or credential becomes usable. Never replaces
an existing user’s credential or attaches one based only on email equality.
Shares one headless client bucket; prefer register_password_from.
pub fn register_password_from(
auth: Auth,
email: String,
password: String,
client: String,
) -> Result(Nil, service.Error)
As register_password, with the requesting client for throttling and audit.
pub fn registration_enabled(auth: Auth) -> Bool
pub fn rename_passkey(
auth: Auth,
principal: user.Principal,
id: String,
name: String,
) -> Result(Nil, service.Error)
pub fn request_email_change(
auth: Auth,
principal: user.Principal,
email: String,
) -> Result(Nil, service.Error)
Request proof of the new mailbox, or first of the current one under
with_email_change_approval. Requires an enabled email delivery flow and a
recent, live sign-in. Every later step must use the same session.
Only a digest is stored; the email change token cannot sign anyone in.
pub fn request_locale(
auth: Auth,
ctx: context.Context(a),
) -> locale.Locale
The locale with_locales chooses for a request.
pub fn request_token(
auth: Auth,
email: String,
intent: Intent,
) -> Result(Nil, service.Error)
Headless operation for custom UI or another transport. Delivery is
identical for known and unknown addresses; account eligibility is checked
at exchange. This shares one client bucket with every other headless
caller; pass the request’s client to request_token_from instead.
pub fn request_token_from(
auth: Auth,
email: String,
intent: Intent,
client: String,
) -> Result(Nil, service.Error)
As request_token, with the requesting client for throttling and audit.
A request that finds a usable token already waiting for that address sends
no second email and still reports success, because one is already in the
inbox. That is deliberate: it means a third party asking for tokens can
neither flood the inbox nor stop its owner from receiving one, since every
request leaves the owner holding exactly one live token. Tune the window
with policy.email_coalesce_margin_seconds.
pub fn required(
auth: Auth,
) -> fn(context.Context(a)) -> Result(
user.Principal,
service.Error,
)
A typed controller/endpoint guard. Ambiguous credentials are rejected.
Cookie-authenticated writes require the configured Origin, protecting
application routes as well as the module’s own routes against CSRF.
Audit events record the socket address; behind a proxy that is the proxy
for everyone, so use required_from with the header your ingress sets.
pub fn required_from(
auth: Auth,
key: fn(context.Context(a)) -> option.Option(String),
) -> fn(context.Context(a)) -> Result(
user.Principal,
service.Error,
)
As required, identifying the client with key. Only read a header your
ingress overwrites; a client-supplied value can claim to be anyone.
pub fn reseal_mfa(auth: Auth) -> Result(Int, service.Error)
Re-encrypt every stored authenticator secret, and every enrollment still in
progress, with the key given to mfa.new: the last step of a rotation (see
mfa.with_decryption_keys). Returns how many needed it; once every node
seals with the new key, a rerun returns 0 and the old key can be dropped.
Safe to run while serving. A secret no configured key opens stops it with
an error naming the user, since dropping any key would not change that.
pub fn resume(
auth: Auth,
user_id: String,
by actor: user.Actor,
) -> Result(Nil, service.Error)
Privileged operation: authorize the caller before invoking it.
pub fn revoke_session(
auth: Auth,
principal: user.Principal,
session_id: String,
) -> Result(Nil, service.Error)
Revoke one of the caller’s own sessions by SessionInfo.id. Another
user’s session is never affected; an unknown id is not an error.
pub fn revoke_session_of(
auth: Auth,
user_id: String,
session_id: String,
by actor: user.Actor,
) -> Result(Nil, service.Error)
Revoke one of a user’s sessions by SessionInfo.id, as an operator.
Privileged: authorize the caller first. An unknown id is not an error.
pub fn revoke_sessions(
auth: Auth,
user_id: String,
by actor: user.Actor,
) -> Result(Nil, service.Error)
Privileged operation: authorize the caller before invoking it.
pub fn revoke_trusted_device(
auth: Auth,
principal: user.Principal,
id: String,
) -> Result(Nil, service.Error)
pub fn schema() -> migration.Package
pub fn send_mfa_code(
auth: Auth,
challenge: String,
) -> Result(Nil, service.Error)
Optional delivered-code fallback. Refused after email-token primary proof, so the same inbox cannot serve as both authentication factors.
pub fn session_cookie_seconds(auth: Auth) -> Int
Max-Age for the session cookie. Without renewal it matches the session. With
it the cookie must outlive any one expiry, so it lasts to
policy.session_max_seconds, or the 400 days browsers allow when there is
no ceiling. The cookie only carries the token; expiry is decided here.
pub fn sessions(
auth: Auth,
principal: user.Principal,
) -> Result(List(SessionInfo), service.Error)
The caller’s own live sessions, newest first.
pub fn sessions_of(
auth: Auth,
user_id: String,
) -> Result(List(SessionInfo), service.Error)
A user’s live sessions, newest first, for operators and management
consoles. Privileged: authorize the caller first. No session is marked
current, since the caller is not the user.
pub fn set_locale(
auth: Auth,
principal: user.Principal,
locale tag: option.Option(String),
) -> Result(Nil, service.Error)
Set, or with None clear, the locale the signed-in user’s pages and
emails use. A tag the negotiator from with_locales does not support
is refused.
pub fn set_password(
auth: Auth,
principal: user.Principal,
password: String,
) -> Result(Nil, service.Error)
Set, replace or reset the caller’s password. One operation covers all three
because the proof is the same: the session must have been created by an
email-token exchange within policy.fresh_session_seconds. A password
session or an older session receives Forbidden; request a login token,
exchange it, then call this. Every other session of the user is revoked.
pub fn sso_enabled(auth: Auth) -> Bool
pub fn sso_for_email(
auth: Auth,
email: String,
) -> Result(option.Option(String), service.Error)
The enabled connection that serves an address’s domain, for a sign-in page that asks for the address first. Which domains use SSO is not a secret: the redirect that follows reveals it to anyone who asks.
pub fn suspend(
auth: Auth,
user_id: String,
by actor: user.Actor,
) -> Result(Nil, service.Error)
Privileged operation. Suspension revokes all sessions atomically; resuming the user never restores old sessions.
pub fn trusted_devices(
auth: Auth,
principal: user.Principal,
) -> Result(List(#(String, Int, Int)), service.Error)
pub fn unlink_provider(
auth: Auth,
principal: user.Principal,
issuer: String,
) -> Result(Nil, service.Error)
Remove a provider by issuer. First sign in recently with a DIFFERENT enabled method; that proves an alternative works, and prevents last-method lockout. All sessions are revoked, including this one. A missing own link is NotFound.
pub fn use_trusted_device(
auth: Auth,
challenge: String,
device: String,
) -> Result(Session, service.Error)
Redeem a remembered device only AFTER a successful primary login challenge.
Tokens are user- and generation-bound and last as long as
mfa.with_device_trust says (30 days by default), restarting on each use
only when renewal is enabled.
pub fn verify_mfa(
auth: Auth,
challenge: String,
method: MfaMethod,
code: String,
remember: Bool,
) -> Result(MfaSession, service.Error)
pub fn with_account_deletion(
auth: Auth,
before_delete: fn(repo.Repo, user.User) -> Result(
Nil,
service.Error,
),
) -> Auth
Enable self-service deletion. The callback runs inside the deletion transaction with its Repo and current user. Clean up application-owned rows there, or return an error to veto deletion. Use only that Repo, not another auth operation or external side effect. Foreign-key failures roll back all database changes. Auth audit events are retained under the retention policy.
pub fn with_email_change_approval(auth: Auth) -> Auth
Require the CURRENT mailbox to approve an email change before the new one
is asked to confirm it. Without this, a hijacked fresh session can move the
account to a mailbox the attacker controls; with it, they also need the
owner’s inbox. request_email_change then emails an EmailChangeApproval
token to the current address, and approve_email_change continues to the
usual EmailChange token. A user who has lost the old mailbox cannot change
address by themselves; trusted administration still can.
pub fn with_email_codes(auth: Auth) -> Auth
Email a six-digit code beside each sign-in or registration token, for a
reader who would rather type than paste; see exchange_code_step. A code
is weaker than a token, so it only works with its address, dies after
three wrong guesses, and every guess counts against the same per-address
and per-client limits as a password. Anyone who can read the auth database
can recover a live code from its digest by trying all million, which a
token’s digest does not allow; leave codes off if that matters.
pub fn with_email_links(
auth: Auth,
at path: String,
) -> Result(Auth, service.Error)
Put a link in each emailed token’s Delivery, beside the token, to the
starter pages mounted at this path, such as “/auth”. A sign-in link opens
<path>/login; an email-change link opens <path>/account. The token
travels in the URL fragment (#token=, #email-confirm= or
#email-approve=), which browsers never send to a server, so it stays out
of access logs and Referer headers. The pages fill it in and wait for a
click, so a mail scanner that opens links spends nothing. Custom pages at
that path can read the same fragments.
pub fn with_email_tokens(
auth: Auth,
deliver deliver: fn(Delivery) -> Result(Nil, Nil),
) -> Auth
Enable email tokens on a runtime constructed without email.
pub fn with_groups(
auth: Auth,
mode: group.Mode,
) -> Result(Auth, service.Error)
Choose how users relate to groups; see howdy/auth/group. Construct once
at startup, like the rest of the configuration:
let assert Ok(identity) = auth.new(repo:, origin:, deliver:)
let assert Ok(identity) = auth.with_groups(identity, group.OneGroupPerUser)
The choice is recorded in the database, and an installation that never
calls this keeps whatever was recorded, Single to begin with. Choosing a
different mode converts the installation in one transaction, or refuses
with Conflict when the users already there do not fit it: Single needs
everyone in the default group, and leaving AccountPerGroup needs no
address to have accounts in two groups. Sessions survive a conversion;
emailed tokens not yet redeemed do not. Change modes with every other
node stopped, since a running node keeps the mode it started with.
pub fn with_locales(
auth: Auth,
negotiator: locale.Negotiator,
) -> Auth
Write the bundled pages and emails in the locales negotiator supports,
detecting each request’s locale the way it says. Without this they are
in English. Translations ship for en, ar, de, es, fr, it, ja, nl, pl, pt
and zh; any other supported locale falls back along its parents, and
then to English.
let locales =
locale.negotiator(["en", "de", "fr"], default: "en")
|> locale.from(auth.locale_source(identity))
|> locale.from_cookie("locale")
|> locale.from_accept_language
let identity = auth.with_locales(identity, locales)
A new account remembers the locale of the request that registered it,
and its emails use that locale from then on; see set_locale.
pub fn with_mfa(auth: Auth, config: mfa.Config) -> Auth
All nodes need the same stable encryption key. Removing this configuration never bypasses MFA for an enrolled account; those sign-ins fail closed.
pub fn with_multi_session(
auth: Auth,
max max: Int,
) -> Result(Auth, service.Error)
Let one browser hold several signed-in accounts and switch between them.
Signing in while signed in then adds an account instead of replacing the
session; signing out returns to another account that is still signed in.
max accounts per browser, from 2 to 10: signing in to one more signs the
oldest out. Only the cookie transport changes. Every account remains an
ordinary session that is listed, renewed, expired and revoked as any other,
and bearer clients are unaffected.
pub fn with_passkey_relying_party(
auth: Auth,
id id: String,
origins origins: List(String),
) -> Result(Auth, service.Error)
Share passkeys across subdomains. id is the RP ID: the public origin’s
hostname or a parent domain of it, such as example.com for
https://app.example.com. origins lists further HTTPS origins under that
domain whose ceremonies finish_passkey_registration and
finish_passkey_login also accept; the public origin always is. Call after
with_passkeys.
Browsers refuse a public suffix (com, co.uk) as RP ID; that is not
checked here. A passkey is bound to the RP ID it was created under, so
changing it later strands every existing passkey: choose it before launch.
The bundled JSON routes still answer only the public origin, so another
origin needs its own deployment or transport calling these functions.
pub fn with_passkeys(
auth: Auth,
relying_party_name: String,
) -> Result(Auth, service.Error)
Passkeys use the exact public-origin hostname as RP ID unless
with_passkey_relying_party says otherwise, and require user verification
(PIN/biometric). Enable once at startup with the displayed name.
pub fn with_password_check(
auth: Auth,
check: fn(String) -> Result(Nil, service.Error),
) -> Auth
Supplement the built-in common-password check with a local breached-password corpus or a privacy-preserving service. Receives NFC-normalized new passwords only. Errors fail closed; never log this argument or send plaintext remotely.
pub fn with_password_hasher(
auth: Auth,
hasher: password_hasher.PasswordHasher,
) -> Result(Auth, service.Error)
Enable passwords as with_passwords does, storing them with another
algorithm. Existing credentials must be readable by hasher: Howdy does
not fall back to the built-in one.
pub fn with_passwords(auth: Auth) -> Result(Auth, service.Error)
Enable password registration and login, hashed with PBKDF2-HMAC-SHA256
(see howdy/auth/password_hasher). Construct once at startup. Email
tokens remain available; this is an additional method, not a second factor.
pub fn with_policy(
auth: Auth,
policy: policy.Policy,
) -> Result(Auth, service.Error)
Replace the default limits and lifetimes. See howdy/auth/policy.
pub fn with_provider(
auth: Auth,
provider: provider.Provider,
) -> Result(Auth, service.Error)
Install a built-in provider once at startup. Duplicate IDs are rejected.
pub fn with_rate_limit_store(
auth: Auth,
store: rate_limit.Store,
) -> Auth
As with_shared_rate_limits, counting in a store of your own, such as
Redis, instead of the auth database.
pub fn with_session_store(
auth: Auth,
store: session_store.SessionStore,
) -> Auth
Keep sessions in a store of your own instead of the auth database; see
howdy/auth/session_store. Construct once at startup. Sessions are not
copied between stores, so changing store signs everyone out.
Users, credentials and suspension stay in the database, and every request
still confirms there that the session’s user is active, so a suspended
user is refused even if the store still holds their sessions. What changes
is atomicity. The database commits first and the store is written second,
so if the store fails in between, the operation returns its error with
the database change already made: a registration or sign-in without a
session (request another token), or a suspension, revoke_sessions or
set_password whose sessions are not yet removed (call it again, or
revoke_sessions).
pub fn with_shared_rate_limits(auth: Auth) -> Auth
Count the per-client limits of every auth route (routes.api,
routes.providers, routes.sso) in the auth database, so that nodes
behind a load balancer share them and a restart does not reset them. Each
limited request then costs one write. Without this, each node counts on
its own. Needs migration 17. Password, code and second-factor guessing
limits are always shared; this is the coarser per-client ceiling in front.
pub fn with_sso(auth: Auth, config: connection.Config) -> Auth
Enable enterprise single sign-on connections; see howdy/auth/connection.
Construct once at startup, then manage them with howdy/auth/connections.