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 | { "allowedHostedDomains": string[] } | Sign-in via Google as an upstream OIDC provider. Leave allowedHostedDomains empty to allow any Google account. Add Google Workspace hosted domains, such as example.com, to require the user’s Google hd claim to match one of them. Consumer Gmail accounts do not carry hd, so they fail non-empty domain allowlists. |
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 | { "allowedDiscordGuilds": string[] } | Sign-in via Discord as an upstream OAuth 2.0 provider (no id_token; profile + email fetched from GET /users/@me). Leave allowedDiscordGuilds empty to allow any Discord account. Add Discord server IDs to require membership in at least one of them; gated apps request the guilds scope. An email is treated as verified only when Discord returns both a non-empty email and verified: true; otherwise Layer 2 EMAIL rules fail closed. |
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; applications that rely only on Layer 2 EMAIL rules reject Battle.net-only accounts. 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; applications that rely only on Layer 2 EMAIL rules reject X-only accounts. Empty payload; no per-application gating. |
ENTERPRISE_FEDERATION_APPLICATION_MANAGED | { "connectorAnchor": string } | ”Sign in with …” via an OIDC or SAML 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}For GOOGLE_OAUTH, use allowedHostedDomains when you only want Google Workspace users from specific domains:
{ "method": "GOOGLE_OAUTH", "payload": { "allowedHostedDomains": ["example.com"] }, "accessTokenTtlSeconds": null, "refreshTokenTtlSeconds": null}For DISCORD_OAUTH, use Discord server IDs, not server names:
{ "method": "DISCORD_OAUTH", "payload": { "allowedDiscordGuilds": ["974519864045756446"] }, "accessTokenTtlSeconds": null, "refreshTokenTtlSeconds": null}The two TTL fields are optional. When present, token issuance uses the shortest matching TTL.
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.