Layer 1 — Authentication rules
Layer 1 controls which authentication methods are usable for an application. It is checked both when the user is offered a list of methods (via /reason/email) and again at every actual authentication attempt.
Supported methods
Section titled “Supported methods”| Method | Payload | What it is |
|---|---|---|
PASSKEY_USERNAMELESS | {} | WebAuthn / FIDO2 passkey via the discoverable-credential / usernameless flow. Gates the single “Sign in with a passkey” button shown before any email is entered. Both passkey methods sign in with the same passkey the user registered — they differ only in which button surfaces it. See Usernameless passkey below. |
PASSKEY_REASONED | {} | WebAuthn / FIDO2 passkey via the email-first flow. Gates the passkey option offered after the user enters their email. |
EMAIL_VERIFICATION | {} | One-time code sent to the user’s email address. |
STEAM_TICKET | { "allowedSteamAppIds": number[] } | One-shot Steam ticket exchange from inside a Steam-distributed game, consumed by native-api POST /direct-issue/steam-ticket. The allowedSteamAppIds list scopes the rule to specific Steam App IDs. See Native clients for the end-to-end flow. |
STEAM_OPENID | {} | Browser-side “Sign in with Steam” button using Steam’s OpenID 2.0 OP. The Steam identity resolved here lives in the same per-user identity row as STEAM_TICKET, so a user who first signed in through a game can subsequently sign in through the web button (and vice versa). Sudomimus requires no client ID, client secret, or Web API key for this path — verification is keyless via openid.mode=check_authentication. |
ACCESS_KEY_DIRECT | {} | One-shot AccessKey credential login, consumed by native-api POST /direct-issue/access-key. See Native clients for the credential format and end-to-end flow. |
GOOGLE_OAUTH | {} | Sign-in via Google as an upstream OIDC provider. Sudomimus acts as the OIDC Relying Party. Payload is empty in Phase 1; a future phase will add allowedHostedDomains for Google Workspace gating. |
GITHUB_OAUTH | { "allowedGitHubOrgs": string[] } | Sign-in via GitHub as an upstream OAuth 2.0 provider (no id_token; profile + verified-email list fetched from the REST API). allowedGitHubOrgs is an exact-match list of GitHub Organization login strings (case-insensitive); empty array = no org gating (any GitHub account); non-empty = the user must belong to at least one listed org. The read:org scope is requested only when at least one matching rule has a non-empty allowlist — apps without org gating keep the minimal read:user user:email consent screen. |
DISCORD_OAUTH | {} | Sign-in via Discord as an upstream OAuth 2.0 provider (no id_token; profile + email fetched from GET /users/@me). Scopes are identify email. An email is treated as verified only when Discord returns both a non-empty email and verified: true — otherwise the account is created without a verified email on file, so Layer 2 EMAIL rules fail-closed (intentional, mirroring Google and GitHub). A future phase will add allowedDiscordGuilds for guild gating. |
BATTLENET_OAUTH | {} | Sign-in via Battle.net as an upstream OIDC-shaped OAuth provider. Battle.net’s /userinfo carries only a subject and a BattleTag — no email — so the account is created without a verified email on file and Layer 2 EMAIL rules fail-closed against a Battle.net-only account (intentional). Battle.net has no per-application gating concept, so the payload is always empty. |
X_OAUTH | {} | Sign-in via X (formerly Twitter) as an upstream OAuth 2.0 provider. X’s v2 /2/users/me exposes no email, so — like Battle.net and Steam — the account is created without a verified email on file and Layer 2 EMAIL rules fail-closed (intentional). Empty payload; no per-application gating. |
ENTERPRISE_FEDERATION_APPLICATION_MANAGED | { "connectorAnchor": string } | ”Sign in with …” via an OIDC identity provider your own organization registered as a federation connector. The connectorAnchor names a connector owned by the application’s organization; one rule renders one button. Login runs through the standard authentication and realize pipeline. See Sign in with your IdP. |
ENTERPRISE_FEDERATION_DOMAIN_MANAGED | {} | Opt the application in to accepting forced-SSO logins. Empty payload — the connector is resolved at login from the user’s email domain (a verified domain whose owner set an SSO_ONLY login policy), never named in the rule. An application without this rule rejects an SSO-gated user. See Sign in with your IdP. |
See Native clients for how STEAM_TICKET and ACCESS_KEY_DIRECT are consumed end-to-end, and the Domains & federation section for the two enterprise-federation methods.
Application rule shape
Section titled “Application rule shape”Every Layer 1 rule on an application records one method. To allow multiple methods, create multiple rules.
{ "method": "PASSKEY_REASONED", "payload": {}, "accessTokenTtlSeconds": null, "refreshTokenTtlSeconds": null}For STEAM_TICKET, the payload carries the App ID allowlist:
{ "method": "STEAM_TICKET", "payload": { "allowedSteamAppIds": [480, 730] }, "accessTokenTtlSeconds": null, "refreshTokenTtlSeconds": null}The two TTL fields are optional. When present they participate in the MIN fold at token-issuance time.
Narrowing on /establish
Section titled “Narrowing on /establish”The authenticationConstraints field on /establish carries the same shape and narrows the choice for a single inquiry:
{ "applicationAnchor": "my-app", "authenticationConstraints": [ { "method": "PASSKEY_REASONED", "payload": {} } ]}- Field absent → no narrowing; the application’s rules alone decide.
- Field present and empty array → rejected.
- Field present and non-empty → AND-combined with the application’s rules.
Worked example
Section titled “Worked example”An application has two Layer 1 rules: PASSKEY_REASONED and EMAIL_VERIFICATION. A particular admin inquiry passes authenticationConstraints: [{ "method": "PASSKEY_REASONED", "payload": {} }].
| Method | App allows? | Inquiry allows? | Result |
|---|---|---|---|
PASSKEY_REASONED | yes | yes | offered |
EMAIL_VERIFICATION | yes | no | hidden |
The user only sees passkey as an option for this session, even though the application itself would normally accept email too.
Usernameless passkey
Section titled “Usernameless passkey”Passkey sign-in splits into two separate, independent Layer-1 methods rather than a single rule with a flag:
PASSKEY_REASONEDis the email-first flow: the user types their email,/reason/emailresolves their account, and they then prove possession of a registered credential.PASSKEY_USERNAMELESSis the discoverable-credential flow: a single “Sign in with a passkey” button rendered at the top of the auth UI, before the email field. The user taps it, the browser shows its native passkey picker, the user picks a credential and verifies (biometric or PIN), and they are signed in without ever typing an email.
A “usernameless-only” application — one with no email box offering a passkey — is expressed simply by allowing only PASSKEY_USERNAMELESS:
{ "method": "PASSKEY_USERNAMELESS", "payload": {}}Notes:
- The two methods are independent rules with empty payloads. List
PASSKEY_REASONEDfor the email-first option,PASSKEY_USERNAMELESSfor the standalone button, or both. There is no longer anallowUsernamelessflag. - Both methods resolve to the same shared
PASSKEYcredential row, so a credential registered through one flow is usable by the other. - Per-inquiry
authenticationConstraintscan carryPASSKEY_USERNAMELESSto narrow a single inquiry to the discoverable-credential button (AND-combined with the application’s rules, like any other method). - The discoverable-credential flow requires the authenticator to set the User Verified (UV) flag (biometric / PIN). Usernameless login without user verification is rejected — there is no typed email to act as a second factor.
- Passkey registration itself is unaffected by these rules: users still register a passkey through the normal post-email-verification flow.
PASSKEY_USERNAMELESSonly controls whether the standalone button is offered for sign-in.