---
title: Layer 1 — Authentication rules
description: Configure which authentication methods an application accepts, and
  how to narrow the choice on a per-inquiry basis.
editUrl: true
head: []
template: doc
sidebar:
  order: 2
  hidden: false
  attrs: {}
pagefind: true
draft: false
---

import { CardGrid, LinkButton, LinkCard } from "@astrojs/starlight/components";

:::tip[Part of the three-layer rules model]
Layer 1 is one of three rule layers. The overview explains allowlist + default-deny, evaluation order, and how the layers compose.

<LinkButton href="/en-us/application-rules/overview/" variant="secondary" icon="left-arrow">Read the overview</LinkButton>
:::

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

| 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](#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](/en-us/native/overview/) 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](/en-us/native/overview/) 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](/en-us/domains-federation/federation-connectors/). 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](/en-us/domains-federation/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](/en-us/domains-federation/domain-login-policy/)), never named in the rule. An application without this rule rejects an SSO-gated user. See [Sign in with your IdP](/en-us/domains-federation/sign-in-with-your-idp/). |

See [Native clients](/en-us/native/overview/) for how `STEAM_TICKET` and `ACCESS_KEY_DIRECT` are consumed end-to-end, and the [Domains & federation](/en-us/domains-federation/overview/) section for the two enterprise-federation methods.

## Application rule shape

Every Layer 1 rule on an application records one method. To allow multiple methods, create multiple rules.

```json
{
  "method": "PASSKEY_REASONED",
  "payload": {},
  "accessTokenTtlSeconds": null,
  "refreshTokenTtlSeconds": null
}
```

For `STEAM_TICKET`, the payload carries the App ID allowlist:

```json
{
  "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](/en-us/application-rules/overview/#token-ttls) at token-issuance time.

## Narrowing on `/establish`

The `authenticationConstraints` field on `/establish` carries the same shape and narrows the choice for a single inquiry:

```json
{
  "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

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

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`:

```json
{
  "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**.

## Related

<CardGrid>
<LinkCard
    title="The three-layer rules model"
    description="The overall picture — allowlist + default-deny, evaluation order, and how the three layers compose."
    href="/en-us/application-rules/overview/"
/>
<LinkCard
    title="Layer 2 — Realize rules"
    description="The post-authentication identity check, using email allowlists."
    href="/en-us/application-rules/realize-rules/"
/>
<LinkCard
    title="Layer 3 — Return rules"
    description="How the realized session is delivered back to the application."
    href="/en-us/application-rules/return-rules/"
/>
</CardGrid>