Skip to content

Layer 1 — Authentication rules

View as Markdown

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.

MethodPayloadWhat 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.

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.

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.

An application has two Layer 1 rules: PASSKEY_REASONED and EMAIL_VERIFICATION. A particular admin inquiry passes authenticationConstraints: [{ "method": "PASSKEY_REASONED", "payload": {} }].

MethodApp allows?Inquiry allows?Result
PASSKEY_REASONEDyesyesoffered
EMAIL_VERIFICATIONyesnohidden

The user only sees passkey as an option for this session, even though the application itself would normally accept email too.

Passkey sign-in splits into two separate, independent Layer-1 methods rather than a single rule with a flag:

  • PASSKEY_REASONED is the email-first flow: the user types their email, /reason/email resolves their account, and they then prove possession of a registered credential.
  • PASSKEY_USERNAMELESS is 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_REASONED for the email-first option, PASSKEY_USERNAMELESS for the standalone button, or both. There is no longer an allowUsernameless flag.
  • Both methods resolve to the same shared PASSKEY credential row, so a credential registered through one flow is usable by the other.
  • Per-inquiry authenticationConstraints can carry PASSKEY_USERNAMELESS to 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_USERNAMELESS only controls whether the standalone button is offered for sign-in.