Authentication & OIDC
All protocol endpoints live under the single versioned prefix /auth/v1
(the bare prefix and unknown paths redirect permanently to the entry path).
The default tenant’s issuer derives from configuration as
<scheme>://<public_url>/auth/v1/. An additional tenant uses its own
tenant-prefixed issuer, or its own hostname (see
Tenant hostnames). When [federation] enabled = true, each registered
OIDC provider exposes an upstream callback at
/auth/v1/{tenant}/providers/{provider}/callback (omit {tenant} for the
default tenant). Copy the exact URI from the provider editor; see
Federation for current capabilities and limitations.
Discovery
Section titled “Discovery”The discovery document is served where OpenID Connect clients look for it,
at the issuer’s own address (<issuer>/.well-known/openid-configuration),
and at the host root:
| Path |
|---|
/auth/v1/.well-known/openid-configuration |
/auth/v1/.well-known/oauth-authorization-server |
/.well-known/openid-configuration |
/.well-known/oauth-authorization-server |
/.well-known/oauth-authorization-server/auth/v1 |
/.well-known/oauth-authorization-server/auth/v1/ |
A tenant served on its own path has its document at
/auth/v1/<tenant>/.well-known/openid-configuration.
With [fedcm] enabled = true, the browser-facing web-identity discovery
document is additionally served at /.well-known/web-identity.
| Flow | Entry points |
|---|---|
| Authorization code + PKCE | GET /auth/v1/oidc/authorize → login page → POST credential submission → code → POST /auth/v1/oidc/token |
| Silent re-authentication | POST /auth/v1/oidc/authorize/refresh |
| Client credentials | POST /auth/v1/oidc/token (client authentication) |
| Refresh | POST /auth/v1/oidc/token (refresh token; device-bound sets for the device grant) |
| Device (RFC 8628) | POST /auth/v1/oidc/device → verification page → POST /auth/v1/oidc/device/verify → poll token |
The console’s own sign-in uses the authorization-code flow with PKCE (S256),
a nonce, a fixed minimal scope set, and the fixed admin-console client
identity — the same machinery any relying party uses.
Device authorization grant
Section titled “Device authorization grant”POST /auth/v1/oidc/device starts the grant and returns a device code plus
the user code shown to the human operator; the bundled verification page and
the decision endpoint (POST /auth/v1/oidc/device/verify) complete it. The
token endpoint is polled at the advertised poll_interval_secs (default 5).
Device refresh tokens are device-bound with a dedicated lifetime
(refresh_lifetime_hours, default 72 h); a user’s devices can be listed,
renamed, and their refresh capability revoked through
/auth/v1/users/{id}/devices.
Tuning lives in the [device_grant] section (code lifetime 300 s, user-code
length 8, optional per-address start rate limit).
Signing algorithms
Section titled “Signing algorithms”Every deployment holds one signing key per algorithm and publishes all of them
at /auth/v1/oidc/certs (the jwks_uri), so a client can choose the algorithm
its libraries handle best:
| Algorithm | Key type | Notes |
|---|---|---|
RS256 |
RSA 2048 | The default. Every OpenID Connect library supports it. |
ES256 |
EC P-256 | Smaller tokens and keys. Signatures are the fixed-width r || s form JWS requires, not DER. |
EdDSA |
OKP Ed25519 | Compact and fast, but not every library verifies it. |
RS384, RS512 |
RSA 3072, 4096 | For policies that ask for a longer digest. |
A client names its algorithms with access_token_signed_response_alg and
id_token_signed_response_alg (the console offers the same choices); one that
names none gets RS256. A token carries its algorithm and key id in the header,
and the key it names is in the published set. The first key of the set is always
the RS256 one, so a consumer that reads only the first key still works. The
platform standard asks every IdP to sign with RS256 or ES256 and every
relying party to accept both; this provider does both, and also verifies
ES256 ID tokens from upstream providers and ES256 DPoP proofs.
Token surface
Section titled “Token surface”| Endpoint | Method | Purpose |
|---|---|---|
/auth/v1/oidc/token |
POST |
Token issuance (all grants). |
/auth/v1/oidc/introspect |
POST |
Token introspection. |
/auth/v1/oidc/token/revoke |
POST |
Token revocation. |
/auth/v1/oidc/userinfo |
GET/POST |
Userinfo document. |
/auth/v1/oidc/forward_auth |
GET/POST |
Forward-authentication headers. |
/auth/v1/oidc/certs |
GET |
Published key set (JWKS). |
/auth/v1/oidc/certs/{kid} |
GET |
Published key by id. |
/auth/v1/oidc/rotate_jwk |
POST |
Rotate the signing keys (admin). |
Signing keys rotate on the [lifetimes] jwk_autorotate_cron schedule
(default 0 30 3 1 * * *, first day of each month) or manually through the
rotation endpoint; the rotation emits a JwksRotated event at the configured
level_jwks_rotate severity.
Sessions and the login page
Section titled “Sessions and the login page”GET /auth/v1/oidc/authorize validates the client, resolves steering,
creates a preliminary session, and renders the login page with the session
cookie set. Credential submission (POST on the same path) is gated by a
proof-of-work challenge fetched from POST /auth/v1/pow — a plain-text
challenge that works exactly once, validated before any account or database
work.
| Endpoint | Method | Purpose |
|---|---|---|
/auth/v1/oidc/session |
POST |
Session bootstrap. |
/auth/v1/oidc/sessioninfo |
GET |
Session document. |
/auth/v1/oidc/sessioninfo/xsrf |
GET |
Session CSRF token re-issue. |
Login protection (per-address blocking and credential-stuffing detection) is
configured under [events.login_protection]; see
Security notes.
Signing in with an e-mail address
Section titled “Signing in with an e-mail address”When a deployment serves several tenants, people are sent to the right sign-in by their e-mail address. None of this applies to a deployment that serves one tenant: its login page stays as described above.
Passing the address in
Section titled “Passing the address in”An application that already knows who is signing in (because the person typed
their address on the application’s own page, for example) adds
login_hint=<address> to the authorization request. TinyGuard keeps it only
when it is an e-mail address: one @, at most 254 characters, no spaces,
control characters or angle brackets. Anything else is ignored and the sign-in
goes on as if no hint had been sent. The login page shows the address already
filled in, with Use another address.
When the person continues to an upstream identity provider, TinyGuard passes
the address on as login_hint, so the company’s sign-in page does not ask for
it again. It also adds domain_hint=<domain> for a Microsoft Entra ID
provider when the domain is a verified sign-in domain of the tenant, and
hd=<domain> for a Google provider with a Workspace domain set. Turn this off
for one provider with Forward sign-in hints in its editor. SAML providers
get no hints.
The e-mail step
Section titled “The e-mail step”A tenant with at least one verified sign-in domain asks for the address first:
- Email address and Continue. Sign in another way below them holds the tenant’s provider buttons and the passkey sign-in.
- What comes next follows the address’s domain: straight to the company’s identity provider, a choice between the providers the tenant lists for that domain and Use my password, or the password page.
Every address that is not at one of the tenant’s verified domains gets the
password page, whether or not an account exists for it, so the first step
reveals nothing about who has an account. A hint skips the first step. The
tenant setting sign_in.email_first is auto (the default: on while the
tenant has a verified domain) or off (the classic page with everything on
one screen). Client layouts keep working: each step renders inside the
auth-content slot.
An address at a verified domain whose target is a provider cannot register a password account or set a password through the reset page: the person is told “Use your company sign-in.” with a link to it. So a company’s employees always go through their company directory.
Finding the right tenant
Section titled “Finding the right tenant”People who do not know their tenant’s address start at the deployment’s
shared sign-in page,
/auth/v1/sign-in, which sends their address to the tenant that owns its
domain. A tenant with its own hostname
needs neither: its people sign in at that hostname directly.
Creating an account
Section titled “Creating an account”With registration open ([users] registration_enabled = true), the sign-in
page shows a Create an account link. It opens the registration page, which
asks for an e-mail address and a password (first and last name are optional;
a username too when [users] preferred_username_required = true). After
Create account the page says to confirm the e-mail address, and the
confirmation link arrives by e-mail. Back to sign-in returns to the sign-in
page the person came from. An address at a company domain gets “Use your
company sign-in.” with a button to it instead. With registration closed the
link is not shown and the registration page does not exist.
Once an address is confirmed (through the link, by an administrator, or by a
company directory that vouches for it), ID tokens and the userinfo answer carry
email_verified: true; until then they carry false.
Self-registered accounts confirm their address
Section titled “Self-registered accounts confirm their address”With registration open, a new account must confirm its e-mail address before it can sign in. Until then, the right password gets “Confirm your e-mail address first”, with Send the link again; a wrong password gets the usual answer, so nobody learns whether an unconfirmed account exists without knowing its password. A form linked from the login and registration pages also sends a new link, and always gives the same answer whatever the address. Each tenant chooses:
| Setting | Default | Meaning |
|---|---|---|
registration.require_email_confirmation |
on | Off lets new accounts sign in at once, as before. |
registration.confirmation_link_hours |
24 |
How long a confirmation link works (1 to 168). A new link cancels the earlier ones. |
registration.unconfirmed_account_days |
30 |
An account never confirmed is removed after this many days, which frees its address. 0 keeps it. |
registration.default_groups |
none | Groups every self-registered account joins, for example customers. |
Accounts created by an upstream identity provider or by an administrator are not affected. Administrators find these settings in the Users section and can resend the link or mark an address as confirmed.
Client-specific sign-in and sign-out screens
Section titled “Client-specific sign-in and sign-out screens”The console’s client detail has a Sign-in / sign-out layout tab and a
full-page screen editor. The same settings are
available at GET/PUT /auth/v1/clients/{id}/screens: theme (dark or
light), composition (centered or split), a six-digit hex
accent_color, headings and descriptions for both screens, and
show_client_logo. The optional logout_theme, logout_composition, and
logout_accent_color and logout_show_client_logo override the sign-out
screen independently; null inherits the sign-in theme, layout, accent, or
logo visibility. Older saved records continue to inherit these values.
login_template and logout_template may each contain
an optional custom declarative layout (maximum 8 KiB). For example:
<screen> <columns ratio="wide-left"> <panel tone="accent" align="center"> <client-logo/> <eyebrow>Member access</eyebrow> <heading size="large">Welcome back</heading> <text tone="muted">Use your company account to continue.</text> </panel> <auth-content/> </columns></screen>The only accepted elements are screen, columns, stack, panel,
eyebrow, heading, text, divider, optional client-logo, and exactly
one auth-content slot. The optional logo slot places the uploaded client
logo at that point rather than before the canonical form; it accepts no URL
or attributes and renders nothing when that screen hides the logo.
stack provides vertical spacing with gap set to small, medium, or
large; divider is empty and draws a design-system hairline. The slot inserts the canonical
authentication or logout form, including CSRF fields and browser logic.
Only enumerated presentation attributes are accepted: columns ratio can be
equal, wide-left, or wide-right; panel tone can be raised, plain, or
accent, with align left or center; heading size can be small,
medium, or large; and text tone can be normal or muted. Templates
cannot contain arbitrary HTML, CSS, attributes, links, scripts, or external
URLs; all heading/text content is escaped. This keeps client-authored layouts
from executing code on the authentication origin. The primary
action chooses black or white text from the configured accent’s luminance.
A login screen reads its client’s settings. A logout confirmation reads them
when the request includes client_id; otherwise
it uses the default theme. Screen settings are tenant-scoped and are removed
with the client.
The editor can preview unsaved sign-in and sign-out settings. Previews use
the server’s authentication-page renderer but contain inert controls, run in
a sandboxed frame, and do not change the saved client settings. The preview
API is administrator-only: POST /auth/v1/clients/{id}/screens/preview
with the draft settings and a screen value of login or logout.
Custom CSS
Section titled “Custom CSS”Each screen also accepts optional custom_css and logout_custom_css
fields (up to 32 KiB of input each; logout_custom_css inherits the
sign-in CSS when absent). This is real CSS parsed by a real CSS parser
(lightningcss) — never a regex — and only the sanitized, minified result is
stored and rendered; the save response returns exactly that sanitized form
plus a per-field count of removed rules, so designers always see what
survived.
What survives:
- Style rules, with every top-level selector rewritten to a descendant of
the authentication card root —
.custom-screenwhen that screen uses a custom template,.cardotherwise.html,:root, andbodytherefore match nothing: page-level theming stays owned by the palette. Pseudo- classes and pseudo-elements (including:has()) survive the rewrite. - Declarations of any other kind: colors, spacing, borders, typography,
animations, transitions, custom properties, and
@media,@supports,@container,@layer,@starting-style,@nest, and@keyframesbodies.
What is stripped, and why:
url(),image-set(),src(),@import, and@font-face— no network egress from an authentication page; imagery comes from the palette and the uploaded client logo.positionandz-index— client CSS must never overlay or displace the canonical authentication form.filter— itsurl()form leaks references.behavior,-moz-binding, andexpression()(legacy IE/JS vectors).- Any input the parser rejects is refused outright: the console shows the server’s refusal verbatim and nothing is saved.
Under a custom template the login page keeps its CSRF pairing token out of every attribute (it lives in the embedded JSON block the page driver reads), so custom CSS attribute selectors cannot read secrets; the hidden no-JS fallback field remains only on the preset layout.
Passkeys (WebAuthn)
Section titled “Passkeys (WebAuthn)”Ceremony timing and policy live in the [webauthn] section: challenge
expiry 60 s, pending ceremony lifetime 90 s, remembered second-factor trust
2160 h.
| Endpoint | Method | Purpose |
|---|---|---|
/auth/v1/users/{id}/webauthn |
GET |
List a user’s passkeys. |
/auth/v1/users/{id}/webauthn/register/start |
POST |
Passkey registration start. |
/auth/v1/users/{id}/webauthn/register/finish |
POST |
Passkey registration finish. |
/auth/v1/users/{id}/webauthn/auth/start |
POST |
Passkey authentication start (user-specific). |
/auth/v1/users/{id}/webauthn/auth/finish |
POST |
Passkey authentication finish (user-specific). |
/auth/v1/users/webauthn_start |
POST |
Passkey authentication start (login-wide). |
/auth/v1/users/webauthn_finish |
POST |
Passkey authentication finish (login-wide). |
/auth/v1/users/{id}/webauthn/delete/{name} |
DELETE |
Delete a passkey by name. |
/auth/v1/users/{id}/mfa_token |
POST |
Issue a modification token. |
/auth/v1/users/{id}/self/convert_passkey |
POST |
Convert the account to passkey-only. |
Passkey enrollment is also available through a magic-link branch backed by
the identity area’s registration links, and an account may be converted to
passkey-only (no password). With no_password_expiry_with_passkey = true,
registering a first passkey clears the account’s password expiry.
Logout
Section titled “Logout”| Endpoint | Method | Purpose |
|---|---|---|
/auth/v1/oidc/logout |
GET |
End-session page or hint-driven logout. |
/auth/v1/oidc/logout |
POST |
Logout confirmation and backchannel-logout receiver. |
Session termination can also be forced administratively through
/auth/v1/sessions (see the API reference).