This is the full developer documentation for Sudomimus
# Sudomimus Documentation
> Authentication, detached from authorization.
## Or — hand it to your AI
[Section titled “Or — hand it to your AI”](#or--hand-it-to-your-ai)
Paste this prompt into Claude, Cursor, ChatGPT, or any AI tool with web access:
```text
Read https://docs.sudomimus.com/llms-full.txt and integrate Sudomimus
authentication into my application.
Stack:
Auth methods:
Application anchor: your-application
Callback URL:
```
The English documentation is published in a machine-readable form so an AI assistant can draft an integration from the current protocol guidance and contracts. Review and test the result against your application configuration. See [Build with AI](/en-us/ai/overview/) for the endpoints and tool-specific tips. When the assistant needs to operate Sudomimus directly, give it the [Sudomimus CLI](/en-us/ai/cli/) as a shell-friendly control surface.
Want to try it first? [Explore the interactive demos in Theater](https://theater.sudomimus.com/).
## What’s in these docs
[Section titled “What’s in these docs”](#whats-in-these-docs)
* **Getting Started** — what Sudomimus is and how to choose an integration path.
* **AI Integration** — machine-readable documentation and the CLI for assistants and automation.
* **Concepts** — accounts, organizations, pairwise identity, claim sharing, and token verification.
* **Application Rules** — the three-layer allowlist model that decides which auth methods, which identities, and which return paths your application accepts.
* **Connect / OIDC / Device / Native** — four separate, peer integration paths with their own protocol flows.
* **Common Operations** — session management and account deletion.
* **With Portal** — manage organizations, applications, sign-in methods, and access keys.
* **Domains & Federation** — adopt domains, set login policy, and connect an identity provider.
* **User-generated Content** — avatar uploads, review, and delivery to applications.
* **SDKs** - official SDK packages by language, plus the API reference for exact product API wire contracts.
* **Brand Resources** — approved marks, sign-in button guidance, motion rules, social cards, and application icon standards.
These docs are also available as raw Markdown for AI agents: append `.md` to any page URL (for example, [`/en-us/connect/three-key-model.md`](/en-us/connect/three-key-model.md)), or fetch [`/llms.txt`](/llms.txt) and [`/llms-full.txt`](/llms-full.txt) for the English corpus. Chinese pages are available through their own `.md` URLs.
# Driving your AI assistant
> Tool-specific tips for Claude, Cursor, ChatGPT and other assistants, plus how to get better integration code.
The general pattern is the same everywhere: give the assistant the [docs corpus](/en-us/ai/endpoints/), then describe what you want it to build. The details differ a little per tool.
## Claude Code / claude.ai
[Section titled “Claude Code / claude.ai”](#claude-code--claudeai)
Claude can fetch URLs directly — just include the URL in your message:
```text
Read https://docs.sudomimus.com/llms-full.txt then implement a complete
Sudomimus integration for my Next.js app. The application anchor is
your-application. Support passkeys and email OTP. Add routes for
/api/auth/establish, /api/auth/callback, and /api/auth/refresh.
```
## Cursor / Continue / similar editor tools
[Section titled “Cursor / Continue / similar editor tools”](#cursor--continue--similar-editor-tools)
Use your editor’s URL-attach feature (e.g. Cursor’s `@web`, Continue’s `@docs`) to attach `https://docs.sudomimus.com/llms-full.txt` to the conversation, then ask the same question.
## ChatGPT with web browsing
[Section titled “ChatGPT with web browsing”](#chatgpt-with-web-browsing)
```text
Fetch https://docs.sudomimus.com/llms-full.txt and write a Python Flask
backend that integrates Sudomimus. Use application anchor your-app and
store pending sessions in PostgreSQL.
```
## Let the assistant use the CLI
[Section titled “Let the assistant use the CLI”](#let-the-assistant-use-the-cli)
When the task involves your Sudomimus account or developer resources, the assistant should use the CLI instead of trying to drive the With portal through a browser. Give it a narrow instruction like:
```text
Use the Sudomimus CLI for Sudomimus account operations. Prefer JSON output,
for example `sudomimus whoami --json`. If the CLI is not logged in, stop and
ask me to run `sudomimus login --no-open`; do not read or print the local
credentials file.
```
See [Sudomimus CLI for AI agents](/en-us/ai/cli/) for the command surface, endpoint overrides, and security boundaries.
## Tips for better output
[Section titled “Tips for better output”](#tips-for-better-output)
* **Be specific about your stack.** “Node.js + Express” produces different code than “Bun + Hono”. The docs cover the protocol; you tell the assistant the surrounding context.
* **Name the authentication methods you need.** Sudomimus supports passkeys, email OTP, Steam, AccessKey, and OIDC; the appropriate choice depends on the product requirements.
* **Give it your application anchor, callback URL, and any env vars you have.** These values are project configuration and are not part of the general documentation.
* **Give it the matching machine-readable contract.** For Connect, Session, Native, and Device, use the [OpenAPI reference](/en-us/ai/endpoints/#the-openapi-reference). For OIDC, provide the issuer so its library can read discovery, plus the [OIDC integration guide](/en-us/oidc/flow/) for the supported profile.
* **Keep the conversation open for follow-ups.** The docs are already in context, so debugging or asking for variants is one more message away.
# Sudomimus CLI for AI agents
> Use the Sudomimus CLI as a shell-friendly control surface for AI coding assistants, local automation, and account checks.
The Sudomimus CLI is the best control surface when an AI assistant needs to operate Sudomimus directly. It gives the assistant normal shell commands instead of a browser session, while keeping authentication in the user’s browser through the device authorization flow.
Use it when the assistant needs to:
* confirm which Sudomimus account is active;
* refresh an expired CLI session without asking the user to sign in again;
* discover organizations and inspect their resource usage;
* list applications, inspect application configuration, and check signing-key lifecycle state;
* work in an editor, terminal, or CI-like agent environment where browser cookies are not available.
The CLI is for operating Sudomimus itself. If you are building your own CLI application that authenticates your users, choose the [Native integration path](/en-us/native/overview/) instead.
## Prerequisites and source checkout
[Section titled “Prerequisites and source checkout”](#prerequisites-and-source-checkout)
Use Node.js 26 or newer. For a source checkout, first complete the repository’s setup instructions and install its workspace dependencies with the repository’s configured pnpm version. Run the following commands from the repository root:
```bash
make dev-cli ARGS="--help"
make dev-cli ARGS="login --no-open"
```
The source entry builds the CLI before running it. Pass other command arguments through `ARGS` in the same way. The examples below use an already-installed `sudomimus` binary; they do not establish a package publication or installation baseline.
## First login
[Section titled “First login”](#first-login)
Installed CLI examples use the `sudomimus` binary:
```bash
sudomimus login
```
The CLI prints a verification URL and user code, opens the browser when possible, and waits for the device grant to complete. For a remote agent or terminal-only environment, keep the browser step manual:
```bash
sudomimus login --no-open
```
The local session is stored under `$SUDOMIMUS_CLI_HOME/credentials.json` when that environment variable is set, otherwise under `~/.sudomimus/credentials.json`. The file contains bearer credentials; an assistant should use the CLI, not read or print this file.
## Machine-readable output
[Section titled “Machine-readable output”](#machine-readable-output)
Commands that expose data to agents support JSON output:
```bash
sudomimus whoami --json
sudomimus org list --json
sudomimus org show --json
sudomimus app list --json
sudomimus app show --json
sudomimus app keys --json
```
List commands are cursor-paginated. Pass the returned opaque cursor unchanged when another page is available:
```bash
sudomimus org list --cursor --json
sudomimus app list --cursor --json
```
If the access token is near expiry, the CLI refreshes it through Session API before calling With. Developer commands are read-only and use the same organization membership and role checks as the With portal.
For prompts, tell the assistant to prefer CLI JSON output over scraping terminal prose:
```text
Use the Sudomimus CLI for Sudomimus account operations. If a command supports
--json, use it. If the CLI is not logged in, ask me to run `sudomimus login
--no-open` and paste the verification URL/code into my browser. Do not read or
print ~/.sudomimus/credentials.json.
```
## Endpoint overrides
[Section titled “Endpoint overrides”](#endpoint-overrides)
For local development, the CLI accepts explicit endpoint overrides:
```bash
sudomimus login \
--device-api-base-url https://sudomimus-device-api.your-domain.com \
--session-api-base-url https://sudomimus-session-api.your-domain.com \
--with-api-base-url https://sudomimus-with-api.your-domain.com
```
The same values can be supplied with environment variables:
```bash
SUDOMIMUS_DEVICE_API_BASE_URL=https://sudomimus-device-api.your-domain.com
SUDOMIMUS_SESSION_API_BASE_URL=https://sudomimus-session-api.your-domain.com
SUDOMIMUS_WITH_API_BASE_URL=https://sudomimus-with-api.your-domain.com
SUDOMIMUS_WITH_APPLICATION_ANCHOR=sudomimus-with
SUDOMIMUS_CLI_HOME=.sudomimus-cli
```
Endpoint overrides are useful for agents running on self hosted, or enterprise internal hosted sudomimus instance, because the assistant can point the CLI at any instance without changing stored production credentials.
`login` stores the endpoints that own the credential profile. On `whoami`, `org`, and `app` read commands, `--session-api-base-url` and `--with-api-base-url` apply only to that command. Even when the command refreshes and stores a rotated token pair, it keeps the login-time endpoints for the next command.
## Unattended work
[Section titled “Unattended work”](#unattended-work)
CLI sign-in still requires a user to approve the device code. Do not copy the CLI’s saved personal session to a server or pipeline for long-term use.
For a job that must run unattended, give it separate [programmatic access](/en-us/programmatic-access/overview/). Create an [agent](/en-us/programmatic-access/agents/) or [automation](/en-us/programmatic-access/automations/) that matches the way it works, then issue a dedicated credential.
# What to feed your AI
> The machine-readable endpoints — llms.txt, the small and full corpora, per-page Markdown, and the OpenAPI reference.
Sudomimus exposes its documentation in several machine-readable forms. Which one you hand your assistant depends on how much context window you can spare and whether you want prose or a formal API contract.
## The llms.txt endpoints
[Section titled “The llms.txt endpoints”](#the-llmstxt-endpoints)
Three plain-text endpoints follow the [llmstxt.org](https://llmstxt.org) convention:
| URL | What it is |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `https://docs.sudomimus.com/llms.txt` | **Index** — a short manifest linking to the two corpora below. Good for assistants that fetch pages selectively rather than all at once. |
| `https://docs.sudomimus.com/llms-full.txt` | **Full corpus** — every English documentation page concatenated into one file. |
| `https://docs.sudomimus.com/llms-small.txt` | **Small corpus** — an alternative generated representation of the English documentation; it is not a curated summary. |
The aggregate corpora are English-only. To give an assistant a Chinese page, use the `.md` URL for the matching page under any other language supported by this website.
### Choosing the amount of context
[Section titled “Choosing the amount of context”](#choosing-the-amount-of-context)
The current `llms-full.txt` and `llms-small.txt` outputs contain the same substantive English content, including detailed rule tables and edge-case guidance. Their byte sizes differ, but `small` does not provide a substantial reduction in subject matter. Do not infer token counts or a fixed size ratio from the filenames.
Use an aggregate corpus when the assistant needs broad documentation context and has enough capacity. When context is limited, start from `llms.txt` and retrieve the specific per-page Markdown needed for the task. Check the current output size against your model’s context budget; integration still requires configuration and validation.
## Per-page Markdown
[Section titled “Per-page Markdown”](#per-page-markdown)
Append `.md` to any documentation page URL to fetch just that page as raw Markdown — for example, [`/en-us/connect/three-key-model.md`](/en-us/connect/three-key-model.md). This is the precise way to give an assistant one page rather than the whole corpus.
## The OpenAPI reference
[Section titled “The OpenAPI reference”](#the-openapi-reference)
For the formal request/response contract of the Sudomimus product APIs, point your assistant at the **OpenAPI 3.1 reference**, generated directly from the published specifications:
* **[Connect API](/en-us/api/connect/)** — the Inquiry endpoints (`/establish`, `/status-poll`, `/redeem`, `/info`), each with its own operation page.
* **[Session API](/en-us/api/session/)** — the ordinary application session endpoints (`/refresh`, `/introspect`, `/logout`, `/revoke-all`).
* **[Native API](/en-us/api/native/)** — the direct-issue endpoints for native clients (`/direct-issue/steam-ticket`, `/direct-issue/access-key`, `/direct-issue/public-key`).
* **[Device API](/en-us/api/device/)** — the public-client device authorization endpoints (`/device-authorize`, `/device-token`).
These pages are authoritative for exact paths, parameters, request bodies, and status codes. Provide the relevant pages to an assistant when generating strongly typed client code.
OIDC uses its native machine-readable contract instead of a duplicate OpenAPI schema. Give your assistant the issuer `https://oidc.sudomimus.com`, let its OIDC library read discovery, and include the [OIDC integration guide](/en-us/oidc/flow/) for the supported Sudomimus profile and platform-specific constraints. The OpenID Connect and OAuth standards continue to define the underlying protocol semantics.
## Staying current
[Section titled “Staying current”](#staying-current)
The `llms*.txt` endpoints and the OpenAPI reference rebuild every time the documentation does — there is no separate “AI version” that lags behind. If your assistant has cached an older fetch, ask it to re-fetch.
# Build with AI
> Give your AI coding assistant the relevant Sudomimus contracts and use them to build and verify an integration.
Sudomimus’s documentation is published in a **machine-readable form** alongside the human-facing pages. If your editor or chat tool can fetch a URL, it can read the relevant protocol guidance and contract while helping you implement an integration without repeated manual copy and paste.
## A starting prompt
[Section titled “A starting prompt”](#a-starting-prompt)
Paste this into Claude, Cursor, ChatGPT, or any AI tool with web access, filling in the angle-bracketed parts:
```text
Read https://docs.sudomimus.com/llms-full.txt and integrate Sudomimus
authentication into my application.
Stack:
Auth methods:
Application anchor:
Callback URL:
```
The assistant can use the corpus to draft the integration. You still need to select the appropriate protocol, supply the surrounding project context, configure the application, review the generated code, and verify the flow against the published contract.
If the assistant needs to operate your Sudomimus account or developer settings while it works, give it the [Sudomimus CLI](/en-us/ai/cli/) instead of browser instructions. The CLI exposes login, session, and account commands in a form an agent can call directly.
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* **[What to feed your AI](/en-us/ai/endpoints/)** — the machine-readable endpoints (`llms.txt`, the abridged and full corpora, per-page Markdown) and the OpenAPI reference, with guidance on which to use when.
* **[Driving your AI assistant](/en-us/ai/assistants/)** — tool-specific tips for Claude, Cursor, ChatGPT and others, and how to get better integration code out of them.
* **[Sudomimus CLI for AI agents](/en-us/ai/cli/)** — the command-line control surface for account checks, local automation, and future developer operations.
## Why this works
[Section titled “Why this works”](#why-this-works)
The documentation covers the choice between Connect, OIDC, and native direct-issue; their protocol flows; the shared identity model; and the three-layer rule model. Giving an assistant the relevant pages lets it work from the same published material as a human integrator. Re-fetch pages when the contract may have changed, and treat generated code as an implementation draft that still needs project-specific review and testing.
# Layer 1 — Authentication rules
> Configure which authentication methods an application accepts, and how to narrow the choice on a per-inquiry basis.
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.
[Read the overview](/en-us/application-rules/overview/)
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 `/inquiry/auth/email/options`) and again at every actual authentication attempt.
## Supported methods
[Section titled “Supported methods”](#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. |
| `AGENT_ACCESS_KEY_DIRECT` | `{}` | Agent-bound AccessKey login. |
| `AUTOMATION_ACCESS_KEY_DIRECT` | `{}` | Automation-bound AccessKey login. |
| `PUBLIC_KEY_DIRECT` | `{}` | Account-bound Ed25519 public-key assertion. |
| `AGENT_PUBLIC_KEY_DIRECT` | `{}` | Agent-bound Ed25519 public-key assertion. |
| `AUTOMATION_PUBLIC_KEY_DIRECT` | `{}` | Automation-bound Ed25519 public-key assertion. |
| `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](/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
[Section titled “Application rule shape”](#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
}
```
For `GOOGLE_OAUTH`, use `allowedHostedDomains` when you only want Google Workspace users from specific domains:
```json
{
"method": "GOOGLE_OAUTH",
"payload": { "allowedHostedDomains": ["example.com"] },
"accessTokenTtlSeconds": null,
"refreshTokenTtlSeconds": null
}
```
For `DISCORD_OAUTH`, use Discord server IDs, not server names:
```json
{
"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](/en-us/application-rules/overview/#token-ttls).
## Narrowing on `/establish`
[Section titled “Narrowing on /establish”](#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
[Section titled “Worked example”](#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”](#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, `/inquiry/auth/email/options` 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
[Section titled “Related”](#related)
[The three-layer rules model](/en-us/application-rules/overview/)The overall picture — allowlist + default-deny, evaluation order, and how the three layers compose.
[Layer 2 — Realize rules](/en-us/application-rules/realize-rules/)The post-authentication identity check, using email allowlists.
[Layer 3 — Return rules](/en-us/application-rules/return-rules/)How the realized session is delivered back to the application.
# The three-layer rules model
> How Sudomimus splits application authorization into three orthogonal layers — which methods, which identities, which return paths — each allowlist-only with default-deny.
Every application on Sudomimus is gated by three independent layers of rules. Each layer answers a different question, lives in its own configuration, and is evaluated at a different point in the authentication flow. None of them have implicit defaults: a layer with zero rules allows nothing.
[Rehearse application rules in Sudomimus Theater](https://theater.sudomimus.com/application-rules/)
## The three layers
[Section titled “The three layers”](#the-three-layers)
| Layer | Question it answers | Checked when |
| ---------------------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------- |
| **Layer 1 — Authentication** | Which authentication methods may be used? | A method is offered to the user, and again at every attempt. |
| **Layer 2 — Realize** | Which identities may complete authentication? | Post-authentication, before the inquiry is marked realized. |
| **Layer 3 — Return** | How the result is delivered back to the application? | At `/establish` (against declared return methods) and at runtime (e.g. `/status-poll`). |
```
flowchart TD
Start["Authentication attempt"] --> Layer1{"Layer 1 — Authentication Any allowed method matches? (OR within the layer)"}
Constraints["Optional per-inquiry narrowing AND-combined with the application layer"]
Constraints -. where present .-> Layer1
Constraints -. where present .-> Layer2
Constraints -. where present .-> Layer3
Layer1 -->|Yes — AND with next layer| Layer2{"Layer 2 — Realize Any allowed identity matches? (OR within the layer)"}
Layer2 -->|Yes — AND with next layer| Layer3{"Layer 3 — Return Any allowed delivery path matches? (OR within the layer)"}
Layer1 -->|No| Reject["Reject allowlist default-deny"]
Layer2 -->|No| Reject
Layer3 -->|No| Reject
Layer3 -->|Yes — all three layers pass| TTL["TtlFold minimum from all matched rules and constraints"]
TTL --> Allow["Allow realization and initial issuance"]
```
Splitting the decision this way means each axis can change independently — opening a new authentication method does not silently widen who can sign in, and tightening the allowed callers does not require touching authentication configuration.
## Allowlist with default-deny
[Section titled “Allowlist with default-deny”](#allowlist-with-default-deny)
Every layer is **allowlist-only**. A new application starts with zero rules in all three layers and cannot be used until rules are explicitly created. There is no implicit “allow everything” mode, and removing the last rule from a layer disables the application — it does not fall back to a default.
This is deliberate: an authentication system that defaults to *allow* tends to leak access whenever the surrounding configuration is misunderstood. Defaulting to *deny* means a misconfiguration produces a visible failure rather than a silent breach.
## Per-inquiry narrowing
[Section titled “Per-inquiry narrowing”](#per-inquiry-narrowing)
Application-level rules describe what the application *as a whole* is willing to allow. A single login session is often more specific — a sensitive admin flow may want passkey only; a tenant-scoped flow may want a single email allowlist. The `/establish` request therefore accepts three optional narrowing fields:
| Field on `/establish` | Narrows |
| --------------------------- | ------- |
| `authenticationConstraints` | Layer 1 |
| `realizeConstraints` | Layer 2 |
| `returnMethods` | Layer 3 |
Each field’s shape mirrors the corresponding rule shape, so the same vocabulary is used in both places.
* **Field absent** — no narrowing for that layer; the application’s rules alone decide.
* **Field present and empty array** — rejected. An empty narrowing would mean “allow nothing”, which is what removing the rules from the application already expresses.
* **Field present and non-empty** — every entry is structurally validated and stored on the inquiry. At evaluation time it is **AND**-combined with the application’s rules.
## OR within, AND across
[Section titled “OR within, AND across”](#or-within-and-across)
When multiple records could apply at the same evaluation point, the combiner is:
* **OR within a single layer and a single source** — multiple matching application rules in Layer 1, for example, all stack: any match passes.
* **AND across layers, and AND across (application rules, inquiry constraints)** — Layer 1, Layer 2, and Layer 3 must each pass, and within each layer both the application rules and the (optional) inquiry constraints must allow it.
The result is that narrowing on an inquiry can only further restrict — it can never grant something the application itself did not allow.
## Token TTLs
[Section titled “Token TTLs”](#token-ttls)
Every rule and every per-inquiry constraint can optionally carry `accessTokenTtlSeconds` and `refreshTokenTtlSeconds`. The issuing flow computes the final TTLs by collecting all matching values across every layer and source and taking the **minimum** — the strictest setting wins. Session API refresh preserves the TTLs resolved at the initial issue point.
Defaults are **3 hours** for access and **30 days** for refresh. If access TTL exceeds refresh TTL, access TTL is reduced to match refresh TTL. For example, access = 7 days and refresh = 1 day resolve to 1 day for both. The full bounds (60 seconds – 7 days for access; 1 day – 365 days for refresh) are in [Tokens and verification](/en-us/concepts/tokens-and-verification/#ttl-bounds).
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
Each layer has its own page with the per-layer schema, glob/match semantics, and a worked example:
[Layer 1 — Authentication rules](/en-us/application-rules/authentication-rules/)Which authentication methods an application accepts, and how to narrow them per inquiry.
[Layer 2 — Realize rules](/en-us/application-rules/realize-rules/)Which identities may complete authentication — by email (glob), Steam ID, account alias (exact), or sector subject (exact).
[Layer 3 — Return rules](/en-us/application-rules/return-rules/)How the result is delivered — callbacks, status polling, device code, REVEAL, DIRECT\_ISSUE, and OIDC.
[Start with Layer 1](/en-us/application-rules/authentication-rules/)
# Layer 2 — Realize rules
> Configure which identities are allowed to complete authentication for an application — by email (with `*` glob), Steam ID, account alias, or sector subject.
Part of the three-layer rules model
Layer 2 is one of three rule layers. The overview explains allowlist + default-deny, evaluation order, and how the layers compose.
[Read the overview](/en-us/application-rules/overview/)
Layer 2 controls **which identities** may complete authentication for an application. It runs **after** the user has successfully proven they own an identity but **before** the inquiry is marked realized.
The reason this is a separate layer: authenticating that a user is `alice@example.com` and *deciding whether the application is willing to let `alice@example.com` in* are different questions. Layer 1 answers the first, Layer 2 answers the second.
## Supported constraint types
[Section titled “Supported constraint types”](#supported-constraint-types)
Layer 2 has five constraint types. An application may mix them — within the layer, rules are OR’d, so any matching rule allows the realize.
| Type | Payload | Match semantics |
| ---------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `EMAIL` | `{ "allowedEmails": string[] }` | Case-insensitive glob with `*` |
| `STEAM_ID` | `{ "allowedSteamIds": (string \| "*")[] }` | Exact match on decimal SteamID64, or literal `"*"` wildcard |
| `ACCOUNT_ALIAS` | `{ "allowedAccountAliases": string[] }` | Exact match on the realizing account’s **account alias** (the user-visible, application-invisible, rotatable handle) — **no wildcard** |
| `SECTOR_SUBJECT` | `{ "allowedSectorSubjects": string[] }` | Exact match on the realizing account’s **sector subject** for the application’s sector (the application-visible token `sub`) — **no wildcard** |
| `EVERYONE` | `{}` | Unconditional allow — matches every authenticated account, including one with no verified email. Empty payload. |
In every case, the matched value comes from the realizing account/session:
* `EMAIL` is checked against **every verified email the account owns** — the full set of `EmailIdentity` rows, not just the email used at this login. The rule matches if **any one** of those emails satisfies the allowlist. For email-OTP register where the account does not yet exist, the matching is against the single email being registered.
* `STEAM_ID` is checked against the verified SteamID64 for **either** Steam path — the native `STEAM_TICKET` direct-issue from inside a game, or the browser `STEAM_OPENID` “Sign in with Steam” button. Both paths resolve to the same per-user Steam identity. Steam-first accounts that have never linked an email need a `STEAM_ID` (or `ACCOUNT_ALIAS` / `SECTOR_SUBJECT`) rule to pass; an `EMAIL`-only Layer 2 will reject them.
* `ACCOUNT_ALIAS` is checked against the account’s **account alias** — the rotatable, user-facing handle a user sees and manages in the With portal (e.g. `quiet-meadow-7h2k-9m4p-3fnp-falcon`). It is never exposed to applications, so it is the value a rule author allow-lists to pin a specific human regardless of which application they realize through.
* `SECTOR_SUBJECT` is checked against the account’s **sector subject** — the per-(account × sector) opaque token that an application actually sees as the token `sub` (e.g. `sub_9SQ5535CRWNDDM2T`). Applications sharing a sector see the same subject for a given user; applications in different sectors see different subjects. Use it to allow-list users by the exact identifier this application receives.
Both `ACCOUNT_ALIAS` and `SECTOR_SUBJECT` are opaque values matched **exactly** (never format-validated) and have **no wildcard**. Because both are minted only after the account (and its per-sector subject) exists, a fresh registration produces values nobody could have pre-allowlisted — an application whose Layer 2 contains **only** `ACCOUNT_ALIAS` or `SECTOR_SUBJECT` rules cannot accept new sign-ups. They are meant for locking access to a fixed set of already-existing accounts. Both values are rotatable; rotating one invalidates any rule that allow-listed the old value.
## EMAIL glob semantics
[Section titled “EMAIL glob semantics”](#email-glob-semantics)
`allowedEmails` entries are matched as case-insensitive globs where **only `*` is special** (it matches zero or more characters). All other characters, including `@`, `.`, and `+`, are literal.
| Pattern | Matches |
| --------------------- | ---------------------------------------------- |
| `alice@example.com` | exact email |
| `*@example.com` | any address on the `example.com` domain |
| `alice+*@example.com` | any plus-tagged variant of `alice@example.com` |
| `*` | any email |
Inputs are trimmed and lowercased before comparison.
## STEAM\_ID semantics
[Section titled “STEAM\_ID semantics”](#steam_id-semantics)
Each entry is either the literal `"*"` (matches any verified SteamID64) or a decimal SteamID64 string (exact case-sensitive match). The `[0-9]{1,20}` shape is enforced at write time.
```json
{
"constraintType": "STEAM_ID",
"payload": {
"allowedSteamIds": ["76561198000000000", "76561198000000001"]
}
}
```
## ACCOUNT\_ALIAS semantics
[Section titled “ACCOUNT\_ALIAS semantics”](#account_alias-semantics)
Each entry is an exact string equal to the target account’s **account alias** — the rotatable, user-facing handle (e.g. `quiet-meadow-7h2k-9m4p-3fnp-falcon`). There is no wildcard and no glob; the value is opaque and compared literally.
```json
{
"constraintType": "ACCOUNT_ALIAS",
"payload": {
"allowedAccountAliases": [
"quiet-meadow-7h2k-9m4p-3fnp-falcon",
"bold-harbor-2x4q-8m1p-5kna-otter"
]
}
}
```
The alias is the With-portal handle a user manages for themselves — it is never disclosed to applications, so it is the right key when you want to pin a specific human across every application they realize through. A user who rotates their alias drops out of any rule that allow-listed the old value.
## SECTOR\_SUBJECT semantics
[Section titled “SECTOR\_SUBJECT semantics”](#sector_subject-semantics)
Each entry is an exact string equal to the target account’s **sector subject** for this application’s sector — the application-visible token `sub` (e.g. `sub_9SQ5535CRWNDDM2T`). There is no wildcard and no glob; the value is opaque and compared literally.
```json
{
"constraintType": "SECTOR_SUBJECT",
"payload": {
"allowedSectorSubjects": [
"sub_9SQ5535CRWNDDM2T",
"sub_4K2P8M1N7QRWXY3Z"
]
}
}
```
The sector subject is the exact identifier this application (and any sibling application sharing its sector) receives for a user; an application in a different sector sees a different subject for the same human. Use it to allow-list by the value you already key your users on. Rotating a user’s sector subject drops them out of any rule that allow-listed the old value.
Both are literal allowlists with **no wildcard**, so combine either with an `EMAIL` or `STEAM_ID` rule when the flow should also let new users sign up.
## EVERYONE semantics
[Section titled “EVERYONE semantics”](#everyone-semantics)
`EVERYONE` is the explicit “this application is open to anyone” rule. It carries an empty payload and matches **every** authenticated account unconditionally.
```json
{
"constraintType": "EVERYONE",
"payload": {}
}
```
This does not weaken the allowlist-with-default-deny model — it is an explicit opt-in. An application with no Layer 2 rules still rejects everyone; `EVERYONE` is how you say “any account that cleared Layer 1 may realize.” It is the right choice only for a genuinely public application that should not gate on identity at all. An account does not need a verified email to match `STEAM_ID`, `ACCOUNT_ALIAS`, or `SECTOR_SUBJECT`, so prefer one of those narrower allowlists when it expresses the intended audience. Add `EVERYONE` alongside other rules and the OR semantics make it dominant — any account matches — so use it deliberately.
## Application rule shape
[Section titled “Application rule shape”](#application-rule-shape)
Every Layer 2 rule has the same envelope; only the `constraintType` + `payload` shape differs.
```json
{
"constraintType": "EMAIL",
"payload": { "allowedEmails": ["*@example.com"] },
"accessTokenTtlSeconds": null,
"refreshTokenTtlSeconds": null
}
```
Multiple rules on an application are OR’d: an identity that matches any rule is allowed through.
## Narrowing on `/establish`
[Section titled “Narrowing on /establish”](#narrowing-on-establish)
The `realizeConstraints` field on `/establish` carries the same shape and narrows the allowed identities for a single inquiry:
```json
{
"applicationAnchor": "my-app",
"realizeConstraints": [
{
"constraintType": "EMAIL",
"payload": { "allowedEmails": ["admin@example.com"] }
}
]
}
```
* 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. The inner payload list must itself be non-empty.
## Worked example
[Section titled “Worked example”](#worked-example)
The application has one Layer 2 rule: `EMAIL` with `allowedEmails: ["*@example.com"]`. A particular admin inquiry passes `realizeConstraints: [{ constraintType: "EMAIL", payload: { allowedEmails: ["admin@example.com"] } }]`.
| User authenticates as | App allows? | Inquiry allows? | Result |
| --------------------- | ----------------------------- | --------------- | ------------------ |
| `admin@example.com` | yes (matches `*@example.com`) | yes | realized |
| `alice@example.com` | yes (matches `*@example.com`) | no | rejected post-auth |
| `attacker@other.com` | no | n/a | rejected post-auth |
Note that Layer 2 runs **after** Layer 1 — the user has already proven they own the address. Layer 2 then decides whether the application is willing to accept that specific identity for this session.
## Related
[Section titled “Related”](#related)
[The three-layer rules model](/en-us/application-rules/overview/)The overall picture — allowlist + default-deny, evaluation order, and how the three layers compose.
[Layer 1 — Authentication rules](/en-us/application-rules/authentication-rules/)Which authentication methods are offered to the user.
[Layer 3 — Return rules](/en-us/application-rules/return-rules/)How the realized session is delivered back to the application.
# Layer 3 — Return rules
> Configure how authentication results reach the application — browser callbacks, native status polling, device authorization, one-time REVEAL, native direct-issue, or OIDC.
Part of the three-layer rules model
Layer 3 is one of three rule layers. The overview explains allowlist + default-deny, evaluation order, and how the layers compose.
[Read the overview](/en-us/application-rules/overview/)
Layer 3 controls **how an authentication result reaches the application**. It is checked twice: once at `/establish` (against each return method declared on the request) and again at runtime when the chosen method actually runs (for example on `/status-poll`, or inside the OIDC `/token` exchange).
## Supported return methods
[Section titled “Supported return methods”](#supported-return-methods)
| Method | Payload (per inquiry) | Used for |
| -------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CALLBACK` | `{ "callbackUrl": "https://..." }` | Web applications — the browser is redirected to a URL on the application after authentication. |
| `STATUS_POLL` | `{}` | Native clients — the client polls `connect POST /status-poll` and is told when the inquiry is realized. |
| `REVEAL` | `{}` | Developer / CLI / manual integration — the access and/or refresh tokens are shown directly on `via.sudomimus.com` after login (masked, with a “Reveal” button) so the user can copy them out by hand. No application sits on the other end. |
| `DIRECT_ISSUE` | `{}` | Opts the application in to one-shot `native-api` issuance — Steam ticket (`POST /direct-issue/steam-ticket`), AccessKey (`POST /direct-issue/access-key`), or PublicKey (`POST /direct-issue/public-key`). Tokens are minted in the same request that authenticates; no `/establish` or `/redeem` is involved. |
| `OIDC` | *(not declared per inquiry)* | Enables this application to be used as an OIDC relying party via `oidc.sudomimus.com`. Required for the OIDC `authorization_code` + PKCE flow. |
| `DEVICE_CODE` | *(not declared per inquiry)* | Enables OAuth-style device authorization through `device-api.sudomimus.com`. Public clients call `/device-authorize` and `/device-token`; no client-auth key is sent to the device. |
## CALLBACK — application rule shape
[Section titled “CALLBACK — application rule shape”](#callback--application-rule-shape)
A `CALLBACK` rule on the application carries the **allowed hostnames** for the callback URL — not the URL itself. The actual URL is supplied per-inquiry on `/establish`; the rule decides whether that URL’s hostname is acceptable.
```json
{
"returnMethod": "CALLBACK",
"payload": {
"allowedCallbackDomains": ["client.example.com", "admin.example.com"]
},
"accessTokenTtlSeconds": null,
"refreshTokenTtlSeconds": null
}
```
Hostname comparison is **exact, case-insensitive** — `client.example.com` does not implicitly cover `sub.client.example.com`. To allow both, list both.
## STATUS\_POLL — application rule shape
[Section titled “STATUS\_POLL — application rule shape”](#status_poll--application-rule-shape)
`STATUS_POLL` rules have no payload — having the rule on the application is the whole configuration.
```json
{
"returnMethod": "STATUS_POLL",
"payload": {},
"accessTokenTtlSeconds": null,
"refreshTokenTtlSeconds": null
}
```
## REVEAL — application rule shape
[Section titled “REVEAL — application rule shape”](#reveal--application-rule-shape)
`REVEAL` rules configure **which tokens are shown to the user** after a successful login. At least one of `includeAccessToken` and `includeRefreshToken` must be `true`.
```json
{
"returnMethod": "REVEAL",
"payload": {
"includeAccessToken": true,
"includeRefreshToken": true
},
"accessTokenTtlSeconds": null,
"refreshTokenTtlSeconds": null
}
```
When an inquiry declares `REVEAL`, Sudomimus signs the tokens at realize time and returns them inline; the inquiry is marked redeemed immediately, so any subsequent `/redeem` call for the same login fails with `InquiryAlreadyRedeemed`. Multiple matching `REVEAL` rules are combined with **OR semantics** — if any matching rule allows the access token, it is included; same for the refresh token.
One-shot, no recovery
Tokens shown via REVEAL are not stored anywhere visible to the user; they’re displayed once on the post-login screen and never re-sent. The user must copy them before navigating away. REVEAL is intended for developer tooling, CLI integration, or one-off manual binding — not for production end-user authentication.
### Combining REVEAL with other methods
[Section titled “Combining REVEAL with other methods”](#combining-reveal-with-other-methods)
REVEAL takes precedence on the `via.sudomimus.com` page: it suppresses the automatic CALLBACK redirect so the user can copy the tokens first. The user then sees a “Continue to app” button if a CALLBACK was also declared. STATUS\_POLL still signals `realized` to a polling client, but the subsequent `/redeem` will fail because REVEAL has already redeemed the inquiry.
## DIRECT\_ISSUE — application rule shape
[Section titled “DIRECT\_ISSUE — application rule shape”](#direct_issue--application-rule-shape)
`DIRECT_ISSUE` rules have no payload — the rule’s presence is the entire configuration. Add this rule to opt an application in to the `native-api` direct-issue endpoints (Steam ticket, AccessKey, and PublicKey).
```json
{
"returnMethod": "DIRECT_ISSUE",
"payload": {},
"accessTokenTtlSeconds": null,
"refreshTokenTtlSeconds": null
}
```
`DIRECT_ISSUE` does not appear in `/establish` `returnMethods` declarations — those endpoints have no `/establish` step. Layer 3 is still checked at runtime inside the `native-api` handlers.
## DEVICE\_CODE — application rule shape
[Section titled “DEVICE\_CODE — application rule shape”](#device_code--application-rule-shape)
`DEVICE_CODE` rules have no payload. Add this rule when a CLI, launcher, TV-style device, or other public client should use the [Device authorization flow](/en-us/device/flow/).
```json
{
"returnMethod": "DEVICE_CODE",
"payload": {},
"accessTokenTtlSeconds": null,
"refreshTokenTtlSeconds": null
}
```
`DEVICE_CODE` is not declared on `/establish`; the device flow starts at `device-api POST /device-authorize`. The rule is the application’s opt-in that allows that public-client flow to start.
## OIDC — application rule shape
[Section titled “OIDC — application rule shape”](#oidc--application-rule-shape)
`OIDC` rules opt the application in to being used as an OpenID Connect relying party via `oidc.sudomimus.com`. The payload carries the standard OIDC client configuration:
```json
{
"returnMethod": "OIDC",
"payload": {
"redirectUris": ["https://app.example.com/oidc/callback"],
"postLogoutRedirectUris": ["https://app.example.com/"],
"allowedScopes": ["openid", "email", "profile", "offline_access"],
"tokenEndpointAuthMethod": "private_key_jwt"
},
"accessTokenTtlSeconds": null,
"refreshTokenTtlSeconds": null
}
```
* **`redirectUris`** — full-URI **exact match**. The `redirect_uri` parameter the RP sends to `/authorize` must equal one of these strings byte-for-byte. No prefix or wildcard matching.
* **`postLogoutRedirectUris`** — full-URI exact match, used by `/end-session`.
* **`allowedScopes`** — the set of OIDC scopes this client is permitted to request. `openid` is always required; `email`, `profile`, and `offline_access` are supported. Requesting a scope outside this list fails at `/authorize`.
* **`tokenEndpointAuthMethod`** — one of `"private_key_jwt"` (confidential client; signs a JWT assertion to authenticate at `/token`), `"client_secret_basic"` (confidential client; presents a shared secret in the HTTP `Authorization: Basic` header), `"client_secret_post"` (confidential client; sends a shared secret in the `/token` form body), or `"none"` (public client; **PKCE is required** for these).
OIDC rules are not declared per-inquiry on `/establish` — the OIDC flow has its own `/authorize` endpoint instead.
End-to-end usage and library setup is in [OIDC relying parties](/en-us/oidc/flow/).
## Declaring on `/establish`
[Section titled “Declaring on /establish”](#declaring-on-establish)
The `returnMethods` field on `/establish` plays two roles at once: it declares which methods will be used for this inquiry **and** it carries the concrete `callbackUrl` (which the application’s rules cannot know in advance).
```json
{
"applicationAnchor": "my-app",
"returnMethods": [
{
"type": "CALLBACK",
"payload": { "callbackUrl": "https://client.example.com/auth/return" }
},
{
"type": "STATUS_POLL",
"payload": {}
},
{
"type": "REVEAL",
"payload": {}
}
]
}
```
`DIRECT_ISSUE`, `DEVICE_CODE`, and `OIDC` are not declared on `/establish` — those flows do not pass through it.
* Field absent → no Layer 3 narrowing; the application’s rules still apply at runtime.
* Field present and empty array → rejected.
* Field present and non-empty → every entry is validated against the application’s Layer 3 rules at `/establish` time. For `CALLBACK`, the URL’s hostname must match an entry in some Layer 3 `CALLBACK` rule’s `allowedCallbackDomains`. For `STATUS_POLL` and `REVEAL`, a Layer 3 rule of the same method must exist on the application.
## Worked example
[Section titled “Worked example”](#worked-example)
The application has one Layer 3 rule: `CALLBACK` with `allowedCallbackDomains: ["client.example.com"]`.
| Inquiry `callbackUrl` | Result |
| --------------------------------------------------- | -------------------------------------- |
| `https://client.example.com/return` | accepted |
| `https://Client.Example.Com/return` | accepted (case-insensitive) |
| `https://sub.client.example.com/return` | rejected (no implicit subdomain match) |
| `https://attacker.com/?redirect=client.example.com` | rejected (hostname is `attacker.com`) |
For `STATUS_POLL`, the runtime check on `/status-poll` re-verifies both that the application still has a `STATUS_POLL` rule and that the inquiry’s per-inquiry narrowing (if any) still allows it.
## Why hostname-only (for CALLBACK)
[Section titled “Why hostname-only (for CALLBACK)”](#why-hostname-only-for-callback)
The check matches only the hostname, not the path or query string. Sudomimus does not own the application’s URL structure and would create a maintenance burden by requiring exact-path allowlists. Application owners are expected to keep their callback handlers safe at the routing level — Sudomimus restricts the *origin* the browser is sent to.
(OIDC `redirectUris` are a different model — they require full-URI exact match because the OIDC standard demands it and because most OIDC libraries already produce a single fixed redirect URI per client.)
## Related
[Section titled “Related”](#related)
[The three-layer rules model](/en-us/application-rules/overview/)The overall picture — allowlist + default-deny, evaluation order, and how the three layers compose.
[Layer 1 — Authentication rules](/en-us/application-rules/authentication-rules/)Which authentication methods are offered to the user.
[Layer 2 — Realize rules](/en-us/application-rules/realize-rules/)The post-authentication identity check, using email allowlists.
[OIDC relying parties](/en-us/oidc/flow/)End-to-end OIDC flow that uses the OIDC return rule.
# Configuration templates
> Copy-and-adapt starting points for the three-layer rules of common application types — web apps, internal tools, Steam games, CLIs, OIDC relying parties, and public apps.
The three layers are flexible, but most applications start from one of a handful of shapes. Pick the closest template below, drop the rules into your application in the [With portal](https://with.sudomimus.com), then adjust.
Every layer needs at least one rule
The model is **allowlist with default-deny**: an application with an empty layer lets **no one** through. A working application needs at least one rule in **each** of the three layers — Authentication, Realize, and Return. If logins are rejected with everything “configured”, check that all three layers are non-empty. See the [overview](/en-us/application-rules/overview/) for how the layers compose.
In the tables below, `*.example.com` style values are placeholders — replace them with your own. Each layer can hold more than one rule; within a layer, rules are OR’d.
## Standard web app (passwordless)
[Section titled “Standard web app (passwordless)”](#standard-web-app-passwordless)
Passkeys plus email one-time codes, open sign-up, redirect back to your site. The most common starting point.
| Layer | Rules |
| ---------------------- | ---------------------------------------------------------------- |
| **1 — Authentication** | `PASSKEY_USERNAMELESS`, `PASSKEY_REASONED`, `EMAIL_VERIFICATION` |
| **2 — Realize** | `EMAIL` → `{ "allowedEmails": ["*"] }` |
| **3 — Return** | `CALLBACK` → `{ "allowedCallbackDomains": ["app.example.com"] }` |
## Web app with social sign-in
[Section titled “Web app with social sign-in”](#web-app-with-social-sign-in)
The passwordless template plus “Sign in with …” buttons. Add only the providers you want; each is one Layer 1 rule.
| Layer | Rules |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------- |
| **1 — Authentication** | `PASSKEY_USERNAMELESS`, `PASSKEY_REASONED`, `EMAIL_VERIFICATION`, `GOOGLE_OAUTH`, `GITHUB_OAUTH`, `DISCORD_OAUTH` |
| **2 — Realize** | `EMAIL` → `{ "allowedEmails": ["*"] }` |
| **3 — Return** | `CALLBACK` → `{ "allowedCallbackDomains": ["app.example.com"] }` |
Note that Steam-, Battle.net-, and X-only accounts have no verified email, so an `EMAIL`-only Layer 2 rejects them. If you offer those providers, pair the `EMAIL` rule with `STEAM_ID` / `EVERYONE` as appropriate (see the public-app template below).
## Internal / team tool (domain-restricted)
[Section titled “Internal / team tool (domain-restricted)”](#internal--team-tool-domain-restricted)
Only people with an email on your company’s domain may sign in.
| Layer | Rules |
| ---------------------- | --------------------------------------------------------------------- |
| **1 — Authentication** | `PASSKEY_USERNAMELESS`, `PASSKEY_REASONED`, `EMAIL_VERIFICATION` |
| **2 — Realize** | `EMAIL` → `{ "allowedEmails": ["*@yourcompany.com"] }` |
| **3 — Return** | `CALLBACK` → `{ "allowedCallbackDomains": ["tool.yourcompany.com"] }` |
To force those users through your own identity provider instead of (or in addition to) the gate above, see [Sign in with your IdP](/en-us/domains-federation/sign-in-with-your-idp/) and [Domain login policy](/en-us/domains-federation/domain-login-policy/).
## Steam game (silent, in-game login)
[Section titled “Steam game (silent, in-game login)”](#steam-game-silent-in-game-login)
A game shipped through Steam that logs the player in without a browser.
| Layer | Rules |
| ---------------------- | -------------------------------------------------- |
| **1 — Authentication** | `STEAM_TICKET` → `{ "allowedSteamAppIds": [480] }` |
| **2 — Realize** | `STEAM_ID` → `{ "allowedSteamIds": ["*"] }` |
| **3 — Return** | `DIRECT_ISSUE` |
Replace `480` with your real Steam App ID. To let the same players also sign in through a browser “Sign in with Steam” button, add a `STEAM_OPENID` Layer 1 rule — it resolves to the same Steam identity. End-to-end flow: [Native clients](/en-us/native/overview/).
## CLI / headless service (AccessKey)
[Section titled “CLI / headless service (AccessKey)”](#cli--headless-service-accesskey)
A command-line tool or service that authenticates as a known, already-existing account with a pre-issued credential.
| Layer | Rules |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------- |
| **1 — Authentication** | `ACCESS_KEY_DIRECT` |
| **2 — Realize** | `SECTOR_SUBJECT` → `{ "allowedSectorSubjects": ["sub_..."] }` (or an `EMAIL` rule that matches the bound account) |
| **3 — Return** | `DIRECT_ISSUE` |
AccessKeys cannot create new accounts — they always act as a specific existing account, so Layer 2 pins that account. See [Native clients](/en-us/native/overview/).
## OIDC relying party
[Section titled “OIDC relying party”](#oidc-relying-party)
Expose the application to a standard OpenID Connect client (`authorization_code` + PKCE).
| Layer | Rules |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **1 — Authentication** | `PASSKEY_USERNAMELESS`, `PASSKEY_REASONED`, `EMAIL_VERIFICATION` (plus any social providers) |
| **2 — Realize** | `EMAIL` → `{ "allowedEmails": ["*"] }` |
| **3 — Return** | `OIDC` → `{ "redirectUris": ["https://app.example.com/oidc/callback"], "allowedScopes": ["openid", "email", "profile", "offline_access"], "tokenEndpointAuthMethod": "private_key_jwt" }` |
Use `"tokenEndpointAuthMethod": "none"` for a public client (PKCE is required for public clients and recommended for confidential clients). End-to-end setup: [OIDC relying parties](/en-us/oidc/flow/).
## Desktop app (browser polling)
[Section titled “Desktop app (browser polling)”](#desktop-app-browser-polling)
A desktop or Electron app that opens the system browser for login, then polls for the result.
| Layer | Rules |
| ---------------------- | ---------------------------------------------------------------- |
| **1 — Authentication** | `PASSKEY_USERNAMELESS`, `PASSKEY_REASONED`, `EMAIL_VERIFICATION` |
| **2 — Realize** | `EMAIL` → `{ "allowedEmails": ["*"] }` |
| **3 — Return** | `STATUS_POLL` |
See the [browser-polling flow](/en-us/native/overview/) for the `/establish` → `/status-poll` → `/redeem` sequence.
## Genuinely public app (no identity gate)
[Section titled “Genuinely public app (no identity gate)”](#genuinely-public-app-no-identity-gate)
Anyone who can clear Layer 1 is allowed in — including accounts with no verified email (Steam-only, Battle.net-only). Use this when you do not want to restrict *who* can sign in at all.
| Layer | Rules |
| ---------------------- | -------------------------------------------------------------------------------------------------- |
| **1 — Authentication** | whichever methods you offer (e.g. `PASSKEY_USERNAMELESS`, `EMAIL_VERIFICATION`, `STEAM_TICKET`, …) |
| **2 — Realize** | `EVERYONE` |
| **3 — Return** | `CALLBACK` / `STATUS_POLL` / `DEVICE_CODE` / `DIRECT_ISSUE` / `OIDC` — whatever your client uses |
`EVERYONE` is unconditional, so it dominates any other Layer 2 rule via the OR semantics. Use it deliberately — see [Realize rules](/en-us/application-rules/realize-rules/).
## After you pick a template
[Section titled “After you pick a template”](#after-you-pick-a-template)
* **Narrow per request.** `/establish` can tighten any layer for a single inquiry via `authenticationConstraints` / `realizeConstraints` / `returnMethods` — useful for, say, an admin-only login link. See the [overview](/en-us/application-rules/overview/).
* **Decide what profile data you request.** Whether the app receives the user’s email or name is a separate setting — see [Identity claims and sharing](/en-us/concepts/identity-claims/).
* **Set token lifetimes.** Each rule can override the access- and refresh-token TTLs; leave them `null` to inherit the platform defaults.
# Brand resources
> Official Sudomimus marks, sign-in button rules, motion guidance, social cards, and application icons.
Sudomimus should feel reliable, secure, and intelligent. Its visual system uses precise celestial-navigation symbols, restrained motion, and a consistent dark ink and warm ivory palette.
This page is the public usage contract. The downloadable files are generated from the same canonical geometry used by Sudomimus products.
## Brand promise
[Section titled “Brand promise”](#brand-promise)
The canonical English promise is:
> Authentication, detached from authorization.
The supporting description is:
> Sudomimus is an identity provider and authentication platform for trusted application sessions.
Do not describe Sudomimus as a general-purpose API authorization server. Integrating applications remain responsible for their own roles, permissions, resources, and business authorization.
## Logo family
[Section titled “Logo family”](#logo-family)
| Surface | Symbol | Meaning |
| ----------------- | ------------------- | ---------------------------------------- |
| Sudomimus | North star | Fixed, dependable guidance |
| Sudomimus Docs | Constellation chart | Connected knowledge and a mapped path |
| Sudomimus Sign-in | Compass | Finding and confirming a trusted bearing |
| Sudomimus With | Star gate | Passage into account and developer work |
| Sudomimus Theater | Orbit stage | Rehearsing and observing trust journeys |
Third-party applications use only the master Sudomimus north star. Product marks identify first-party Sudomimus surfaces and must not be used to label an integrating application.
## Responsive marks
[Section titled “Responsive marks”](#responsive-marks)
Two geometries are provided:
* **Micro** is designed for 12–24 CSS pixels, including sign-in buttons and compact controls.
* **Standard** is designed for 32 CSS pixels and above, including headers, presentations, and large brand placements.
Do not shrink the standard geometry as a substitute for the micro mark. Maintain clear space of at least one quarter of the rendered mark width on every side. Do not crop, skew, rotate, add a container shape, or combine the mark with another symbol.
### Downloads
[Section titled “Downloads”](#downloads)
* [Standard north-star SVG](https://sudomimus.com/brand-mark.svg)
* [Micro north-star SVG](https://sudomimus.com/brand-mark-micro.svg)
* [Micro north star for light backgrounds](https://sudomimus.com/brand-mark-micro-on-light.svg)
* [Micro north star for dark backgrounds](https://sudomimus.com/brand-mark-micro-on-dark.svg)
* [North star for light backgrounds](https://sudomimus.com/brand-mark-on-light.svg)
* [North star for dark backgrounds](https://sudomimus.com/brand-mark-on-dark.svg)
* [512 px application icon](https://sudomimus.com/pwa-512.png)
* [512 px maskable application icon](https://sudomimus.com/pwa-maskable-512.png)
* [Open Graph social card](https://sudomimus.com/og-image.png)
Download and self-host the approved asset. Do not hotlink production interface assets to `sudomimus.com`.
## Color
[Section titled “Color”](#color)
| Token | Value | Use |
| ----------------- | --------- | ----------------------------------------- |
| Brand Ink | `#0B1422` | Primary dark field and light-surface mark |
| Brand Ivory | `#EBE4D1` | Primary mark on Brand Ink |
| Brand Slate | `#6E83A6` | Supporting detail |
| Brand Muted Slate | `#34466A` | Ambient detail and dark borders |
| Brand Border | `#1E2A3E` | Dark-field boundaries |
The SVG marks use `currentColor`. Set one approved foreground color on the containing element; do not recolor individual paths.
## Sign-in button
[Section titled “Sign-in button”](#sign-in-button)
Use a real link or button rather than an image of a button. The minimum contract is:
* minimum height: `44px`;
* horizontal padding: `16px`;
* micro mark: `20px`;
* gap between mark and label: `10px`;
* label size: `16px`, weight `600`;
* border radius: `8px`;
* visible keyboard focus; and
* a text label that remains available to assistive technology.
```html
{{ localizedSignInLabel }}
```
```css
.sudomimus-sign-in {
align-items: center;
background: #ffffff;
border: 1px solid #b7c0ce;
border-radius: 8px;
color: #0b1422;
display: inline-flex;
font: 600 16px/1 system-ui, sans-serif;
gap: 10px;
min-height: 44px;
padding: 0 16px;
text-decoration: none;
}
.sudomimus-sign-in picture {
display: contents;
}
.sudomimus-sign-in img {
display: block;
flex: none;
}
.sudomimus-sign-in:focus-visible {
outline: 3px solid #6e83a6;
outline-offset: 2px;
}
@media (prefers-color-scheme: dark) {
.sudomimus-sign-in {
background: #0b1422;
border-color: #34466a;
color: #ebe4d1;
}
}
```
The base SVG uses `currentColor`. Use a fixed light- or dark-background export through ``, or inline the base SVG when it should inherit the button’s `color` directly. When an application uses an explicit theme rather than the system preference, select the fixed export from that application theme.
## Motion
[Section titled “Motion”](#motion)
Brand motion is short, precise, deterministic, and one-shot. It resolves into the exact static mark and honors `prefers-reduced-motion`. Third-party sign-in buttons should remain static; do not add spinning, pulsing, bouncing, particle, or loading behavior to the Sudomimus mark.
## Social cards and application icons
[Section titled “Social cards and application icons”](#social-cards-and-application-icons)
Sudomimus social cards use `1200 × 630` PNG output. Application icons are provided at `192 × 192` and `512 × 512`, plus a `512 × 512` maskable version. The maskable export includes the required safe-zone inset and must not be re-cropped by hand.
Each first-party Sudomimus surface publishes its own related product symbol in its favicon, social card, and web app manifest. Third-party applications should use their own application identity; they should use the Sudomimus north star only inside the sign-in affordance.
## Incorrect usage
[Section titled “Incorrect usage”](#incorrect-usage)
Do not:
* redraw or trace the mark;
* add a central circle, internal axes, gradients, shadows, or glow;
* stretch, skew, or rotate the mark;
* place text inside the mark;
* animate a third-party sign-in button indefinitely;
* use a first-party product mark for an integrating application; or
* imply that Sudomimus endorses or owns the integrating application.
# Accounts and credentials
> How Sudomimus separates the person, their sign-in credentials, and their verified email ownership.
Sudomimus keeps account identity, authentication credentials, and email ownership in separate records. This model is shared by Connect, OIDC, and native direct-issue.
| Record | What it represents |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| **Account** | The person: a stable internal record with profile data. It does not carry an email field. |
| **Authentication** | One credential the account can use, such as an email OTP login, passkey, Steam identity, OAuth identity, or enterprise federation identity. |
| **EmailIdentity** | A verified email the account owns. Each row records how ownership was verified, and at most one is primary. |
## Why they are separate
[Section titled “Why they are separate”](#why-they-are-separate)
A credential proves **how this person can sign in**. An email identity proves **which email this person owns**. Those are related, but they are not the same fact.
For example:
* An email-OTP registration creates both an email credential and a verified email identity.
* A Google, GitHub, or Discord login can establish verified email ownership without enrolling email OTP as a login method.
* A Steam-only account can sign in without owning any verified email.
* Removing a login method does not, by itself, rewrite the account’s email ownership history.
This separation lets the platform evaluate login methods, email-based access rules, and shared profile data independently.
## What applications see
[Section titled “What applications see”](#what-applications-see)
Applications identify a signed-in user by the purpose-scoped [sector subject](/en-us/concepts/pairwise-identity/) in their tokens. They fetch any permitted [identity claims](/en-us/concepts/identity-claims/) from UserInfo.
# Identity claims and sharing
> How Sudomimus shares a user's email, name, and avatar through UserInfo, using application policy and user consent.
Beyond a stable identifier, an application may need profile data such as the user’s **email**, **first name**, **last name**, **static avatar**, or **animated avatar**. Sudomimus treats these as five separate claims. Sharing has two halves:
* **The claim policy** — set by the *developer*, per application: which claims the application requests, and how strongly.
* **The claim grant** — set by the *user*, per application: which claims they have agreed to share.
A claim is returned by UserInfo only when the policy requests it **and** the user has granted it. For OIDC, the relevant scope must also be requested. The user stays in control, and a revoked claim stops being shared on the next UserInfo request.
## The claim policy (developer)
[Section titled “The claim policy (developer)”](#the-claim-policy-developer)
On your application’s detail page in the [With portal](https://with.sudomimus.com), you set each claim to one of:
| Policy | Meaning |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Off** | Never requested. The claim is never shared, regardless of what the user would allow. |
| **Optional** | Requested, but the user may decline. If they decline, the application simply does not receive it. |
| **Required** | The application needs it. The user must grant it to finish logging in. |
| **Placeholder only** (`SYNTHETIC_ONLY`) | Always provided as a generated placeholder and never asks for or shares the user’s real data. |
| **Fallback placeholder** (`SYNTHETIC_FALLBACK`) | Always provided. If the user grants real data, the real value is shared; otherwise the application receives a generated stand-in (a placeholder name, a proxy `…@proxy.sudomimus.email` address, or a generated avatar). It never blocks login and never raises an errand. |
By default, email, first name, last name, and static avatar use **Placeholder only**. Animated avatar is **Off**. This gives new applications a useful placeholder identity without sharing real profile data. Set any claim to **Off** when you want it omitted completely.
## The user’s grant
[Section titled “The user’s grant”](#the-users-grant)
The first time a user logs in to an application that requests claims, Sudomimus shows a **consent screen**:
* **Required** claims are shown as locked-on — granting them is part of completing the login.
* **Optional** claims are shown as checkboxes, **unchecked by default** — the user opts in.
* **Fallback placeholder** claims are shown like optional checkboxes too, with one difference disclosed in the copy: leaving one unchecked sends the application a placeholder value rather than nothing.
* **Placeholder only** claims are not shown as consent choices because they never share real data.
Their decision is remembered as one of three states — **granted**, **denied**, or **not yet decided**. A denied optional claim is not requested again; a claim that is not yet decided is shown on the next interactive login.
A user can review and change every decision in the **Data sharing** view of their account portal: it lists each application they share claims with, whether each claim is currently shared, and whether the application requires it, with a **Revoke** action per application. Because grants are read live by UserInfo, revoking takes effect immediately — the next UserInfo response omits the claim.
## When a claim appears in UserInfo
[Section titled “When a claim appears in UserInfo”](#when-a-claim-appears-in-userinfo)
For a given claim, the rule is:
> the policy permits a value, the user’s grant state permits the real value if real data is requested, and, for OIDC, the matching scope was requested.
```
flowchart TD
Request["Evaluate one claim for session authority and UserInfo"] --> OIDC{"OIDC session?"}
OIDC -->|Yes| Scope{"Matching scope requested?"}
Scope -->|No| Omit["Omit the claim"]
Scope -->|Yes| Policy{"Claim policy"}
OIDC -->|No — Session API| Policy
Policy -->|OFF| Omit
Policy -->|SYNTHETIC_ONLY| Placeholder["Return a placeholder"]
Policy -->|OPTIONAL| Optional{"Real data granted and present?"}
Optional -->|Yes| Real["Return real data"]
Optional -->|No| Omit
Policy -->|REQUIRED| Required{"Real data granted and present?"}
Required -->|Yes| Real
Required -->|No| Block["Block issuance or refresh before UserInfo"]
Policy -->|SYNTHETIC_FALLBACK| Fallback{"Real data granted and present?"}
Fallback -->|Yes| Real
Fallback -->|No| Placeholder
```
The OIDC scope gate maps as follows:
* `email` scope → the **email** claim
* `profile` scope → **first name**, **last name**, and **avatar**
Ordinary application sessions use Session API `/userinfo` and have no scope gate — policy + grant alone decide. OIDC sessions use the discovered OIDC `/userinfo` endpoint.
The placeholder modes are the exception to “granted or omitted”: `SYNTHETIC_ONLY` is *always* present as a placeholder, and `SYNTHETIC_FALLBACK` is present as real data when granted or as a placeholder otherwise. Placeholders are stable per account and sector: generated names, a `…@proxy.sudomimus.email` proxy address, and a generated avatar URL. A placeholder claim never blocks a login and is never omitted — the application is just told, for an OIDC email, that the address is unverified.
## Required claims and non-interactive logins
[Section titled “Required claims and non-interactive logins”](#required-claims-and-non-interactive-logins)
A **required** claim needs an interactive grant and the underlying account data. Non-interactive sign-in is rejected when that requirement is not met, with one of two reasons:
* `ClaimConsentRequired` — the user has not granted a required claim.
* `RequiredClaimDataMissing` — the user *has* granted it, but the account lacks the underlying data (e.g. a Steam account with no email).
This guarantee — that required claim authority is satisfied before a session can issue or refresh — is what the rejection protects. It covers native direct-issue, token refresh, and the OIDC token endpoint.
How the user clears it depends on the client:
* **Native clients** (Steam / AccessKey direct-issue) have no interactive login *to the application*, so the `403` carries an **[Errand](/en-us/native/claims-and-errand/)** — a browser side-trip where the user signs in (if data is being written), supplies the missing data, and grants consent. The client then retries. For an [AccessKey](/en-us/native/overview/), the same consent can also be collected up front, at the moment the user creates the key in the portal.
* **Browser / OIDC clients** clear it on the next ordinary interactive login to that application, where the consent screen is shown inline.
An ungranted **optional** claim never blocks anything — it is just omitted. Placeholder modes never block either, so they are the way to *guarantee a value is present* without ever forcing a user through a browser side-trip.
## The `claims` block
[Section titled “The claims block”](#the-claims-block)
Whenever Sudomimus issues or refreshes a token through Connect, Session API, or native direct-issue (`/redeem`, `/refresh`, `/direct-issue/*`), the response carries a top-level `claims` block alongside the tokens. It describes the policy and consent state that UserInfo will apply.
```json
{
"email": { "requirement": "REQUIRED", "state": "GRANTED" },
"firstName": { "requirement": "OPTIONAL", "state": "DENIED" },
"lastName": { "requirement": "OFF", "state": "UNKNOWN" },
"staticAvatar": { "requirement": "SYNTHETIC_ONLY", "state": "UNKNOWN" },
"animatedAvatar": { "requirement": "OFF", "state": "UNKNOWN" }
}
```
For each claim you get its `requirement` (the developer’s policy: `SYNTHETIC_ONLY` / `OFF` / `OPTIONAL` / `REQUIRED` / `SYNTHETIC_FALLBACK`) joined with its `state` (the user’s standing decision: `UNKNOWN` / `GRANTED` / `DENIED`). The distinction between `UNKNOWN` (“never asked”) and `DENIED` (“explicitly declined”) is the reason this is three states and not a nullable boolean.
Read together with [the inclusion rule above](#when-a-claim-appears-in-userinfo), the block tells you why UserInfo may return or omit a claim — policy `OFF`, never asked, declined, or granted but with no data behind it. On the claim-gate `403` from direct-issue, the same block lists what is still owed before a session can be established.
For a live view after issuance, call Session `GET /claim-state` with the access token. Its `claims` map uses UserInfo names: `email`, `given_name`, `family_name`, `picture`, and `picture_animated`. OIDC clients discover the provider-specific `claim_state_endpoint`; its response contains only the state entries covered by the session’s `email` and `profile` scopes. `name` and `email_verified` have no separate state because they are derived from other claims.
## What the application receives
[Section titled “What the application receives”](#what-the-application-receives)
* **Application access, refresh, and OIDC ID tokens** — carry no profile claims.
* **Session `/userinfo`** — gated by policy and grant, without an OIDC scope gate.
* **OIDC `/userinfo`** — gated by scope **and** grant: the **email** claim becomes `email` (plus `email_verified`); **first name** becomes `given_name`; **last name** becomes `family_name`; **static avatar** becomes `picture`; **animated avatar** becomes `picture_animated`; `name` is composed from the granted name parts. A **synthetic** email is sent with `email_verified: false` — it is a proxy address, not a verified mailbox, so do not treat it as one.
See [Tokens and verification](/en-us/concepts/tokens-and-verification/) for the minimal token layouts and UserInfo endpoint. Avatar delivery also has URL scoping, revocation, and caching rules; see [Avatar claims and delivery](/en-us/user-generated-content/avatar-claims-and-delivery/).
## Where to manage it
[Section titled “Where to manage it”](#where-to-manage-it)
* **Developer** — your application’s detail page in the [With portal](https://with.sudomimus.com): set each claim to Off / Optional / Required / Placeholder only / Fallback placeholder.
* **User** — the **Data sharing** view in the account portal: see and revoke what each application receives.
# Organizations and sectors
> How organizations hold applications and sectors, what each member role can do, and how to retire resources safely.
When you build on Sudomimus, your applications and related resources live inside an **organization**. Invite teammates to the organization and give each person the role they need.
```
flowchart TD
Account["Account"] -->|member of| Organization["Organization"]
Organization -->|owns| Application["Application"]
Organization -->|owns| Sector["Sector"]
Application -->|belongs to exactly one| Sector
```
## Organizations
[Section titled “Organizations”](#organizations)
An **organization** is the multi-tenant container that owns everything you create as a developer — applications, sectors, [adopted domains](/en-us/domains-federation/overview/), and [federation connectors](/en-us/domains-federation/federation-connectors/).
* You become a developer simply by **creating an organization** from the With portal; that makes you its first **Owner**. “Developer” is not a separate kind of account — it is a capability any Sudomimus user picks up by belonging to an organization.
* Every account holder always has the personal **Account** surface (Profile, sign-in methods, data sharing). The **Developer** surface — Organizations, Applications, Sectors — simply lists the organizations you belong to. Belonging to none is a normal state, not an error.
* An organization has limits on how many applications and sectors it can hold, and each account has a limit on how many organizations it can own.
## Applications
[Section titled “Applications”](#applications)
An **application** is a single integration — a web app, game, CLI, or OIDC relying party. It carries a permanent [`applicationAnchor`](/en-us/connect/three-key-model/), its signing and client-auth keys, and the [three layers of rules](/en-us/application-rules/overview/) that decide who can log in and how. Every application belongs to exactly one organization and exactly one sector.
## Sectors
[Section titled “Sectors”](#sectors)
A **sector** is the identity-isolation boundary. Applications in the same sector see the **same** opaque identifier for a given user; applications in different sectors see **unrelated** identifiers for the same person. By default each application gets its own fresh sector (maximum isolation); you opt in to shared identity by placing two applications in one sector.
The privacy contract behind sectors — the sector subject, account alias, and how applications receive pairwise user identity — is covered in [Privacy & pairwise identity](/en-us/concepts/pairwise-identity/).
## Roles and membership
[Section titled “Roles and membership”](#roles-and-membership)
Collaboration is managed at the **organization** level — there is no per-application or per-sector membership. Each member holds one role, and the three roles form a rank:
**Viewer < Admin < Owner**
| What you can do | Minimum role |
| -------------------------------------------------------------------------------------------------------------------- | -------------- |
| View applications, sectors, rules, and organization settings | **Viewer** |
| Create and edit applications, configure rules, rotate keys, set the [claim policy](/en-us/concepts/identity-claims/) | **Admin** |
| Manage members — invite, change a role, remove | **Owner** |
| Retire (disable) an application, a sector, or the organization | **Sole Owner** |
Members are invited by their **account alias** (the user-visible handle, never the internal account ID). An organization can have more than one Owner; the portal refuses to remove or demote the **last** remaining Owner, so a developer cannot accidentally orphan their own organization.
## Retiring resources
[Section titled “Retiring resources”](#retiring-resources)
There is **no hard delete** on the developer surface. To take a resource out of service you **retire** it by disabling it:
* Retiring an **application** stops new sign-ins and refreshes. Already-issued access tokens may still pass an application’s offline checks until they expire.
* Retiring a **sector** requires that every application in it is already disabled.
* Retiring an **organization** requires that every application and sector it owns is already disabled. A retired organization is **frozen** — no member, rename, or create operation works on it until you re-enable it.
Each of these retire operations requires you to be the organization’s **sole** Owner, so one co-owner cannot unilaterally pull a shared resource out from under the others.
## How this interacts with account deletion
[Section titled “How this interacts with account deletion”](#how-this-interacts-with-account-deletion)
Because retiring is the only way to wind a resource down, account deletion has a guard rail: you **cannot erase your account while you are the sole active Owner of an organization that still holds a live resource** (a Draft or Active application, or a non-disabled sector). Retire those first. An organization with another active Owner does not block deletion because that person can continue managing it. See [Account deletion](/en-us/guides/account-deletion/) for the full erasure flow.
## Where this lives
[Section titled “Where this lives”](#where-this-lives)
Everything above is managed in the With portal at [`with.sudomimus.com`](https://with.sudomimus.com). A resource that belongs to no organization is platform-managed and never appears in a developer’s dashboard.
# Privacy & pairwise identity
> How Sudomimus stops applications from correlating the same user across products — purpose-scoped pairwise identifiers, user-controlled rotation, and per-application claim sharing.
Sudomimus is built so that **the applications a user signs in to cannot quietly link that person across products**, and so that the user — not the application — stays in control of the identity they hand out. This page explains the identity model that makes that true.
## The core promise
[Section titled “The core promise”](#the-core-promise)
When a user authenticates, Sudomimus gives the application a **purpose-scoped identifier** that is meaningful only to that application — or to that one developer’s family of applications. Three principles hold:
* **Applications use pairwise identifiers.** Internally every account has a primary key, but token payloads and user-facing flows use the account alias or sector subject described below.
* **No cross-application correlation by default.** Two unrelated applications receive *different* identifiers for the same person. Two owners who compare notes cannot tell that their two users are the same human.
* **The user can rotate their identifiers.** Identity continuity is a choice the user can revoke.
## Sectors: the unit of isolation
[Section titled “Sectors: the unit of isolation”](#sectors-the-unit-of-isolation)
Every application belongs to exactly one **sector**. A sector is the boundary that decides who shares a user’s identity:
```
flowchart TD
Account["One Sudomimus account"]
Account --> SectorA["Sector A"]
Account --> SectorB["Sector B"]
SectorA --> SubjectA["Sector subject A"]
SubjectA --> App1["Application 1"]
SubjectA --> App2["Application 2"]
SectorB --> SubjectB["Different sector subject B"]
SubjectB --> App3["Application 3"]
```
* Within a sector, a user has exactly **one** identifier, so two applications placed in the same sector see the **same** identifier for that user. This is deliberate — it lets one developer run a family of related products as a single identity (a game launcher and its companion app, say).
* Applications in **different** sectors see **unrelated** identifiers for the same user. No amount of comparing those identifiers reveals the shared person behind them.
By default each application gets its own fresh sector — **maximum isolation**. A developer who genuinely wants two of their applications to share a user’s identity opts in by placing both in one sector; nothing is shared until they ask for it. Sectors live inside an organization alongside your applications — see [Organizations and sectors](/en-us/concepts/organizations-and-sectors/).
Note
Because the per-user identifier is a function of *(user, sector)*, moving an application between sectors changes the identifier every one of its users presents. It is a deliberate, identity-severing operation — not a routine config tweak.
## The identifiers a user actually has
[Section titled “The identifiers a user actually has”](#the-identifiers-a-user-actually-has)
| Identifier | Who sees it | Rotatable | What it’s for |
| ------------------ | ------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Account alias** | The user, in their account portal. **Never the application.** | ✅ | A handle the user shares out of band with whoever configures an allow-list — so an operator can permit “this specific person” without the application ever learning the handle. |
| **Sector subject** | The application — it **is** the `sub` claim in the token. | ✅ | The application’s key for that user, and the value a developer allow-lists against in their own rules. Unique per *(user, sector)*. |
Both are **opaque, human-readable tokens** — a sector subject looks like `sub_9SQ5535CRWNDDM2T`, an account alias like `quiet-meadow-7h2k-9m4p-3fnp-falcon`. Applications are expected to treat them as opaque strings: never parse them, never assume a format. That is exactly what lets the format evolve later without breaking anyone.
### Rotation, and what it means
[Section titled “Rotation, and what it means”](#rotation-and-what-it-means)
* **Rotating the account alias** changes the handle the user hands out for allow-listing. The application never saw the old one and never sees the new one — only out-of-band allow-list configuration is affected.
* **Rotating the sector subject** changes the `sub` an application sees for that user. The previous subject stops resolving, so the application now sees what looks like a brand-new user. This is the user’s “forget my continuity with this product” control, exercised from their account portal.
Users inspect their current values in the With portal’s **External identifiers** view and perform rotations from [Privacy controls](/en-us/with-portal/privacy/), where each permanent consequence is confirmed explicitly.
## The other half: claim sharing
[Section titled “The other half: claim sharing”](#the-other-half-claim-sharing)
The privacy boundary provided by a pairwise identifier depends on the **strongest stable claim** an application still receives. If every application also receives the same real email address, applications can still correlate users by email; the pairwise `sub` cannot prevent that correlation.
Identity claims are therefore a **separate, user-controlled layer**. For each application, the user decides whether UserInfo may return their real **email**, **first name**, **last name**, or **avatar**:
* A developer declares, per claim, whether the application requests it: **off**, **optional**, **required**, **placeholder only**, or **fallback placeholder**.
* The user makes the final call on optional claims at sign-in and can **revoke a grant at any time** from the account portal. The next UserInfo request uses the new decision.
* An application sees optional or required claim fields only after the user grants them. Placeholder policies receive stable per-sector stand-ins instead of real data.
Together these two layers — pairwise identifiers plus per-application claim control — mean an application receives exactly the identity surface the user chose to give it, and nothing it can use to quietly find that same user somewhere else.
See [Identity claims and sharing](/en-us/concepts/identity-claims/) for the policy levels, consent screen, and OIDC scope rules.
[Explore how sectors, consent, and claim policy shape application identity in the Sudomimus Theater Trust Journey →](https://theater.sudomimus.com/trust-journey/)
# Authentication philosophy
> The threat model and design principles that underpin every other concept in Sudomimus.
Sudomimus treats every protocol boundary as independently exposed: browsers, application servers, native clients, relying parties, and the network between them may each be compromised. A flow should therefore avoid giving any one participant enough material to impersonate every other participant.
These principles apply across Connect, OIDC, and native direct-issue. Some sections use Connect as the concrete example; the protocol-specific mechanics live in the corresponding integration section.
## 1. Redemption proof is scoped to one session
[Section titled “1. Redemption proof is scoped to one session”](#1-redemption-proof-is-scoped-to-one-session)
Many authentication systems use a long-lived credential — OAuth’s `client_secret`, an API key, or a static signing key in the application’s environment. A leak may authorize repeated operations until the credential is rotated; the exact impact depends on the protocol’s other proofs and checks.
Connect avoids using a long-lived shared secret as sufficient proof to redeem every login. Each round-trip mints a **fresh `hiddenKey`** at `/establish`, uses it exactly once at `/redeem`, and then permanently consumes it. Leaking one hidden key compromises that redemption, not every login the application has ever handled.
This applies proof-of-possession with short, narrowly scoped secrets instead of long-lived shared secrets.
> **Excluded alternative:** using one long-lived `client_secret` to authenticate every token exchange.
## 2. Identity is separate from credentials
[Section titled “2. Identity is separate from credentials”](#2-identity-is-separate-from-credentials)
A Sudomimus **account** stores who someone is — a stable identity with a name. An **authentication method** stores how they prove it — a passkey, an email address, a Steam identity, an AccessKey credential. The two live in different records, and one account can hold many methods.
Consequences:
* **Email ownership is not embedded in the account record.** This removes one direct account-by-email data path. Enumeration resistance still depends on observable endpoint behavior, rate limits, and flow controls.
* **Switching auth methods doesn’t touch identity.** A user adding a passkey to their email-OTP account is just a new authentication record. The account stays the same, and so does anything tied to it.
* **There is no password column anywhere.** Sudomimus does not store passwords. It cannot leak passwords because it has none to leak. The supported methods are passkeys, email OTP, social sign-in (Google, GitHub, Discord, Battle.net, X), Steam, and AccessKey credentials, with more being added.
> **Excluded alternative:** treating email as the account identity or storing password records.
## 3. Identity is opaque, and different for every application
[Section titled “3. Identity is opaque, and different for every application”](#3-identity-is-opaque-and-different-for-every-application)
Your application receives a **pairwise identifier** — a stable, opaque `sub` that is unique to *your* application. The same person signing into two different applications presents two unrelated identifiers, and applications use only the identifier scoped to their own sector.
Consequences:
* **No cross-application correlation.** Two applications cannot collude to link “their” users into one real person by comparing identifiers.
* **The identifier is opaque by contract.** It is an exact-match token, not a structured value to parse. Sudomimus can change its internal format without breaking you, precisely because you were never meant to read anything out of it.
* **A leaked token leaks one application’s view** of a user — not a platform-wide identity.
> **Excluded alternative:** exposing one global user identifier to every relying party.
## 4. Users consent to what each application learns
[Section titled “4. Users consent to what each application learns”](#4-users-consent-to-what-each-application-learns)
Profile data is returned only by UserInfo, and only for claims the **user has agreed to share with that specific application**, plus any placeholder values the application’s policy explicitly asks for. Email, first name, last name, and avatar are each controlled independently, and a grant can be revoked at any time. UserInfo resolves policy and consent live on every call, so a revocation takes effect immediately.
Consequences:
* **A requested claim may be absent.** Applications must handle missing optional claims. If an application requires a claim, declare it required; Sudomimus blocks non-interactive issuance until the user consents rather than returning incomplete UserInfo.
* **Bearer credentials disclose less.** Access, refresh, and ID tokens contain no profile data; clients fetch current profile data from UserInfo only when needed.
> **Excluded alternative:** releasing a user’s full profile to every application after sign-in.
## 5. Verification is cryptographic, not relational
[Section titled “5. Verification is cryptographic, not relational”](#5-verification-is-cryptographic-not-relational)
When your application receives an access token, it is a **signed JWT**. To trust it, your application reads `kid` from the JOSE header and `aud` from the payload, selects that `kid` from the application’s Session JWK Set at `GET /applications/{applicationAnchor}/jwks.json`, and caches the set according to its response headers. Verification is local and does not require a Sudomimus request for every authenticated application request.
Consequences:
* **No additional network request** for each authenticated request — verification is local.
* **No availability dependency** between your service and Sudomimus once the user is signed in.
* **Tokens are deliberately short-lived** (access tokens default to a few hours, not days). When they expire, one HTTPS call to `/refresh` obtains a replacement without requiring the user to authenticate again.
OIDC ID tokens are a separate case: relying parties verify them against the JWKS at `oidc.sudomimus.com/.well-known/jwks.json`. See [Tokens and verification](/en-us/concepts/tokens-and-verification/) for the full picture.
> **Excluded alternative:** requiring a remote IdP session lookup for every authenticated request.
## 6. Access is allow-listed and default-deny
[Section titled “6. Access is allow-listed and default-deny”](#6-access-is-allow-listed-and-default-deny)
Who may complete an authentication, by which method, and how the result is returned are all governed by explicit allow-lists. An application with no configured rules authenticates **nobody**. Access remains closed until an administrator explicitly enables it.
> **Excluded alternative:** allowing access by default until an administrator adds restrictions.
## 7. Failure is scoped, not amplified
[Section titled “7. Failure is scoped, not amplified”](#7-failure-is-scoped-not-amplified)
A common anti-pattern is the **account lockout**: too many failed attempts freeze the account for a period. This can reduce brute-force attempts, but it also creates an account-level denial-of-service vector.
Each Sudomimus authentication session carries its own *life* counter. Failed attempts decrement that session’s life rather than locking the account. When the counter reaches zero, the session becomes invalid and the user can start a new session.
> **Excluded alternative:** account-level lockouts triggered by failures within one authentication session.
## 8. Three keys, three vantage points
[Section titled “8. Three keys, three vantage points”](#8-three-keys-three-vantage-points)
To forge a successful `/redeem` against Sudomimus, an attacker must simultaneously hold:
* A secret that lives only on the application server (the **hidden key**)
* A reference that was only ever sent to one specific browser (the **exposure key**)
* A proof that Sudomimus only mints after a real challenge succeeds (the **confirmation key**)
No single point of failure produces all three. A leaked URL does not compromise the server’s secret. A compromised server does not compromise other users’ sessions. A phished user does not compromise the server.
The mechanics are in [the three-key model](/en-us/connect/three-key-model/). The principle is general: split a proof three ways across three trust domains.
> **Excluded alternative:** a monolithic session token whose compromise grants all redemption authority.
## 9. Trust boundaries are enforced, not documented
[Section titled “9. Trust boundaries are enforced, not documented”](#9-trust-boundaries-are-enforced-not-documented)
Sudomimus has exactly six public authentication and integration surfaces — `connect-api.sudomimus.com`, `session-api.sudomimus.com`, `via.sudomimus.com`, `device-api.sudomimus.com`, `native-api.sudomimus.com`, `oidc.sudomimus.com`. Everything an integration can reach is exposed through one of these surfaces. Product support and account-management surfaces do not grant integration authority. Other services are unreachable from outside the platform, with that boundary enforced at the platform edge.
The practical consequence: there are a small number of well-defined paths through which an authentication can be completed, and integrations cannot bypass them.
> **Excluded alternative:** relying only on documentation or caller convention to protect an internal API.
## What this means for you
[Section titled “What this means for you”](#what-this-means-for-you)
An integration with Sudomimus has these properties:
* You never see a password, so you have nothing to store securely.
* You receive an opaque, per-application identifier rather than a global identifier that can be used for cross-site correlation.
* You only ever hold the identity claims a user agreed to share with you.
* Your application verifies tokens offline; Sudomimus availability doesn’t gate access to your own backend.
* A leaked per-session `hiddenKey` alone is bounded to its Connect session; long-lived application credentials still require careful custody and rotation.
* You don’t need to build account lockout logic.
* You still need ordinary enumeration and abuse defenses on application-owned endpoints; Sudomimus protects its own email-discovery flows at their protocol boundaries.
Next, [choose an integration path](/en-us/getting-started/choose-integration/) or inspect the concrete [Connect flow](/en-us/connect/flow/).
# Tokens and verification
> The full JWT claim reference for access tokens, refresh tokens, and OIDC ID tokens — what each carries, how to verify, TTL bounds, and how identity claims are selected.
Sudomimus issues three kinds of token, signed by different keys and verified through different mechanisms. This page is the single reference for verification and token contents — what the claims mean and the rules that decide their values.
## At a glance
[Section titled “At a glance”](#at-a-glance)
| Token | Issued by | Signed with | Verified via | Carries |
| ----------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
| **Access token** | Connect, Session API, Native API, or OIDC (`/redeem`, `/refresh`, `/direct-issue/*`, `/token`) | The **application’s active** token-signing private key | Session `GET /applications/{applicationAnchor}/jwks.json` by `kid` | Payload `iss`, `aud`, `sub`, `sid`, `jti`, `iat`, `exp`; Workload tokens also carry `act.sub` |
| **Refresh token** | Connect, Session API, Native API, or OIDC (`/redeem`, `/refresh`, `/direct-issue/*`, `/token`) | The **application’s active** token-signing private key | Same as access token | Payload `iss`, `aud`, `sid`, `jti`, `iat`, `exp`, `rotationVersion` |
| **OIDC ID token** | OIDC (`/token`) | A **platform-wide** OIDC signing key | `oidc.sudomimus.com/.well-known/jwks.json` | `sub`, `iss`, `aud`, `exp`, `iat`, `at_hash`, `nonce?`, `auth_time?`, `amr?`, `acr?` |
All three are JWTs signed with **RS256** (RSA-2048).
The response envelope is more than the token
This page is about what lives *inside* a token. The `/redeem`, `/refresh`, and `/direct-issue/*` responses that deliver those tokens also carry a top-level **`claims` block** beside them — a per-claim view of what the application requests and what the user has decided. Profile values themselves are fetched from UserInfo. See [the `claims` block](/en-us/concepts/identity-claims/#the-claims-block).
## Access and refresh tokens
[Section titled “Access and refresh tokens”](#access-and-refresh-tokens)
These are the tokens your application backend deals with day to day. Both are signed by Sudomimus using a keypair that is **specific to your application** — every application has its own pair, and rotating one application’s key has no effect on any other.
### Verification
[Section titled “Verification”](#verification)
Your application verifies signatures against its own **token-signing JWK Set**, published by Session API. The recommended flow is:
```
flowchart TD
Receive["Receive a JWT"] --> Parse["Parse as untrusted data"]
Parse --> Header{"Expected alg, typ, aud, non-empty kid, and valid exp?"}
Header -->|No| Reject["Reject"]
Header -->|Yes| Cached{"kid found in the cached application JWK Set?"}
Cached -->|Yes| Verify["Verify the signature"]
Cached -->|No| Refresh["Refresh the configured application JWK Set once"]
Refresh --> Found{"kid found now?"}
Found -->|No| Reject
Found -->|Yes| Verify
Verify --> Valid{"Signature valid?"}
Valid -->|No| Reject
Valid -->|Yes| Trust["Trust the verified claims"]
```
1. Fetch `GET https://session-api.sudomimus.com/applications/{applicationAnchor}/jwks.json` and cache the response according to its `Cache-Control` header.
2. For each incoming request, parse the JWT as untrusted data. Require `alg === "RS256"`, the expected `typ`, `aud === applicationAnchor`, a non-empty `kid`, and a valid `exp`.
3. Select the exact JWK whose `kid` matches the header and verify the signature. Only after verification may your application trust any claim.
4. If the `kid` is unknown, refresh the JWK Set once immediately before rejecting. Do not derive a fetch URL from an untrusted audience; use your configured application anchor and Session API origin.
The JWK Set may contain a prepublished next key, the active signer, and retiring keys that still verify outstanding tokens. Revoked and retention-expired keys are omitted. This overlap is what lets key rotation preserve verification for tokens already in circulation.
Emergency revocation removes a retiring key from fresh Session JWKS responses immediately. A verifier that already cached that public key may continue accepting its signatures for the remainder of the advertised cache lifetime, up to 300 seconds after its last successful fetch. This bounded convergence is part of offline JWT verification; do not interpret revocation as instantaneous at every cache-honoring verifier. Use Session `POST /introspect` in addition to signature verification when a sensitive operation requires current live session authority.
Application-token JWKS is intentionally **per application**, separate from the platform-wide OIDC JWKS. A verifier only receives keys belonging to its configured `applicationAnchor`, and rotating one application has no effect on another.
### The `typ` header
[Section titled “The typ header”](#the-typ-header)
Sudomimus access and refresh JWTs carry an explicit `typ` protected-header field:
* Account access tokens: `typ: "vnd.sudomimus.application-access+jwt"`
* Agent and Automation (Workload) access tokens: `typ: "vnd.sudomimus.workload-access+jwt"`
* Refresh tokens: `typ: "vnd.sudomimus.application-refresh+jwt"`
Reject a token whose `typ` does not match the credential your endpoint expects. An Account-only endpoint must reject Workload tokens even when their signatures are valid. To accept Agents or Automations, the application must both enable the exact [authentication method](/en-us/application-rules/authentication-rules/) and implement Workload token verification. Enabling an authentication method alone does not make an Account-only verifier accept actors.
### TTL bounds
[Section titled “TTL bounds”](#ttl-bounds)
| | Default | Minimum | Maximum |
| ------------- | ------------------ | -------------- | -------------------- |
| Access token | 3 hours (10800s) | 60 seconds | 7 days (604800s) |
| Refresh token | 30 days (2592000s) | 1 day (86400s) | 365 days (31536000s) |
Per-rule and per-inquiry overrides are subject to these bounds. When multiple TTLs apply (e.g. one from a Layer 1 rule and another from a Layer 3 inquiry constraint), Sudomimus folds them by taking the **minimum**. The access TTL is then reduced, if necessary, so that it never exceeds the refresh TTL.
### Access token claims
[Section titled “Access token claims”](#access-token-claims)
The protected header is deliberately small. Registered JWT claims and session bindings live in the payload. `sub` is the pairwise sector subject and the user key your application should use; `sid` is the stable logical ApplicationSession id; `jti` identifies one bearer instance. No application token contains profile data or a raw account id.
```json
// JWT header
{
"alg": "RS256",
"kid": "",
"typ": "vnd.sudomimus.application-access+jwt"
}
```
```json
// JWT payload
{
"iss": "https://sudomimus.com",
"aud": "",
"sub": "",
"sid": "",
"jti": "",
"iat": ,
"exp":
}
```
| Claim | Meaning |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `typ` (header) | `"vnd.sudomimus.application-access+jwt"` for Account access; `"vnd.sudomimus.workload-access+jwt"` for Agent or Automation access. Accept only the types your endpoint supports. |
| `act.sub` | Present only in Workload access tokens: the Agent or Automation’s pairwise sector subject. `act` contains exactly `sub`; Account tokens must omit `act`. |
| `sub` | The application-visible **sector subject** — a per-(account × sector) opaque identifier (e.g. `sub_9SQ5535CRWNDDM2T`). This is the value to key your users on. Stable for a given user within your sector, but rotatable by the user, and different across sectors. Treat it as opaque — do not parse it. |
| `sid` | The stable identifier of the logical ApplicationSession. Access and refresh tokens issued for one login share it across refresh rotations. It is not a user id. |
| `jti` | The unique identifier of this access-token instance. It is distinct from `sid` and changes whenever a new access token is issued. |
| `iss` | Sudomimus’s HTTPS application-token issuer. |
| `aud` | The `applicationAnchor` of the application this token was issued for. |
| `iat`, `exp` | Standard JWT issued-at / expiration, in seconds since epoch. |
### Workload actor identity
[Section titled “Workload actor identity”](#workload-actor-identity)
The access-token example above is an Account token. A Workload token uses the Workload `typ` and adds `"act": { "sub": "" }` to the payload. Its top-level `sub` still identifies the owner Account, while `act.sub` identifies the acting Agent or Automation within the sector. Validate the exact actor shape and keep both identities when making application authorization decisions. Neither identifier is a raw Account or Workload UUID. Authentication and credential scope do not grant business permissions inside your application.
Workload sessions use the same refresh-token type as Account sessions. Refresh tokens contain neither `sub` nor `act`; refreshing resolves the session’s current authority and returns the corresponding Account or Workload access-token type.
### Refresh token claims
[Section titled “Refresh token claims”](#refresh-token-claims)
The refresh token uses the same minimal JOSE header. Its payload deliberately omits `sub` and all profile fields; `sid`, `jti`, and `rotationVersion` identify one exact signed refresh version.
```json
// JWT header
{
"alg": "RS256",
"kid": "",
"typ": "vnd.sudomimus.application-refresh+jwt"
}
```
```json
// JWT payload
{
"iss": "https://sudomimus.com",
"aud": "",
"sid": "",
"jti": "",
"iat": ,
"exp": ,
"rotationVersion": 1
}
```
`rotationVersion` is a positive integer that increases by one on each successful rotation. Store the newly returned refresh JWT as one opaque credential; do not edit or derive a token from `sid`, `jti`, or the version. Sudomimus verifies the signature, strongly loads the ApplicationSession by `sid`, and exact-matches the application, `jti`, and version before it rotates anything. The internal session separately proves the current subject authority.
### Application UserInfo
[Section titled “Application UserInfo”](#application-userinfo)
Use `GET https://session-api.sudomimus.com/userinfo` with `Authorization: Bearer ` to fetch current consent-gated profile values. The response always carries `sub` and may carry `email`, `email_verified`, `name`, `given_name`, `family_name`, `picture`, and the private claim `picture_animated`. Treat these fields as live, replaceable profile data; key users only by payload `sub`.
Use `GET https://session-api.sudomimus.com/claim-state` with the same Bearer token when you need the live policy requirement and consent state without the profile values. Its claim keys are `email`, `given_name`, `family_name`, `picture`, and `picture_animated`.
## OIDC ID tokens
[Section titled “OIDC ID tokens”](#oidc-id-tokens)
If your application is integrated as an **OIDC relying party**, the `/token` endpoint additionally returns an `id_token` alongside `access_token`. The ID token is signed by a platform-wide OIDC signing key (not your per-application key) and is verified against the JWKS at `https://oidc.sudomimus.com/.well-known/jwks.json`.
ID token claims follow the OpenID Connect standard:
```json
{
"iss": "https://oidc.sudomimus.com",
"sub": "",
"aud": "",
"exp": ,
"iat": ,
"at_hash": "",
"nonce": "",
"auth_time": ,
"amr": [""],
"acr": ""
}
```
| Claim | Meaning |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `iss` | Always `"https://oidc.sudomimus.com"`. |
| `sub` | The **sector subject** — the same per-(account × sector) value carried as payload `sub` on the paired access token. Stable for a user within your sector, rotatable, and different across sectors. |
| `aud` | The OIDC `client_id` of the relying party. |
| `exp`, `iat` | Standard JWT lifetimes, in seconds since epoch. |
| `nonce` | Echoed from the relying party’s `/authorize` request on initial issuance. **Not** echoed on refresh-token grants, per [OIDC core 1.0 §12.1](https://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken). |
| `auth_time` | When the user actually authenticated, in seconds since epoch. Preserved across refresh-token grants. |
| `at_hash` | Binds the ID token to the paired access token. |
| `amr`, `acr` | Authentication method references and authentication context. |
OIDC signing keys are rotated periodically; the JWKS always publishes the keys for both the currently-active and the recently-retired key so verification continues to work during rotation.
### OIDC access tokens
[Section titled “OIDC access tokens”](#oidc-access-tokens)
OIDC `/token` returns the same minimal per-application access token described above. It contains no scope-dependent profile fields. Your application verifies it against the per-application Session JWKS and fetches scoped identity claims from the discovered OIDC `/userinfo` endpoint.
See [OIDC relying parties](/en-us/oidc/flow/) for the full RP flow, including `/userinfo` and `/end-session`.
## When to use which
[Section titled “When to use which”](#when-to-use-which)
* **Connect protocol plus Session API** (access / refresh tokens via the per-application Session JWKS): your own application backend talks to Sudomimus directly. Lowest overhead, no extra hop.
* **OIDC** (ID tokens via JWKS): your application uses an off-the-shelf OIDC library, or you want to integrate with a third party that already speaks OIDC. Sudomimus acts as the IdP.
You generally pick one or the other per application — there is no requirement to use both.
## Related
[Section titled “Related”](#related)
[OIDC relying parties](/en-us/oidc/flow/)The full RP integration — discovery, /authorize, /token, /userinfo, /end-session, scopes.
[Managing sessions](/en-us/guides/managing-sessions/)The session lifecycle after the initial login — /refresh, /introspect, /logout, /revoke-all.
[SDK overview](/en-us/sdk/overview/)SDKs and JOSE tools for access-token signature, typ, and expiry verification.
# Connect flow
> The Connect protocol from Establish through Authenticate and Redeem, plus the Session API refresh step for web applications.
This page covers the **Connect protocol**: the browser-mediated Sudomimus flow used when your application wants direct control over the login round-trip. Connect speaks JSON over HTTPS, so any backend language with an HTTP client works; examples below are in curl, Node.js, Python, and Go.
For production code, prefer an official SDK where one exists. Start with the [SDK overview](/en-us/sdk/overview/) or jump to the [TypeScript SDK](/en-us/sdk/typescript/) for `@sudomimus/connect`.
If you’re building a native client (desktop, game, CLI), see [Native clients](/en-us/native/overview/). For OIDC, see [OIDC relying parties](/en-us/oidc/flow/).
Tabs are synchronised across the page: pick your language once and every block below switches with it.
Before starting login, configure the rules and credentials for your chosen flow, then have an organization OWNER [take the application live](/en-us/with-portal/application-lifecycle/). New applications remain `DRAFT` until explicitly activated; login requires `ACTIVE` and available parent organization and sector.
## Protocol at a glance
[Section titled “Protocol at a glance”](#protocol-at-a-glance)
| Phase | Initiator | Endpoint | Result |
| ------------------- | ------------------- | ----------------------------- | -------------------------------------------- |
| **1. Establish** | Application backend | `connect-api POST /establish` | `{ exposureKey, hiddenKey }` |
| **2. Authenticate** | Browser | `via.sudomimus.com` | The user completes an allowed challenge |
| **3. Redeem** | Application backend | `connect-api POST /redeem` | `{ accessToken, refreshToken }` |
| **4. Refresh** | Application backend | `session-api POST /refresh` | A new access token and rotated refresh token |
The sequence below shows the standard `CALLBACK` path used by the examples on this page:
```
sequenceDiagram
autonumber
participant App as Application backend
participant Browser as User browser
participant Connect as Connect API
participant Via as via.sudomimus.com
participant Session as Session API
App->>Connect: POST /establish client-auth JWT + return method
Connect-->>App: exposureKey + hiddenKey
Note over App: Keep hiddenKey server-side
App-->>Browser: 302 redirect with exposureKey
Browser->>Via: Open hosted authentication UI
Note over Browser,Via: User completes an allowed challenge
Via-->>Browser: 302 to application callback exposureKey + confirmationKey
Browser->>App: GET application callback
App->>Connect: POST /redeem exposureKey + hiddenKey + confirmationKey
Connect-->>App: accessToken + refreshToken
App->>Session: GET application JWKS
Session-->>App: Public verification keys
Note over App: Verify the access token locally
loop Before the access token expires
App->>Session: POST /refresh current refreshToken
Session-->>App: new accessToken + rotated refreshToken
end
```
Three parties split responsibility:
* **Your backend** signs `/establish`, stores `hiddenKey`, redeems the completed inquiry, and verifies the resulting tokens.
* **The browser** carries `exposureKey` to the hosted authentication UI but never sees `hiddenKey`.
* **`via.sudomimus.com`** runs the passkey, email OTP, OAuth, or federation challenge and creates `confirmationKey` only after authentication succeeds.
The first three phases are specific to Connect. OIDC uses authorization code + PKCE, while native direct-issue exchanges a Steam ticket, AccessKey, or PublicKey in one request. After any ordinary application flow has a refresh token, the shared Session API owns refresh, introspection, logout, and revocation.
## 1. Establish — start a session
[Section titled “1. Establish — start a session”](#1-establish--start-a-session)
Your backend asks Connect to open an authentication session. The response gives you an **exposure key** (passed to the browser) and a **hidden key** (kept on the server).
`/establish` requires a signed client-auth JWT
Every `/establish` request must carry `Authorization: SudomimusClientJWT `, where `` is an RS256 JWT signed with your application’s client-auth private key. Required claims: `iss = applicationAnchor`, `aud = "sudomimus-connect"`, `iat`, `exp` (≤ 60s from `iat`), `jti` (UUID, replay-protected), `body_sha256` (base64 SHA-256 of the raw HTTP body).
Hand-rolling this in 4 lines of curl is impractical. The examples below assume `$SUDOMIMUS_CLIENT_AUTH_JWT` is produced by your signing code; in real integrations, use [`@sudomimus/connect`](/en-us/sdk/typescript/) which signs internally. Unsigned requests are rejected with HTTP 401.
`applicationAnchor` format
The anchor is the stable identifier you chose when creating the application at [`with.sudomimus.com`](https://with.sudomimus.com). It is lowercase kebab-case (`[a-z][a-z0-9-]*`, 3–64 characters, no leading/trailing/consecutive hyphens) and globally unique — for example `my-app` or `acme-checkout`.
* curl
```bash
curl -X POST https://connect-api.sudomimus.com/establish \
-H "Content-Type: application/json" \
-H "Authorization: SudomimusClientJWT $SUDOMIMUS_CLIENT_AUTH_JWT" \
-d '{
"applicationAnchor": "your-application",
"returnMethods": [
{
"type": "CALLBACK",
"payload": { "callbackUrl": "https://your-app.com/auth/callback" }
}
]
}'
```
* Node.js
```js
const body = JSON.stringify({
applicationAnchor: process.env.SUDOMIMUS_APPLICATION_ANCHOR,
returnMethods: [
{
type: "CALLBACK",
payload: { callbackUrl: "https://your-app.com/auth/callback" },
},
],
});
const res = await fetch("https://connect-api.sudomimus.com/establish", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `SudomimusClientJWT ${await signEstablishJwt(body)}`,
},
body,
});
const { exposureKey, hiddenKey } = await res.json();
```
* Python
```python
import json, os, requests
body = json.dumps({
"applicationAnchor": os.environ["SUDOMIMUS_APPLICATION_ANCHOR"],
"returnMethods": [
{
"type": "CALLBACK",
"payload": {"callbackUrl": "https://your-app.com/auth/callback"},
},
],
}, separators=(",", ":")).encode("utf-8")
res = requests.post(
"https://connect-api.sudomimus.com/establish",
headers={
"Content-Type": "application/json",
"Authorization": f"SudomimusClientJWT {sign_establish_jwt(body)}",
},
data=body,
)
data = res.json()
exposure_key = data["exposureKey"]
hidden_key = data["hiddenKey"]
```
* Go
```go
body, _ := json.Marshal(map[string]any{
"applicationAnchor": os.Getenv("SUDOMIMUS_APPLICATION_ANCHOR"),
"returnMethods": []map[string]any{{
"type": "CALLBACK",
"payload": map[string]any{
"callbackUrl": "https://your-app.com/auth/callback",
},
}},
})
req, _ := http.NewRequest(
"POST",
"https://connect-api.sudomimus.com/establish",
bytes.NewReader(body),
)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "SudomimusClientJWT "+signEstablishJwt(body))
res, err := http.DefaultClient.Do(req)
var data struct {
ExposureKey string `json:"exposureKey"`
HiddenKey string `json:"hiddenKey"`
}
json.NewDecoder(res.Body).Decode(&data)
```
Store `hiddenKey` against the user’s pending session (e.g. in a server-side store). Send the user to `via.sudomimus.com` with the `exposureKey` in the URL.
## 2. Authenticate — hand off to `via.sudomimus.com`
[Section titled “2. Authenticate — hand off to via.sudomimus.com”](#2-authenticate--hand-off-to-viasudomimuscom)
Redirect the user’s browser to `via.sudomimus.com` with the exposure key. The user completes the passkey or email-OTP challenge there.
* curl
```text
# No HTTP call — this is a 302 redirect from your application:
Location: https://via.sudomimus.com/?exposure-key=
```
* Node.js
```js
const authUrl = new URL("https://via.sudomimus.com/");
authUrl.searchParams.set("exposure-key", exposureKey);
return Response.redirect(authUrl.toString(), 302);
```
* Python
```python
from urllib.parse import urlencode
from flask import redirect
return redirect(
"https://via.sudomimus.com/?" + urlencode({"exposure-key": exposure_key}),
code=302,
)
```
* Go
```go
http.Redirect(
w, r,
"https://via.sudomimus.com/?exposure-key="+url.QueryEscape(exposureKey),
http.StatusFound,
)
```
When the user finishes, `via.sudomimus.com` redirects the browser to your concrete `callbackUrl` with `exposure-key` and `confirmation-key` appended as query parameters. Existing query parameters and fragments are preserved. If the URL already contains either reserved parameter, Connect overwrites it with the current Inquiry value; do not put key templates in the callback URL.
URL params are kebab-case
Query parameters travelling through the browser URL use kebab-case (`exposure-key`, `confirmation-key`). The JSON fields on API request/response bodies use camelCase (`exposureKey`, `confirmationKey`).
## 3. Redeem — exchange for a token
[Section titled “3. Redeem — exchange for a token”](#3-redeem--exchange-for-a-token)
In your callback handler, combine the three keys and exchange them at Connect for an access token plus a refresh token.
* curl
```bash
curl -X POST https://connect-api.sudomimus.com/redeem \
-H "Content-Type: application/json" \
-d '{
"exposureKey": "...",
"hiddenKey": "...",
"confirmationKey": "..."
}'
```
* Node.js
```js
// inside GET /auth/callback?exposure-key=...&confirmation-key=...
const { exposureKey, hiddenKey } = await loadPendingSession(req);
const confirmationKey = req.query["confirmation-key"];
const res = await fetch("https://connect-api.sudomimus.com/redeem", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ exposureKey, hiddenKey, confirmationKey }),
});
const { accessToken, refreshToken } = await res.json();
```
* Python
```python
# inside GET /auth/callback?exposure-key=...&confirmation-key=...
exposure_key, hidden_key = load_pending_session(request)
confirmation_key = request.args["confirmation-key"]
res = requests.post(
"https://connect-api.sudomimus.com/redeem",
json={
"exposureKey": exposure_key,
"hiddenKey": hidden_key,
"confirmationKey": confirmation_key,
},
)
data = res.json()
access_token = data["accessToken"]
refresh_token = data["refreshToken"]
```
* Go
```go
// inside GET /auth/callback?exposure-key=...&confirmation-key=...
exposureKey, hiddenKey := loadPendingSession(r)
confirmationKey := r.URL.Query().Get("confirmation-key")
body, _ := json.Marshal(map[string]string{
"exposureKey": exposureKey,
"hiddenKey": hiddenKey,
"confirmationKey": confirmationKey,
})
res, _ := http.Post(
"https://connect-api.sudomimus.com/redeem",
"application/json",
bytes.NewReader(body),
)
var data struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
}
json.NewDecoder(res.Body).Decode(&data)
```
The access token is a signed JWT. Read its `kid`, select that key from the application’s Session JWKS at `GET /applications/{applicationAnchor}/jwks.json`, verify the signature, and only then trust its claims. Require `typ: "vnd.sudomimus.application-access+jwt"` for Account-only endpoints. If an endpoint intentionally admits Agent or Automation tokens, also accept `typ: "vnd.sudomimus.workload-access+jwt"` and handle its `act.sub` actor. See [Tokens and verification](/en-us/concepts/tokens-and-verification/) for the full verification and cache-refresh recipe.
## 4. Refresh — keep the session alive
[Section titled “4. Refresh — keep the session alive”](#4-refresh--keep-the-session-alive)
Before the access token expires, exchange the refresh token at Session API for a fresh access token **and a new refresh token**. `/refresh` does not require a client-auth JWT. Refresh tokens are rotated — the token you present is consumed, and the response returns its replacement. Persist the new `refreshToken` and use it for the next refresh; re-using a spent one revokes the whole session. Near-simultaneous concurrent refreshes of the same token (e.g. multiple tabs) are tolerated and converge on one session; only reuse *after* the replacement has been issued revokes it.
* curl
```bash
curl -X POST https://session-api.sudomimus.com/refresh \
-H "Content-Type: application/json" \
-d '{ "refreshToken": "..." }'
```
* Node.js
```js
const res = await fetch("https://session-api.sudomimus.com/refresh", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refreshToken }),
});
// The refresh token is rotated — capture the new one and persist it,
// replacing the token you just sent.
const { accessToken, refreshToken: newRefreshToken } = await res.json();
await store.saveRefreshToken(newRefreshToken);
```
* Python
```python
res = requests.post(
"https://session-api.sudomimus.com/refresh",
json={"refreshToken": refresh_token},
)
data = res.json()
access_token = data["accessToken"]
# The refresh token is rotated — persist the new one, replacing the old.
store.save_refresh_token(data["refreshToken"])
```
* Go
```go
body, _ := json.Marshal(map[string]string{"refreshToken": refreshToken})
res, _ := http.Post(
"https://session-api.sudomimus.com/refresh",
"application/json",
bytes.NewReader(body),
)
var data struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
}
json.NewDecoder(res.Body).Decode(&data)
// The refresh token is rotated — persist data.RefreshToken, replacing the old one.
store.SaveRefreshToken(data.RefreshToken)
```
For introspection, logout, and account-wide revocation, see [Managing sessions](/en-us/guides/managing-sessions/).
## Looking up application metadata
[Section titled “Looking up application metadata”](#looking-up-application-metadata)
`POST /info` returns the localized public profile of an application given its anchor. It does **not** require a client-auth JWT, so it is safe to call from browsers and untrusted contexts. Signing keys deliberately live on the Session JWKS endpoint, not this metadata route.
* curl
```bash
curl -X POST https://connect-api.sudomimus.com/info \
-H "Content-Type: application/json" \
-d '{ "applicationAnchor": "your-application", "locale": "en-US" }'
```
* Node.js
```js
const res = await fetch("https://connect-api.sudomimus.com/info", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ applicationAnchor, locale: "en-US" }),
});
const { applicationAnchor: anchor, applicationName } = await res.json();
```
* Python
```python
res = requests.post(
"https://connect-api.sudomimus.com/info",
json={"applicationAnchor": application_anchor, "locale": "en-US"},
)
info = res.json()
```
* Go
```go
body, _ := json.Marshal(map[string]string{
"applicationAnchor": applicationAnchor,
"locale": "en-US",
})
res, _ := http.Post(
"https://connect-api.sudomimus.com/info",
"application/json",
bytes.NewReader(body),
)
```
Use `GET https://session-api.sudomimus.com/applications/{applicationAnchor}/jwks.json` for token verification keys. Cache that response according to `Cache-Control`, select the JWT’s exact `kid`, and refresh once when an unknown `kid` appears.
## Browser polling
[Section titled “Browser polling”](#browser-polling)
When your native client can open the user’s system browser but cannot easily receive a callback URL, use the polling flow:
1. The client backend calls `connect POST /establish` (signed with the application’s client-auth JWT) declaring a `STATUS_POLL` return method, and receives `{ exposureKey, hiddenKey }`.
2. The client opens the system browser pointed at `https://via.sudomimus.com/?exposure-key=`.
3. The user completes the passkey or email-OTP challenge in the browser.
4. The client **polls** `connect POST /status-poll` every few seconds with `{ exposureKey, hiddenKey }`. Once the user finishes, the poll returns `{ status: "REALIZED", confirmationKey }`.
5. The client then redeems the three keys at `connect POST /redeem` for `{ accessToken, refreshToken }`.
This works on any platform with a default browser — Windows, macOS, Linux desktop apps, Electron, etc. The application’s Layer 3 rules must allow `STATUS_POLL`.
The `/establish` call is the standard client-auth-signed Connect request — see [Web applications](/en-us/connect/flow/) for the full shape — except the return method is `STATUS_POLL`:
```bash
curl -X POST https://connect-api.sudomimus.com/establish \
-H "Content-Type: application/json" \
-H "Authorization: SudomimusClientJWT $SUDOMIMUS_CLIENT_AUTH_JWT" \
-d '{
"applicationAnchor": "your-application",
"returnMethods": [ { "type": "STATUS_POLL", "payload": {} } ]
}'
# → { "exposureKey": "exp_...", "hiddenKey": "hid_..." }
```
Then poll `/status-poll` with those two keys every few seconds. The poll carries **no** client-auth JWT — possession of the `hiddenKey` is what authorizes it:
```bash
curl -X POST https://connect-api.sudomimus.com/status-poll \
-H "Content-Type: application/json" \
-d '{
"exposureKey": "exp_...",
"hiddenKey": "hid_..."
}'
# While the user is still authenticating in the browser:
# { "status": "PENDING" }
# Once they finish:
# { "status": "REALIZED", "confirmationKey": "cnf_..." }
```
When the poll returns `REALIZED`, redeem the three keys at `connect POST /redeem` for the access and refresh tokens (same `/redeem` call as the web flow).
# The three-key model
> How Sudomimus proves a redemption is legitimate by splitting per-session proof across three keys — exposureKey, hiddenKey, and confirmationKey — each held by a different party.
The three-key model is the heart of Sudomimus’s session security for the browser-mediated flow. Every authentication round-trip through Connect mints **three distinct keys** at different moments, held by different parties, defending against different classes of attack. A successful `/redeem` must present all three.
Applies to the Connect browser flow
The three-key model is specific to the **Connect** lifecycle — the four-phase flow described in [the Connect flow](/en-us/connect/flow/). Native one-shot flows (`/direct-issue/steam-ticket`, `/direct-issue/access-key`) bypass it: the credential they present *is* the proof. OIDC uses its own authorization-code + PKCE shape. This page describes the shape that backs the Connect flow.
This is not about the long-lived material Sudomimus uses to sign tokens (covered in [Tokens and verification](/en-us/concepts/tokens-and-verification/)). It is about how a single browser-mediated login proves itself.
## The three keys
[Section titled “The three keys”](#the-three-keys)
| Key | Created by | Held by | Visible to |
| ------------------- | ------------------------------------------------- | ------------------------------------- | --------------------------------- |
| **exposureKey** | `/establish` | Application backend, then the browser | Browser, URL, `via.sudomimus.com` |
| **hiddenKey** | `/establish` | Application backend only | Backend |
| **confirmationKey** | `via.sudomimus.com`, after the user authenticates | Application backend (via callback) | Browser, URL |
`exposureKey` and `hiddenKey` are issued together as a **pair** by the same `/establish` call. They belong to one session, and only that session.
Each key carries a role-specific prefix so a misplaced key can be rejected at the request boundary:
* `exp_` + 32 lowercase hex characters
* `hid_` + 32 lowercase hex characters
* `cnf_` + 32 lowercase hex characters
## Why three keys, and not one
[Section titled “Why three keys, and not one”](#why-three-keys-and-not-one)
If a single opaque session reference were enough to redeem a token, anyone who saw that reference could redeem on the user’s behalf. Splitting the proof three ways means an attacker has to compromise **three different vantage points at the same time**:
* **Without `hiddenKey`** — an attacker who steals the URL the user is visiting still cannot redeem. The hidden key never leaves your server.
* **Without `exposureKey`** — an attacker who breaches your server still cannot redeem someone else’s session, because each pending session’s exposure key is bound to the specific browser it was sent to.
* **Without `confirmationKey`** — neither party can redeem a session the user never actually completed. The confirmation key is only minted by `via.sudomimus.com` after a real passkey or OTP challenge succeeds.
## Lifecycle
[Section titled “Lifecycle”](#lifecycle)
```
flowchart TD
Establish["POST /establish"] --> Exposure["exposureKey public session reference"]
Establish --> Hidden["hiddenKey server-side secret"]
Exposure -->|redirect URL| Browser["User browser"]
Hidden -->|kept by| Backend["Application backend"]
Browser -->|exposureKey| Via["via.sudomimus.com user authenticates"]
Via -->|authentication succeeds| Confirmation["confirmationKey authentication proof"]
Exposure -. carried through browser .-> Callback["Application callback"]
Confirmation -->|callback URL| Callback
Callback -->|exposureKey + confirmationKey| Redeem["POST /redeem"]
Backend -->|hiddenKey| Redeem
Redeem --> Tokens["accessToken + refreshToken"]
```
After `/redeem` succeeds, all three keys are consumed and cannot be reused. A second redemption with the same triple fails — even if the application repeats the request.
## Comparison with OAuth 2.0
[Section titled “Comparison with OAuth 2.0”](#comparison-with-oauth-20)
If you are familiar with the OAuth authorization-code flow, the rough analogues are:
| Sudomimus | OAuth 2.0 |
| ----------------- | --------------------------- |
| `exposureKey` | `state` parameter (loosely) |
| `confirmationKey` | authorization `code` |
| `hiddenKey` | *no direct equivalent* |
OAuth relies on the long-lived `client_secret` to authenticate the token exchange. Sudomimus instead uses a per-session `hiddenKey` that is fresh on every `/establish` call. Leaking one session’s hidden key compromises that single redemption — not every redemption your application ever performs.
This is the same idea as proof-of-possession tokens: prefer short-lived, narrowly-scoped secrets over long-lived shared ones.
If you actually want OAuth/OIDC semantics, Sudomimus also runs a [standard OIDC provider](/en-us/oidc/flow/) at `oidc.sudomimus.com`. The three-key model is what powers the Connect protocol underneath; relying parties using OIDC do not see it directly.
## The long-lived keypairs
[Section titled “The long-lived keypairs”](#the-long-lived-keypairs)
Aside from the three per-session keys, each application has two independent RSA-2048 key authorities:
* **Token-signing keys** — Sudomimus signs access and refresh JWTs with the active private key; the application selects the matching `kid` from the Session JWK Set at `GET /applications/{applicationAnchor}/jwks.json`.
* **Client-auth keypair** — the application signs every `/establish` request with its private half (delivered once at application creation and at each rotation); Sudomimus verifies with the stored public half.
Neither of these is part of the “three-key” model. They authenticate the *channel* between Sudomimus and the application; the three per-session keys authenticate a *single login*. The details are in [Tokens and verification](/en-us/concepts/tokens-and-verification/).
# Device authorization flow
> Use the Device API to sign in CLIs, launchers, and other public clients through a browser-confirmed user code.
Device authorization is for clients that cannot safely hold an application client-auth private key: CLIs, launchers, terminal tools, shared devices, and other public clients. The client asks Sudomimus for a short-lived `deviceCode` / `userCode` pair, shows the user a code, sends them to the browser, and polls until approval turns into ordinary Sudomimus application tokens.
For implementation, see the [SDK overview](/en-us/sdk/overview/) and the language page for your runtime. Follow the available language guides or the protocol examples below; confirm package availability in the SDK source documentation.
The flow follows the [OAuth 2.0 Device Authorization Grant (RFC 8628)](https://www.rfc-editor.org/rfc/rfc8628) model: the initiating client receives a device code and a user code, the user approves in a browser-capable user agent, and the client polls until the authorization is complete. Sudomimus keeps the standard device-flow shape while returning Sudomimus application tokens.
This is a separate integration path from Connect and Native direct-issue:
| Path | What proves the request |
| -------------------- | ------------------------------------------------------------------- |
| Connect | A client-auth JWT signed by the application’s private key |
| Native direct-issue | A platform credential such as a Steam ticket or AccessKey secret |
| Device authorization | A public client starts a code session; the browser user approves it |
The public HTTP surface is `device-api.sudomimus.com`. The browser approval page is still hosted by `via.sudomimus.com`.
Interactive demo
[Experience browser approval and device sign-in](https://theater.sudomimus.com/device-authorization/). Switch between phone, CLI, and TV views to follow the same device authorization. This local simulation does not perform real sign-ins or change your account.
## Protocol overview
[Section titled “Protocol overview”](#protocol-overview)
| Step | Actor | Endpoint | Result |
| ----------- | --------------- | ----------------------------------- | ----------------------------------------------------------------------------------------- |
| 1. Start | Client | `device-api POST /device-authorize` | `{ deviceCode, userCode, verificationUri, verificationUriComplete, expiresIn, interval }` |
| 2. Approve | User in browser | `via.sudomimus.com/device` | The user signs in, confirms the displayed code, and approves or denies |
| 3. Poll | Client | `device-api POST /device-token` | Pending state, polling instruction, denial, expiry, or `{ accessToken, refreshToken }` |
| 4. Continue | Client | `session-api POST /refresh` | Later token rotation uses the normal Session API operations |
```
sequenceDiagram
autonumber
participant Client as Public client
participant Browser as User browser
participant Device as Device API
participant Via as via.sudomimus.com
participant Session as Session API
Client->>Device: POST /device-authorize applicationAnchor
Device-->>Client: deviceCode + userCode verificationUri + interval
Client-->>Browser: Open verificationUri show userCode
par Browser approval
Browser->>Via: Open /device and confirm userCode
Note over Browser,Via: User authenticates and approves
Via-->>Browser: Approved, return to the client
and Client polling
loop Until approval completes
Client->>Device: POST /device-token deviceCode
Device-->>Client: authorization_pending or slow_down
end
end
Client->>Device: POST /device-token deviceCode
Device-->>Client: accessToken + refreshToken device session consumed
Client->>Session: Later: POST /refresh refreshToken
Session-->>Client: new accessToken + rotated refreshToken
```
Only the client sees `deviceCode`. Only the user sees and confirms `userCode`. A successful `/device-token` response consumes the device session, so the same `deviceCode` cannot mint another token pair.
## 1. Start device authorization
[Section titled “1. Start device authorization”](#1-start-device-authorization)
The client starts by posting its application anchor:
```bash
curl -X POST https://device-api.sudomimus.com/device-authorize \
-H "Content-Type: application/json" \
-d '{
"applicationAnchor": "your-application"
}'
```
Successful response:
```json
{
"applicationAnchor": "your-application",
"deviceCode": "dvc_...",
"userCode": "ABCD-EFGH",
"verificationUri": "https://via.sudomimus.com/device",
"verificationUriComplete": "https://via.sudomimus.com/device?user_code=ABCD-EFGH",
"expiresIn": 600,
"interval": 5
}
```
`/device-authorize` does **not** accept a client-auth JWT. The application opts in through configuration instead: it must have an enabled Layer 3 `DEVICE_CODE` ReturnRule. If that rule is missing, the request is refused.
## 2. Send the user to the browser
[Section titled “2. Send the user to the browser”](#2-send-the-user-to-the-browser)
Show the `userCode` clearly in the client UI and ask the user to open the browser approval page:
```text
Visit https://via.sudomimus.com/device
Enter code: ABCD-EFGH
```
When possible, open `verificationUriComplete` instead. It pre-fills the code and is also the best target for QR-code or clickable terminal output:
```text
https://via.sudomimus.com/device?user_code=ABCD-EFGH
```
The browser page displays the application and code again before approval. The user should compare the browser code with the code shown by the client. The browser never receives `deviceCode` and never displays access or refresh tokens; it only approves or denies the pending session.
## 3. Poll for tokens
[Section titled “3. Poll for tokens”](#3-poll-for-tokens)
The client polls `/device-token` with the private `deviceCode`:
```bash
curl -X POST https://device-api.sudomimus.com/device-token \
-H "Content-Type: application/json" \
-d '{
"deviceCode": "dvc_..."
}'
```
Poll no faster than the returned `interval`. While the user is still working, `/device-token` returns an OAuth-style polling error:
```json
{
"error": "authorization_pending"
}
```
After approval, the same endpoint returns ordinary Sudomimus application tokens:
```json
{
"applicationAnchor": "your-application",
"accessToken": "...",
"refreshToken": "...",
"claims": {
"email": { "requirement": "OPTIONAL", "state": "GRANTED" },
"firstName": { "requirement": "OFF", "state": "UNKNOWN" },
"lastName": { "requirement": "OFF", "state": "UNKNOWN" },
"staticAvatar": { "requirement": "SYNTHETIC_ONLY", "state": "UNKNOWN" },
"animatedAvatar": { "requirement": "OFF", "state": "UNKNOWN" }
}
}
```
The `claims` block explains the application’s claim policy joined with the user’s standing sharing decision. The actual token payload follows the normal Sudomimus token rules.
## 4. Use Session API for later token operations
[Section titled “4. Use Session API for later token operations”](#4-use-session-api-for-later-token-operations)
Device authorization only owns the initial public-client exchange. Once `/device-token` succeeds, the refresh token belongs to the normal Sudomimus application session model.
Use Session API token operations afterward:
* `POST /refresh` to rotate the refresh token and issue a new access token.
* `POST /logout` to terminally revoke the current ApplicationSession.
* `POST /introspect` to inspect a token.
* `POST /revoke-all` for application-level session revocation.
See [Managing sessions](/en-us/guides/managing-sessions/) for those operations and [Device polling and errors](/en-us/device/polling-and-errors/) for the polling state machine.
## Application configuration
[Section titled “Application configuration”](#application-configuration)
New applications start in `DRAFT`. Configure the rules below, then have an organization OWNER [take the application live](/en-us/with-portal/application-lifecycle/) before calling `/device-authorize`. Device authorization requires an `ACTIVE` application and available parent organization and sector.
An application must allow the browser-side authentication and realization checks it wants users to pass:
| Layer | Requirement |
| ------- | ------------------------------------------------------------- |
| Layer 1 | Existing authentication methods such as email OTP or passkeys |
| Layer 2 | Existing identity rules that allow the realized account |
| Layer 3 | A `DEVICE_CODE` ReturnRule |
`DEVICE_CODE` is a Layer 3 return method, not a new authentication method. The user still signs in through the application’s ordinary Layer 1 methods and is still checked against Layer 2 before the device session can be approved.
Available browser sign-in methods
The device approval page currently supports email OTP and passkeys. OAuth, Steam OpenID, and enterprise sign-in are not available in this flow yet. If your application needs one of those methods, use Connect or OIDC instead.
## When to choose this path
[Section titled “When to choose this path”](#when-to-choose-this-path)
Use device authorization when your client is public and cannot keep a client-auth private key secret. That is the usual fit for a CLI distributed to users, a launcher installed on a desktop, a terminal-only environment, or a shared-screen device that needs the user to finish sign-in on another browser-capable device.
Use [Connect browser polling](/en-us/connect/flow/#browser-polling) instead when you have a confidential application backend that can sign `/establish`. Use [Native direct-issue](/en-us/native/overview/) when the client has a platform credential such as a Steam ticket or a pre-issued AccessKey.
The raw endpoint contract is published in the [Device API reference](/en-us/api/device/).
# Device polling and errors
> Handle Device API polling states, single-use sessions, and the security boundaries around deviceCode and userCode.
Device authorization clients spend most of their time waiting. A good implementation treats `/device-token` as a state machine: pending means keep polling, `slow_down` means back off, terminal errors stop the flow, and success consumes the session.
```
stateDiagram-v2
state "Pending" as Pending
state "Back off" as SlowDown
state "Approved / tokens returned" as Approved
state "Denied" as Denied
state "Expired" as Expired
state "Failed" as Failed
[*] --> Pending: /device-authorize
Pending --> Pending: authorization_pending
Pending --> SlowDown: slow_down
SlowDown --> Pending: wait for returned interval
Pending --> Approved: HTTP 200 / tokens
Pending --> Denied: access_denied
Pending --> Expired: expired_token
Pending --> Failed: invalid_request or server_error
Approved --> [*]: session consumed
Denied --> [*]: stop polling
Expired --> [*]: start a new authorization
Failed --> [*]: stop polling
```
## Polling loop
[Section titled “Polling loop”](#polling-loop)
After `POST /device-authorize`, store `deviceCode`, show `userCode`, and start polling no faster than the returned `interval`.
```js
let intervalSeconds = authorize.interval;
while (true) {
await sleep(intervalSeconds * 1000);
const res = await fetch("https://device-api.sudomimus.com/device-token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ deviceCode: authorize.deviceCode }),
});
const body = await res.json();
if (res.ok) {
return body; // { accessToken, refreshToken, claims, ... }
}
if (body.error === "authorization_pending") continue;
if (body.error === "slow_down") {
intervalSeconds = body.interval ?? intervalSeconds + 5;
continue;
}
throw new Error(body.error);
}
```
The server may return `slow_down` if the client polls too quickly. When that happens, use the returned `interval` for subsequent polls.
## Polling errors
[Section titled “Polling errors”](#polling-errors)
`POST /device-token` uses the [RFC 8628 device-flow error vocabulary](https://www.rfc-editor.org/rfc/rfc8628#section-3.5) for polling states. Branch on `error`; do not expect Sudomimus `{ "reason": "..." }` wire reasons from this endpoint’s polling outcomes.
| Error | Meaning | Client behavior |
| ----------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `authorization_pending` | The user has not approved or denied yet. | Keep polling after the current interval. |
| `slow_down` | The client is polling too quickly. | Increase the interval; use `interval` if present. |
| `access_denied` | The user denied the request, approval failed, or policy no longer allows issuance. | Stop polling and show a denied/failed state. |
| `expired_token` | The device authorization session expired. | Stop polling; start again with `/device-authorize`. |
| `invalid_request` | The `deviceCode` is malformed, unknown, or already consumed. | Stop polling; start a new flow if appropriate. |
| `server_error` | Token issuance failed after approval. | Stop polling and report a retryable service failure. |
Successful responses use Sudomimus’s normal camelCase JSON shape. Polling failures intentionally use the device-flow `error` vocabulary so public clients can follow the familiar device-code state machine.
## Expiry and single use
[Section titled “Expiry and single use”](#expiry-and-single-use)
Each device authorization session is short lived. The production default is currently `expiresIn: 600` seconds, but clients should use the value returned by `/device-authorize` instead of hard-coding a lifetime.
Once `/device-token` succeeds, the session is consumed. Repeating the same `/device-token` request with the same `deviceCode` returns `invalid_request` and cannot mint another token pair.
## Code handling
[Section titled “Code handling”](#code-handling)
Treat the two codes differently:
| Value | Who sees it | Purpose |
| ------------ | ---------------------------------- | ---------------------------------------------- |
| `deviceCode` | Only the initiating client | High-entropy bearer secret for `/device-token` |
| `userCode` | The user and browser approval page | Short code the user compares and confirms |
Do not display `deviceCode`, put it in logs, embed it in a browser URL, or send it to your application backend unless that backend is the component polling `/device-token`. Losing `deviceCode` before expiry lets whoever holds it poll and consume the approved session.
`userCode` is safe to display, but it is not a token and cannot mint tokens by itself. It exists so the browser user can confirm they are approving the same client session shown in the terminal, launcher, or device UI.
## No client secret
[Section titled “No client secret”](#no-client-secret)
Device authorization is for public clients. `/device-authorize` does not require a client-auth JWT because the client could not protect the signing key. Instead, Sudomimus gates the flow on application configuration:
* The `applicationAnchor` must resolve to an enabled application.
* The application must have an enabled Layer 3 `DEVICE_CODE` ReturnRule.
* Browser approval still runs through the application’s Layer 1 authentication rules.
* The realized account is still checked by Layer 2 before approval can complete.
If you do have a confidential backend that can protect a client-auth private key, [Connect](/en-us/connect/flow/) or [Connect browser polling](/en-us/connect/flow/#browser-polling) may be a better fit.
## Claims and token operations
[Section titled “Claims and token operations”](#claims-and-token-operations)
`/device-token` returns the same kind of application access and refresh tokens as other Sudomimus flows. Claim sharing follows the application’s claim policy and the user’s grant state, just like Connect or Native direct-issue.
Device API has no refresh endpoint. After the initial exchange, use Session API:
* `session-api POST /refresh`
* `session-api POST /logout`
* `session-api POST /introspect`
* `session-api POST /revoke-all`
See [Managing sessions](/en-us/guides/managing-sessions/) for token lifecycle calls and the [Device API reference](/en-us/api/device/) for exact request and response schemas.
# Adopt a domain
> Claim a domain for your organization on Sudomimus by proving DNS control, binding it exclusively to the organization as the basis for login policy and federation.
If your organization owns a domain (`example.com`, your company’s domain, a personal domain you control), an **owner** of that organization can **adopt** it on Sudomimus. Once adopted, the domain belongs to your organization on the platform: it is bound exclusively to the organization that proved control of it.
Adoption is the **foundation** the rest of this section builds on — it is the ownership primitive that a [login policy](/en-us/domains-federation/domain-login-policy/) and [enterprise federation](/en-us/domains-federation/sign-in-with-your-idp/) attach to. On its own, an adopted domain has no effect on any sign-in flow; it becomes load-bearing the moment you give it a policy.
## What an adopted domain gives you
[Section titled “What an adopted domain gives you”](#what-an-adopted-domain-gives-you)
* **Exclusive ownership.** A verified domain is bound to a single organization. No other organization can hold a verified claim on the same domain while you hold it.
* **A mount point for policy.** Once verified, the domain can carry a [login policy](/en-us/domains-federation/domain-login-policy/) (allow / block / force SSO) and can be bound to one of your [federation connectors](/en-us/domains-federation/federation-connectors/).
## Why this exists
[Section titled “Why this exists”](#why-this-exists)
Most authentication products treat domain ownership as a paid enterprise feature. In Sudomimus it is a first-class primitive — the platform already needs to verify which email addresses belong to whom in order to support email OTP, and domain adoption is a natural extension of that model.
It is particularly useful when:
* You run a company whose employees share a domain.
* You operate a community or organization whose members share a domain.
* You want a verified, organization-owned claim on your domain as the basis for policy and federation.
## The adoption flow
[Section titled “The adoption flow”](#the-adoption-flow)
1. **Initiate adoption** from the With portal at [`with.sudomimus.com`](https://with.sudomimus.com): pick one of your organizations and claim the domain you want against it. Only an organization owner can start an adoption.
2. **Publish the DNS record.** Sudomimus gives you a one-line `TXT` record to add to the domain:
| Field | Value |
| ----------- | -------------------------------------------- |
| Host / name | `_sudomimus-challenge.example.com` |
| Type | `TXT` |
| Value | `sudomimus-domain-verification=` |
The dedicated `_sudomimus-challenge` subhost keeps the record clear of any apex SPF / DKIM / other `TXT` records you already publish. The token is fixed for the life of the claim.
3. **Verify.** Back in the portal, click **Verify**. Sudomimus performs one live DNS lookup, compares the record, and on success binds the domain exclusively to your organization. The claim moves from `PENDING` to `VERIFIED`.
DNS takes a moment to propagate
Verification is on-demand and synchronous — there is no background polling. If you click **Verify** before the record has propagated (or while a stale “no such record” answer is still cached by the resolver), it can fail for a short while. Wait a minute and retry.
## Contested domains and exclusivity
[Section titled “Contested domains and exclusivity”](#contested-domains-and-exclusivity)
* **Pending claims may coexist.** Two organizations can each have a `PENDING` claim on the same domain — initiation is not exclusive.
* **Verification is the exclusive gate.** Only one organization can hold a `VERIFIED` claim at a time: whoever proves DNS control first wins the single verified slot. (This is deliberate — it stops anyone from “squatting” a competitor’s domain in pending state.)
* **Verifying a domain another organization already holds** fails with `DomainAlreadyAdopted`.
## Releasing a domain
[Section titled “Releasing a domain”](#releasing-a-domain)
Releasing a domain is permanent for that claim. The same organization or another organization may adopt the domain again later, but that creates a new claim and requires DNS verification again.
An owner may release a pending claim. Releasing a verified domain requires the organization’s sole owner. The release frees one active-domain quota slot.
Caution
Before releasing a verified domain, set its [login policy](/en-us/domains-federation/domain-login-policy/) to `ALLOW_ALL`. Sudomimus will not silently remove a blocking or SSO policy during release.
## Quota
[Section titled “Quota”](#quota)
Each organization can hold a limited number of active domain claims (default **3**, counting pending and verified together). Releasing a claim frees an active slot, and Sudomimus staff can raise this active limit on request.
There is also a lifetime limit of **32 domain claims per organization**. Released claims still count toward this limit, so do not repeatedly release and re-adopt a domain as a routine configuration change.
## Domain format
[Section titled “Domain format”](#domain-format)
Sudomimus accepts standard ASCII domains with at least two labels (`example.com`, `mail.example.co.uk`). Internationalized (punycode / `xn--`) domains are not accepted yet. Apex and subdomains are independent claims — adopting `example.com` does **not** adopt `mail.example.com`.
## Related
[Section titled “Related”](#related)
[Domain login policy](/en-us/domains-federation/domain-login-policy/)Now that the domain is verified, decide how its users may authenticate.
[Federation connectors](/en-us/domains-federation/federation-connectors/)Register the OIDC or SAML identity provider you will bind this domain to.
# Configure an OIDC connector
> Register Sudomimus as a confidential OIDC client at your identity provider, create the connector, and verify enterprise sign-in end to end.
This guide configures an external corporate identity provider as an **OIDC federation connector**. Sudomimus is the relying party: it redirects the browser to your IdP, validates the returned ID token, and then runs the normal Layer 1, Layer 2, and issuance pipeline.
## Before you begin
[Section titled “Before you begin”](#before-you-begin)
You need:
* an organization in the [With portal](https://with.sudomimus.com/);
* permission to create an application at your IdP;
* an IdP that supports OpenID Connect discovery, Authorization Code flow, confidential clients, PKCE `S256`, and RS256-signed ID tokens; and
* a client secret issued by that IdP.
Decide whether the connector will appear as an application-specific **“Sign in with …”** button, enforce SSO for a verified domain, or both. The same connector supports both modes.
## 1. Create the client at your IdP
[Section titled “1. Create the client at your IdP”](#1-create-the-client-at-your-idp)
Create a **web** or **confidential** OIDC application at your IdP. Provider terminology differs, but configure these values:
| IdP setting | Value |
| --------------------- | -------------------------------------------------- |
| Application type | Web / confidential client |
| Grant or flow | Authorization Code |
| Redirect URI | `https://federation.sudomimus.com/oidc/callback` |
| PKCE | Enabled; `S256` must be accepted |
| Client authentication | Client secret |
| Scopes | At least `openid`; normally `openid email profile` |
The redirect URI must match exactly. Do not register the Core UI URL: provider responses terminate at Auth API.
Record the following values before leaving the IdP:
* the exact **issuer URL**;
* the **client ID**; and
* the **client secret**.
Issuer and discovery
Sudomimus reads `/.well-known/openid-configuration` when the connector is saved. The discovery document must report that exact issuer and expose safe HTTPS authorization, token, and JWKS endpoints.
## 2. Configure claims
[Section titled “2. Configure claims”](#2-configure-claims)
Every ID token must contain a stable, non-empty `sub`. Sudomimus also validates `iss`, `aud`, `exp`, `iat`, the per-login `nonce`, the RS256 signature, and its `kid` against the discovered JWKS.
The following standard claims are optional but useful:
| Claim | Use |
| -------------------------------- | -------------------------------------------------------- |
| `email` + `email_verified: true` | Candidate email for account matching and email ownership |
| `given_name` | First-name profile value |
| `family_name` | Last-name profile value |
An IdP-verified email is not automatically trusted by Sudomimus. It becomes usable for email ownership and account matching only when the connector’s organization has a current verified claim for that email domain.
## 3. Create the connector in With
[Section titled “3. Create the connector in With”](#3-create-the-connector-in-with)
In the With portal:
1. Open your organization.
2. Open **Federation connectors** and choose **New connector**.
3. Select **OpenID Connect**.
4. Enter a display name, issuer URL, client ID, and client secret.
5. Enter space- or newline-separated scopes. `openid` is required; `openid email profile` is the usual starting point.
6. Create the connector.
Sudomimus immediately validates the issuer and discovery document. A failure to reach or validate it is reported as `FederationConnectorDiscoveryFailed`.
The client secret is write-only. It is encrypted when saved and never returned by the connector API or portal. Keep the original value in your secret manager; enter a new value only when rotating it.
## 4. Put the connector to work
[Section titled “4. Put the connector to work”](#4-put-the-connector-to-work)
Choose one or both modes:
[Application-managed sign-in](/en-us/domains-federation/sign-in-with-your-idp/#application-managed-sign-in)Add an ENTERPRISE\_FEDERATION\_APPLICATION\_MANAGED Layer 1 rule that names this connector.
[Domain-managed SSO](/en-us/domains-federation/sign-in-with-your-idp/#forced-sso-domain-managed)Verify a domain, bind this connector to SSO\_ONLY, and allow ENTERPRISE\_FEDERATION\_DOMAIN\_MANAGED on participating applications.
The protocol is already fixed by the connector. Neither Layer 1 rule needs a separate OIDC/SAML switch.
## 5. Test the login
[Section titled “5. Test the login”](#5-test-the-login)
Use a non-administrator test user whose IdP account has the claims you configured.
For application-managed sign-in, start an Inquiry for the application and choose the connector’s **“Sign in with …”** button. For domain-managed SSO, enter an email on the verified `SSO_ONLY` domain and continue through the required connector.
Verify that:
* the browser reaches the expected IdP tenant;
* the IdP returns to the fixed Auth API callback;
* the login reaches the application’s normal completion path; and
* Layer 2 admits the test identity. Use `EVERYONE` for a protocol smoke test, or ensure an `EMAIL` rule matches an email Sudomimus can trust.
If saving succeeds but login fails, first check the registered redirect URI, the ID token’s `nonce`, audience and issuer, RS256 signing, JWKS `kid`, and whether the configured scopes actually release the expected claims. Browser-facing federation failures are intentionally generic.
## Related
[Section titled “Related”](#related)
[Federation connector reference](/en-us/domains-federation/federation-connectors/)Field semantics, lifecycle rules, validation, and secret handling.
[Configure a SAML connector](/en-us/domains-federation/configure-saml-connector/)Use SAML 2.0 instead of OIDC for the same enterprise-federation modes.
# Configure a SAML connector
> Register Sudomimus as a SAML service provider at your identity provider, create the connector, and verify a signed assertion end to end.
This guide configures an external corporate identity provider as a **SAML 2.0 federation connector**. Sudomimus is the service provider (SP): it sends an AuthnRequest to your IdP, validates the signed assertion posted to its ACS, and then runs the same Layer 1, Layer 2, and issuance pipeline used by OIDC federation.
## Before you begin
[Section titled “Before you begin”](#before-you-begin)
You need:
* an organization in the [With portal](https://with.sudomimus.com/);
* permission to create a SAML application at your IdP;
* the IdP’s entity ID and SP-initiated SSO URL; and
* the public X.509 certificate for the key the IdP uses to sign assertions.
Sudomimus supports SP-initiated SAML browser SSO only. The initial AuthnRequest is sent with HTTP-Redirect binding and the response must return with HTTP-POST binding.
## 1. Create the SAML application at your IdP
[Section titled “1. Create the SAML application at your IdP”](#1-create-the-saml-application-at-your-idp)
Create a custom SAML 2.0 application and register these production SP values:
| IdP setting | Value |
| ----------------------- | ------------------------------------------- |
| SP Entity ID / Audience | `urn:sudomimus:production:federation-sp` |
| ACS / Reply URL | `https://federation.sudomimus.com/saml/acs` |
| Initiation mode | SP-initiated |
| Response binding | HTTP-POST |
| Assertion signing | Required |
The ACS must match exactly. The audience in the assertion must be the SP Entity ID above. If you are configuring a non-production environment, use the read-only SP Entity ID and ACS displayed for that environment instead.
Sign the assertion
Sudomimus requires a signed **Assertion**. Signing only the outer SAML Response is insufficient. Do not enable encrypted assertions; they are not supported.
## 2. Configure subject and profile attributes
[Section titled “2. Configure subject and profile attributes”](#2-configure-subject-and-profile-attributes)
The stable external identity normally comes from SAML `NameID`. Configure `NameID` to be stable for the lifetime of the user and unique within this IdP application. Do not use a mutable display name.
You may also release profile values as SAML attributes. Record the exact attribute names, including URI-style names if your IdP uses them:
| Suggested purpose | Example attribute name |
| ----------------- | ---------------------- |
| Email | `mail` |
| Given name | `given_name` |
| Family name | `family_name` |
The names are examples, not required literals. The values entered in With must exactly match the attribute names emitted in the assertion.
An IdP-provided email is not automatically trusted by Sudomimus. It becomes usable for email ownership and account matching only when the connector’s organization has a current verified claim for that email domain.
## 3. Collect the IdP values
[Section titled “3. Collect the IdP values”](#3-collect-the-idp-values)
From the IdP, copy:
* the exact **IdP Entity ID** used as the assertion issuer;
* the HTTPS **IdP SSO URL** that accepts AuthnRequests; and
* the active assertion-signing certificate.
Export the certificate as PEM-encoded X.509 public certificate data, including the delimiters:
```text
-----BEGIN CERTIFICATE-----
MIIC...
-----END CERTIFICATE-----
```
Do not paste a private key. During certificate rollover, keep both the old and new public certificates configured for the overlap window before removing the retired certificate.
## 4. Create the connector in With
[Section titled “4. Create the connector in With”](#4-create-the-connector-in-with)
In the With portal:
1. Open your organization.
2. Open **Federation connectors** and choose **New connector**.
3. Select **SAML 2.0**.
4. Enter a display name, IdP Entity ID, IdP SSO URL, and signing certificate.
5. Optionally enter the exact email, given-name, and family-name attribute names.
6. Create the connector.
The With portal creates new SAML connectors with `NameID` as the stable subject source. The protocol, IdP Entity ID, and subject source define the credential namespace and cannot be changed later; create a new connector if one of them must change.
The connector detail page shows the authoritative SP Entity ID and ACS again. Compare them with the values registered at the IdP before testing.
## 5. Put the connector to work
[Section titled “5. Put the connector to work”](#5-put-the-connector-to-work)
Choose one or both modes:
[Application-managed sign-in](/en-us/domains-federation/sign-in-with-your-idp/#application-managed-sign-in)Add an ENTERPRISE\_FEDERATION\_APPLICATION\_MANAGED Layer 1 rule that names this connector.
[Domain-managed SSO](/en-us/domains-federation/sign-in-with-your-idp/#forced-sso-domain-managed)Verify a domain, bind this connector to SSO\_ONLY, and allow ENTERPRISE\_FEDERATION\_DOMAIN\_MANAGED on participating applications.
The protocol is already fixed by the connector. Neither Layer 1 rule needs a separate OIDC/SAML switch.
## 6. Test the login
[Section titled “6. Test the login”](#6-test-the-login)
Use a non-administrator test user and start the appropriate application-managed or domain-managed flow.
Verify that:
* the browser receives an SP-initiated AuthnRequest at the configured IdP;
* the IdP posts the response to the exact ACS;
* the assertion issuer, audience, recipient, and `InResponseTo` match the flow;
* the assertion contains one AuthnStatement and is within its validity window;
* the assertion, not only the response, is signed by a configured certificate; and
* Layer 2 admits the resulting identity.
For an initial protocol smoke test, use an `EVERYONE` Layer 2 rule. If you use an `EMAIL` rule, ensure the mapped email attribute is present and its domain is verified by the connector’s organization.
Sudomimus rejects replayed requests and assertions. Browser-facing failures are intentionally generic, so configuration troubleshooting should start with the IdP’s SAML response inspector and the checklist above.
## Unsupported settings
[Section titled “Unsupported settings”](#unsupported-settings)
Do not enable IdP-initiated SSO, encrypted assertions, Single Logout, artifact binding, metadata import, requested authentication context policy, or signed AuthnRequests. These profiles are not currently accepted.
## Related
[Section titled “Related”](#related)
[Federation connector reference](/en-us/domains-federation/federation-connectors/)Field semantics, lifecycle rules, validation, and certificate rollover.
[Configure an OIDC connector](/en-us/domains-federation/configure-oidc-connector/)Use OpenID Connect instead of SAML for the same enterprise-federation modes.
# Domain login policy
> For users whose email is on a verified domain you own, decide platform-wide whether logins are allowed, blocked, or forced through your identity provider.
Once your organization holds a [verified domain](/en-us/domains-federation/adopt-a-domain/), you can give it a **login policy**. The policy governs every account that owns a verified email address on that domain, **platform-wide** — on every application, not just your own. Because you proved DNS control of the domain, Sudomimus treats you as authoritative over how that email namespace signs in.
## The three policies
[Section titled “The three policies”](#the-three-policies)
| Policy | Effect on accounts with an email on this domain |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`ALLOW_ALL`** | No restriction. This is the default for every verified domain (and what an absent policy means). |
| **`BLOCK_ALL`** | Every login is refused, platform-wide, regardless of method. |
| **`SSO_ONLY`** | Every login must go through a specific [federation connector](/en-us/domains-federation/federation-connectors/) — your IdP. Every other method (passkey, email OTP, consumer OAuth, Steam, native keys) is refused. |
`SSO_ONLY` is covered end-to-end in [Sign in with your IdP](/en-us/domains-federation/sign-in-with-your-idp/#forced-sso-domain-managed); the rest of this page focuses on how all three behave.
Interactive demo
[Explore enterprise sign-in policies](https://theater.sudomimus.com/enterprise-sso/). Switch domain policies and observe sign-in routes and rejection reasons. This local simulation does not perform real sign-ins or change your account.
## How enforcement works
[Section titled “How enforcement works”](#how-enforcement-works)
The policy is an **upper gate** evaluated at the moment a login is realized, with one important property:
```
flowchart TD
Start["Account is active"] --> Policies["Collect policies for every verified email domain"]
Policies --> Block{"Any BLOCK_ALL policy?"}
Block -->|Yes| Blocked["Reject EmailDomainBlocked"]
Block -->|No| Authorities{"How many distinct SSO_ONLY connectors?"}
Authorities -->|More than one| Conflict["Reject SsoAuthorityConflict"]
Authorities -->|Exactly one| Satisfied{"Did this login use that connector?"}
Satisfied -->|No| RequiresSso["Reject EmailDomainRequiresSso"]
Satisfied -->|Yes| AppRules["Continue to application Layer 2 and Layer 3 rules"]
Authorities -->|None| AppRules
```
Account-taint, not email-of-the-moment
If **any** of an account’s verified emails sits on a `VERIFIED` domain with a `BLOCK_ALL` or `SSO_ONLY` policy, the **whole account** is governed by that policy — on every login, with every method, to every application. It does not matter which email the user typed this time, or whether they logged in with a passkey that has no email at all. The taint follows the account, not the request.
Different SSO authorities cannot be combined
Several domains may require the same connector; that remains one satisfiable SSO requirement. If verified emails on the account belong to domains that require **different** connectors, Sudomimus cannot choose A without bypassing B, or choose B without bypassing A. The account cannot start a login or receive fresh tokens until an administrator aligns a domain policy, a conflicting verified email is removed, or the identities are separated into different accounts. The login UI explains the conflict and does not offer a connector picker.
This mirrors how account-level disable works. The gate runs **after** the account-active check and **before** the identity (Layer 2) check, so it is genuinely a platform-wide policy, not a per-application rule.
A login the policy refuses is rejected with the wire reason `EmailDomainBlocked` (for `BLOCK_ALL`), `EmailDomainRequiresSso` (for one unsatisfied `SSO_ONLY` connector), or `SsoAuthorityConflict` (for distinct connector pins).
## What a policy does and does not do
[Section titled “What a policy does and does not do”](#what-a-policy-does-and-does-not-do)
* **It governs authentication, not authorization.** A login that satisfies `SSO_ONLY` still has to pass the application’s [Layer 2 realize rules](/en-us/application-rules/realize-rules/) and [Layer 3 return rules](/en-us/application-rules/return-rules/). Authentication ≠ authorization — forcing SSO does not grant access, it only constrains *how* a user proves who they are.
* **It does not revoke already-issued access tokens in place.** An access token that was already minted keeps working until it expires by TTL (3 hours by default). Refresh-token reissuance is different: Session API `/refresh` and the OIDC refresh-token grant re-check the current domain login policy. `BLOCK_ALL`, an `SSO_ONLY` connector change that the session’s original login no longer satisfies, or distinct `SSO_ONLY` pins deny the next refresh instead of allowing the session to keep rotating for the full refresh TTL.
* **It does not touch the account’s email ownership.** Reverting to `ALLOW_ALL` restores normal access — nothing about the account was deleted.
## Setting a policy
[Section titled “Setting a policy”](#setting-a-policy)
The login policy is set from the With portal, on the verified domain’s detail page (a **Login policy** tab). It can only be changed by the **sole owner** of the organization that owns the domain — if your organization has more than one owner, no single owner can unilaterally change how everyone on the domain signs in.
You can lock yourself out
The policy applies to **every** application, including the With portal itself, and there is **no** exemption for your own login. If you set `BLOCK_ALL` (or a misconfigured `SSO_ONLY`) on the domain of your *own* login email, you will lock yourself out of Sudomimus and will need staff to un-block you. The portal warns you before you do this — read the warning.
## Related
[Section titled “Related”](#related)
[Adopt a domain](/en-us/domains-federation/adopt-a-domain/)A login policy requires a verified domain — start here.
[Sign in with your IdP](/en-us/domains-federation/sign-in-with-your-idp/)The SSO\_ONLY policy in full: forcing a domain's users through your connector.
# Federation connectors
> Register your organization's OIDC or SAML identity provider as a reusable connector, then use it to offer enterprise sign-in or force a domain through SSO.
A **federation connector** is your organization’s own external OIDC or SAML identity provider, registered with Sudomimus once and reused everywhere. If your company runs Microsoft Entra ID, Okta, Google Workspace, Ping, Auth0, or another standards-compliant provider, you register it as a connector and then either:
* offer it as a **“Sign in with …” button** on one of your applications ([application-managed](/en-us/domains-federation/sign-in-with-your-idp/#application-managed-sign-in)), or
* **force** a verified domain’s users through it ([domain-managed / forced SSO](/en-us/domains-federation/sign-in-with-your-idp/#forced-sso-domain-managed)).
The connector is the shared mount point for both. Sudomimus acts as an OIDC **relying party** or SAML **service provider** against your IdP. Your provider stays the source of truth for those identities.
## What a connector stores
[Section titled “What a connector stores”](#what-a-connector-stores)
You register a connector from the With portal on one of your organizations. Every connector has a display name and immutable protocol. Its remaining fields depend on that protocol.
### OIDC
[Section titled “OIDC”](#oidc)
| Field | What it is |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Display name** | Shown on the login button and in management UIs (e.g. “Acme Corp SSO”). |
| **Issuer** | Your IdP’s issuer URL (`https://login.acme.example`). Sudomimus fetches its OIDC discovery document from `/.well-known/openid-configuration`. |
| **Client ID** | The OAuth client ID Sudomimus presents to your IdP. |
| **Client secret** | The confidential client secret. **Encrypted at rest** and **write-only** — see below. |
| **Scopes** | The scopes Sudomimus requests; must include `openid`. |
Each connector is addressed by an opaque **connector anchor** (a value like `Bastion-K7Q2-M9XB-3FNP-Covenant`) — the developer-facing identifier you reference from rules and policies.
Confidential client + PKCE
Sudomimus authenticates to your IdP as a **confidential client** using the client secret, and uses **PKCE (S256)** on every authorization. `private_key_jwt` and public (secret-less) clients are not supported yet — the providers Sudomimus targets first (Entra, Okta, Workspace) all support confidential + PKCE.
### The client secret is never revealed back to you
[Section titled “The client secret is never revealed back to you”](#the-client-secret-is-never-revealed-back-to-you)
The client secret is encrypted the moment you save it. On every read — the connector list, the detail page, the API — it is simply **absent**: the portal can never display it back to you. To rotate it, supply a new secret; to keep the existing one when editing other fields, leave the secret blank.
### SAML
[Section titled “SAML”](#saml)
| Field | What it is |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| **IdP entity ID** | The exact issuer Sudomimus accepts in SAML assertions. |
| **IdP SSO URL** | The HTTPS endpoint that receives SP-initiated AuthnRequests. |
| **Signing certificates** | One or more IdP signing certificates. Keep both old and new certificates during a rollover window. |
| **Subject source** | Either assertion `NameID` or one named attribute. This is immutable because it defines the external credential identity. |
| **Profile attributes** | Optional attribute names for email, given name, and family name. |
The connector detail page shows Sudomimus’s fixed SP entity ID and Assertion Consumer Service (ACS) URL. Register both values at your IdP. SAML connectors have no client secret.
The supported profile is SAML 2.0 SP-initiated browser SSO: HTTP-Redirect AuthnRequest, HTTP-POST response, and a signed assertion. IdP-initiated SSO, encrypted assertions, Single Logout, metadata import, and signed AuthnRequests are not currently supported.
## Setup guides
[Section titled “Setup guides”](#setup-guides)
[Configure an OIDC connector](/en-us/domains-federation/configure-oidc-connector/)Create a confidential OIDC client at your IdP, register the callback, and test enterprise sign-in.
[Configure a SAML connector](/en-us/domains-federation/configure-saml-connector/)Register the Sudomimus SP and ACS, configure a signed assertion, and test enterprise sign-in.
## Validation at save time
[Section titled “Validation at save time”](#validation-at-save-time)
When you create or update an OIDC connector, Sudomimus **fetches your IdP’s discovery document then and there**. If the issuer is unreachable or invalid, the save is rejected (`FederationConnectorDiscoveryFailed`). SAML configuration is structurally validated at save time; assertion signatures and profile constraints are checked at every login.
## OIDC redirect URI
[Section titled “OIDC redirect URI”](#oidc-redirect-uri)
Sudomimus uses a single, platform-fixed redirect (callback) URI for **all** connectors — your IdP distinguishes flows by the per-login `state` value, not by the redirect URI. The connector page shows this URI read-only; register it as an allowed redirect URI in your IdP’s application configuration:
```text
https://federation.sudomimus.com/oidc/callback
```
## Managing connectors
[Section titled “Managing connectors”](#managing-connectors)
* **Disable** a connector to retire it without deleting it — useful when you are migrating IdPs.
* **Delete** removes it entirely.
Connectors in use cannot be removed
A connector that is still referenced cannot be disabled or deleted:
* referenced by a Layer-1 application sign-in rule → `FederationConnectorInUse`;
* pinned by a domain’s `SSO_ONLY` login policy → `FederationConnectorInUseByLoginPolicy`.
Remove the rule, or revert the domain’s policy to `ALLOW_ALL`, first. This guard exists so a live forced-SSO domain can never be stranded behind a deleted IdP.
## Quota
[Section titled “Quota”](#quota)
Each organization can hold a limited number of connectors (default **3**). Sudomimus staff can raise the limit on request. The quota is enforced on the self-service surface only.
## Browsing connectors
[Section titled “Browsing connectors”](#browsing-connectors)
The With portal has a top-level **Connectors** view that lists every connector across all your organizations, with an organization switcher.
## Related
[Section titled “Related”](#related)
[Sign in with your IdP](/en-us/domains-federation/sign-in-with-your-idp/)Put a connector to work — as an application button or a forced-SSO domain policy.
[Domain login policy](/en-us/domains-federation/domain-login-policy/)Bind a connector to a verified domain to force its users through SSO.
# Domains and enterprise federation
> How an organization claims a domain it owns, decides how that domain's users authenticate, and federates them to its own corporate identity provider.
The developer surface on Sudomimus is **organization-based**. An organization owns its applications and sectors, and an organization is also where the enterprise-grade identity features live: claiming the domains you control, deciding how the users on those domains may sign in, and pointing them at your own corporate identity provider.
This section covers that whole story. It builds up in three steps, each independently useful and each the foundation for the next.
## The three steps
[Section titled “The three steps”](#the-three-steps)
| Step | What it is | Who it is for |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| **1. Adopt a domain** | Prove you control a domain (`example.com`) via a DNS `TXT` record; the platform binds it exclusively to your organization. | Any organization that owns a domain. |
| **2. Set a login policy** | For users whose email is on that verified domain, decide platform-wide whether logins are allowed, blocked, or forced through SSO. | Organizations that want to govern how their domain’s users authenticate. |
| **3. Federate to your IdP** | Register your organization’s OIDC or SAML identity provider as a connector, then either offer it as a “Sign in with …” button on an application or force a domain’s users through it. | Companies and communities that run their own IdP (Entra ID, Okta, Google Workspace, …). |
Each step stands on its own. Adopting a domain is valuable purely as a verified, organization-owned claim. A login policy needs nothing more than a verified domain. Federation adds the IdP. You only go as far down this path as your needs require.
## A note on organizations
[Section titled “A note on organizations”](#a-note-on-organizations)
Everything here hangs off an **organization** — the multi-tenant container that owns applications, sectors, domains, and connectors. An account becomes an organization **owner** by creating an organization from the With portal. Membership carries a role (`VIEWER` < `ADMIN` < `OWNER`); the mutations in this section are **owner-only**, and several of them — setting a login policy, disabling an organization’s domain federation — require the caller to be the organization’s **sole** owner, so one co-owner cannot unilaterally change how everyone on a shared domain signs in.
All of these surfaces live in the With portal at [`with.sudomimus.com`](https://with.sudomimus.com).
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
[Adopt a domain](/en-us/domains-federation/adopt-a-domain/)Prove DNS control of a domain and bind it exclusively to your organization.
[Domain login policy](/en-us/domains-federation/domain-login-policy/)Allow, block, or force SSO for every user on a verified domain — platform-wide.
[Federation connectors](/en-us/domains-federation/federation-connectors/)Register your organization's OIDC or SAML identity provider as a reusable connector.
[Configure OIDC](/en-us/domains-federation/configure-oidc-connector/)Set up the IdP client, fixed callback, connector, and first test login.
[Configure SAML](/en-us/domains-federation/configure-saml-connector/)Set up the SP values, signed assertion, connector, and first test login.
[Sign in with your IdP](/en-us/domains-federation/sign-in-with-your-idp/)Offer your IdP on an application, or force a domain's users through it.
[Start by adopting a domain](/en-us/domains-federation/adopt-a-domain/)
# Sign in with your IdP
> Put a federation connector to work — offer your identity provider as a sign-in button on an application, or force a verified domain's users through it.
Once you have registered a [federation connector](/en-us/domains-federation/federation-connectors/), there are **two** ways to put it to work. They share the same connector, the same login machinery, and the same account model — they differ only in *who decides* that a user signs in through your IdP.
| Mode | Who turns it on | Who it affects |
| ------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------- |
| **Application-managed** | An application developer, per application | Anyone who chooses the “Sign in with …” button on that app |
| **Domain-managed (forced SSO)** | A domain owner, via login policy | Every account on a verified domain, on every application, with no opt-out |
Both are **Layer-1 authentication methods**, so they compose with the rest of the [three-layer rules model](/en-us/application-rules/overview/) exactly like any other method.
## Application-managed sign-in
[Section titled “Application-managed sign-in”](#application-managed-sign-in)
This is the straightforward “let users of my app sign in with our corporate IdP” case. On an application your organization owns, add a Layer-1 authentication rule:
```json
{
"method": "ENTERPRISE_FEDERATION_APPLICATION_MANAGED",
"payload": { "connectorAnchor": "Bastion-K7Q2-M9XB-3FNP-Covenant" }
}
```
* The `connectorAnchor` must reference a connector **owned by the application’s own organization** — this is enforced when the rule is saved.
* Sudomimus renders a **“Sign in with ``”** button in the authentication UI.
* One rule = one button. To offer several IdPs, add several rules (they OR together, like any Layer-1 rules).
When the user clicks the button, the browser is redirected to your IdP. An OIDC provider returns an authorization code to the fixed callback; a SAML provider posts a signed assertion to the fixed ACS. Sudomimus validates either proof and runs the **same realize pipeline as every other sign-in method**: account-linking decision, identity-row writes, Layer 2 realize, the consent gate, and token issuance. Nothing about federation is special downstream — it produces a normal Sudomimus session.
### How the federated identity links to an account
[Section titled “How the federated identity links to an account”](#how-the-federated-identity-links-to-an-account)
Sudomimus first looks for the identity bound to this connector subject:
1. **Seen this connector subject before** → reuse the existing account, even if the email at the IdP changed.
2. **No existing subject binding, and the asserted email is trusted** → look up the email owner and link to that account if one exists. Trust requires both an IdP-verified email and the connector organization’s current verified ownership of that email domain.
3. **No matching subject or trusted email owner** → create a new account for the connector subject. An absent or untrusted email does not establish mailbox ownership; the subject identity still has to pass the application’s Realize rules.
One account can hold identities from several connectors. Two IdPs asserting `jordan@acme.example` resolve to the same email owner only when each connector independently satisfies the domain-trust requirement. Matching email text alone is insufficient.
## Forced SSO (domain-managed)
[Section titled “Forced SSO (domain-managed)”](#forced-sso-domain-managed)
Domain-managed SSO requires **every** user on a domain you own to sign in only through the designated IdP. Passkeys, email OTP, and consumer OAuth are unavailable to those users while the policy is enabled. The configuration has two parts:
1. A verified domain with its [login policy](/en-us/domains-federation/domain-login-policy/) set to **`SSO_ONLY`**, bound to one of your organization’s connectors.
2. The Layer-1 method **`ENTERPRISE_FEDERATION_DOMAIN_MANAGED`** enabled on each application that accepts these logins:
```json
{
"method": "ENTERPRISE_FEDERATION_DOMAIN_MANAGED",
"payload": {}
}
```
The payload is **empty** — unlike application-managed, the connector is **not** named in the rule. It is resolved at login time from the user’s email domain → the verified domain → the connector bound to that domain’s `SSO_ONLY` policy. A domain owned by another organization can therefore determine the IdP used for login; the application accepts the IdP designated by the user’s domain owner.
### What a gated user experiences
[Section titled “What a gated user experiences”](#what-a-gated-user-experiences)
* **Email-first flows.** When an SSO-gated user enters their email, Sudomimus suppresses every other method and offers **only** the “Continue with ``” path.
* **No-email flows** (usernameless passkey, consumer OAuth, Steam). The user is only known *after* they authenticate, so Sudomimus catches them at realize time and redirects them into the SSO flow, then completes the login on the second pass.
* **A confirmation screen, not a silent jump.** core-ui shows a “Continue with ``” screen rather than auto-redirecting when there is one satisfiable connector. If verified domains require different connectors, the UI shows a terminal authority-conflict explanation instead. It never asks the user to choose which domain owner to bypass.
* **New employees.** A brand-new user whose email is on a forced-SSO domain is registered *through* the IdP on first sign-in (registration-via-SSO) — there is no separate enrolment step.
### Apps that have not opted in
[Section titled “Apps that have not opted in”](#apps-that-have-not-opted-in)
An application that does **not** list `ENTERPRISE_FEDERATION_DOMAIN_MANAGED` simply **rejects** an SSO-gated user — there is no implicit SSO escape hatch injected into an app that did not ask for it. This keeps Layer-1 default-deny intact: forcing SSO never silently adds a method to an application.
Forced SSO overrides Layer 1 only
`SSO_ONLY` changes *which authentication method* a gated user may use. It does **not** override Layer 2 or Layer 3 — an SSO-authenticated user can still be rejected by an application’s email allowlist, and the result is still delivered by the application’s return rules. Authentication ≠ authorization.
### Offboarding
[Section titled “Offboarding”](#offboarding)
Because login requires a fresh IdP assertion, a departed employee whose IdP account is disabled cannot start a new session. Already-issued access tokens are not revoked immediately; they expire by TTL (3 hours by default). Existing refresh sessions continue only while their original login still satisfies the current domain policy: Session API `/refresh` and the OIDC refresh-token grant re-check `BLOCK_ALL` and `SSO_ONLY`, so switching the domain to `BLOCK_ALL` or repinning `SSO_ONLY` to a connector the session did not use cuts it off at the next refresh. Disabling only the user at the IdP does not make an existing Sudomimus refresh token contact the IdP retroactively.
If the same account later owns verified emails under domains pinned to distinct IdPs, **neither** IdP wins. Interactive login, code redemption, device/native issuance, and refresh all fail closed until the conflict is repaired. Multiple domains pinned to the same connector remain usable.
## Related
[Section titled “Related”](#related)
[Federation connectors](/en-us/domains-federation/federation-connectors/)Register the OIDC or SAML identity provider both modes use.
[Configure OIDC](/en-us/domains-federation/configure-oidc-connector/)Create and test an OIDC federation connector end to end.
[Configure SAML](/en-us/domains-federation/configure-saml-connector/)Create and test a SAML federation connector end to end.
[Domain login policy](/en-us/domains-federation/domain-login-policy/)The SSO\_ONLY policy that drives domain-managed forced SSO.
[Layer 1 — Authentication rules](/en-us/application-rules/authentication-rules/)How both federation methods sit in the three-layer rules model.
# Choose an integration path
> Choose between Connect, OIDC, device authorization, and native direct-issue, then see which public Sudomimus services that path uses.
Sudomimus offers **four peer integration paths**. Choose the one that matches your client and existing stack; none is a prerequisite for another.
Most integrations should start from the [SDK overview](/en-us/sdk/overview/) after choosing a path. The SDK overview links the available language guides; this page explains which protocol shape you need.
| Path | Best fit | Protocol shape | Start here |
| ------------------------ | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------ |
| **Connect** | Web applications and custom browser sign-in | `establish → authenticate → redeem`, then Session API refresh | [Connect flow](/en-us/connect/flow/) |
| **OIDC** | Frameworks and partner systems that already speak OpenID Connect | Authorization code + PKCE | [OIDC flow](/en-us/oidc/flow/) |
| **Device authorization** | CLIs, launchers, terminal tools, and public clients without a client secret | `device-authorize → browser approval → device-token` | [Device authorization flow](/en-us/device/flow/) |
| **Native direct-issue** | Games, desktop apps, CLIs, and services with Steam tickets, AccessKeys, or public keys | One credential exchange, with an optional browser errand for remediation | [Native flows](/en-us/native/overview/) |
Using the official Sudomimus CLI?
The table above is for applications you are building. If you want an AI assistant or local script to operate Sudomimus itself, use the [Sudomimus CLI](/en-us/ai/cli/) instead.
Browser polling is Connect, not direct-issue
A desktop application can also open the system browser and use Connect with `STATUS_POLL`. That flow belongs to the Connect protocol even though the client is native. The Native section documents it alongside direct-issue so desktop integrators can compare both choices.
Device authorization is not Connect `STATUS_POLL`
Both flows can involve a native-looking client and a browser, but their trust model is different. Connect `STATUS_POLL` starts from a signed `/establish` request by a confidential client; device authorization starts from an unsigned public-client `/device-authorize` request and requires a Layer 3 `DEVICE_CODE` ReturnRule.
Device browser approval
The device approval page currently supports email OTP and passkeys. Choose Connect or OIDC if your application needs OAuth, Steam OpenID, or enterprise sign-in.
The four paths are exposed through six public services. The shared browser service is `via.sudomimus.com`, the hosted UI used by Connect, OIDC, device approval, and native errands.
## The six surfaces
[Section titled “The six surfaces”](#the-six-surfaces)
| Domain | Audience | Protocol |
| --------------------------- | ------------------------------------------------ | --------------------------------------- |
| `connect-api.sudomimus.com` | Application backend | Connect protocol (JSON over HTTPS) |
| `session-api.sudomimus.com` | Applications holding ordinary refresh tokens | Session lifecycle (JSON over HTTPS) |
| `via.sudomimus.com` | End users in a browser | Hosted auth page (browser-only) |
| `device-api.sudomimus.com` | Public clients (CLIs, launchers, shared devices) | Device authorization (JSON over HTTPS) |
| `native-api.sudomimus.com` | Native clients (desktop apps, games, CLIs) | One-shot direct-issue (JSON over HTTPS) |
| `oidc.sudomimus.com` | OIDC relying parties | OpenID Connect 1.0 |
### `connect-api.sudomimus.com`
[Section titled “connect-api.sudomimus.com”](#connect-apisudomimuscom)
The HTTPS API your **application backend** calls. It hosts:
* **Inquiry lifecycle:** `POST /establish`, `POST /redeem`, `POST /status-poll`
* **Localized application metadata:** `POST /info`
`/establish` requires a client-auth JWT signed with the application’s client-auth private key (RS256, 60-second lifetime, body-bound via `body_sha256`, replay-protected via `jti`). `/redeem` and `/status-poll` are authorized by Inquiry keys; `/info` is public.
See [Connect flow](/en-us/connect/flow/) for end-to-end examples.
### `session-api.sudomimus.com`
[Section titled “session-api.sudomimus.com”](#session-apisudomimuscom)
The HTTPS API for the ordinary application refresh-token lifecycle after Connect, device authorization, or native direct-issue has returned `{ accessToken, refreshToken }`.
* `GET /applications/{applicationAnchor}/jwks.json` returns the public keys used to verify application tokens by `kid`.
* `POST /refresh` rotates a refresh token and issues a new access token.
* `POST /introspect` checks whether an access token’s backing session is still active.
* `POST /logout` terminally revokes one ApplicationSession.
* `POST /revoke-all` ends every session for one application-visible subject.
`/refresh`, `/introspect`, and `/logout` are self-authenticating with the presented token. `/revoke-all` requires a client-auth JWT with `aud = "sudomimus-session"`. See [Managing sessions](/en-us/guides/managing-sessions/).
### `via.sudomimus.com`
[Section titled “via.sudomimus.com”](#viasudomimuscom)
A hosted web page that runs the actual user-facing authentication flow — passkey prompts, email OTP entry, platform sign-ins. Your application redirects the user to `via.sudomimus.com` with an `exposure-key`; the user completes the challenge there; control returns to your application according to the inquiry’s return method.
`via.sudomimus.com` is the user-facing surface. Your code never calls its endpoints directly — it just sends users there.
### `device-api.sudomimus.com`
[Section titled “device-api.sudomimus.com”](#device-apisudomimuscom)
For public clients that cannot safely hold an application client-auth private key. It hosts:
* `POST /device-authorize` — start a short-lived device-code session and receive `{ deviceCode, userCode, verificationUri, verificationUriComplete, expiresIn, interval }`.
* `POST /device-token` — poll with `deviceCode` until the browser user approves, denies, or the session expires.
`/device-authorize` does not require a client-auth JWT. The application opts in with a Layer 3 `DEVICE_CODE` ReturnRule, and the user completes ordinary Sudomimus authentication in `via.sudomimus.com`. After `/device-token` succeeds, use Session API for refresh, logout, introspection, and revocation. See [Device authorization flow](/en-us/device/flow/).
### `native-api.sudomimus.com`
[Section titled “native-api.sudomimus.com”](#native-apisudomimuscom)
Three credential-based direct-issue endpoints:
* `POST /direct-issue/steam-ticket` — a Steamworks ticket.
* `POST /direct-issue/access-key` — an AccessKey identifier and secret.
* `POST /direct-issue/public-key` — a signed Ed25519 assertion.
AccessKey and PublicKey support Account, Agent, and Automation principals with independent Layer-1 admission. These endpoints use their own credential proof rather than an application client-auth JWT. See [Native flows](/en-us/native/overview/) for configuration and Errand recovery.
### `oidc.sudomimus.com`
[Section titled “oidc.sudomimus.com”](#oidcsudomimuscom)
A standard OpenID Connect provider. Hosts:
* `GET /.well-known/openid-configuration`
* `GET /.well-known/jwks.json`
* `GET /authorize` (with PKCE, `S256` only)
* `POST /token`
* `GET /userinfo`, `POST /userinfo`
* `GET /end-session`
Supported grants: `authorization_code`, `refresh_token`. Supported client authentication: `private_key_jwt`, `client_secret_basic`, and `client_secret_post` (confidential-client options) and `none` + PKCE (the public-client option). See [OIDC flow](/en-us/oidc/flow/).
## A typical request flow
[Section titled “A typical request flow”](#a-typical-request-flow)
The topology below maps each integration path to the public services it uses. Native clients that use browser polling follow the **Connect** edges, not the Native direct-issue edge.
```
flowchart LR
subgraph Paths["Integration paths"]
Connect["Connect web apps and confidential browser polling"]
OIDC["OIDC relying parties"]
Device["Device authorization public clients"]
Native["Native direct-issue Steam tickets, AccessKeys, and public keys"]
end
subgraph Surfaces["Public Sudomimus services"]
ConnectAPI["connect-api.sudomimus.com"]
SessionAPI["session-api.sudomimus.com"]
Via["via.sudomimus.com"]
DeviceAPI["device-api.sudomimus.com"]
NativeAPI["native-api.sudomimus.com"]
OIDCAPI["oidc.sudomimus.com"]
end
Connect -->|establish, redeem, status-poll| ConnectAPI
Connect -->|hosted authentication| Via
Connect -->|session operations| SessionAPI
Device -->|authorize and poll| DeviceAPI
Device -->|browser approval| Via
Device -->|after initial issuance| SessionAPI
Native -->|one-shot credential exchange| NativeAPI
Native -->|after initial issuance| SessionAPI
OIDC -->|discovery, authorize, token, userinfo| OIDCAPI
OIDCAPI -. hosted browser handoff .-> Via
```
The browser and the application backend each talk to a different surface; Sudomimus stitches the two halves together internally, so the application never has to handle the user’s raw authentication material.
A public CLI using device authorization starts at `device-api`, sends the user to `via.sudomimus.com/device`, then keeps polling `device-api` until approval returns tokens. A game using Steam direct-issue collapses the login to a single round trip against `native-api`. An OIDC relying party talks only to `oidc.sudomimus.com`; the user is still authenticated via `via.sudomimus.com` underneath, but the RP does not see it.
## Everything else is internal
[Section titled “Everything else is internal”](#everything-else-is-internal)
These six surfaces are the **only** supported integration points. Sudomimus runs other services behind them — but they are internal to the platform and unreachable from outside it, so there is nothing else for an integration to call. If a flow you need isn’t expressed through one of the six surfaces above, it isn’t an integration point.
# Quickstart
> A minimal end-to-end integration with the Sudomimus Connect API.
This page walks through the smallest possible integration: pointing a web application at Sudomimus and obtaining a verified user identity.
Recommended: use an official SDK
The fastest way to integrate the browser login round-trip is via an official SDK — install [`@sudomimus/connect`](/en-us/sdk/overview/) and call typed methods (`establish`, `redeem`, `verifyAccessToken`) instead of building raw HTTP requests by hand. Use the Session API for refresh after tokens are issued.
[Install the SDK](/en-us/sdk/overview/)
Using an AI coding assistant?
Use the Sudomimus CLI when an assistant needs to operate Sudomimus directly. It exposes shell commands and JSON output while keeping login in your browser through the device authorization flow.
[Use the CLI with AI](/en-us/ai/cli/)
Not building a web app? Pick the right guide:
[Sudomimus CLI](/en-us/ai/cli/)Shell-friendly account and developer operations for AI agents, local automation, and source checkouts.
[Native clients](/en-us/native/overview/)Desktop apps, games via Steam, and CLI tools — including the browser Errand for consent and profile completion.
[OIDC relying parties](/en-us/oidc/flow/)Standard OpenID Connect — authorization\_code + PKCE.
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
1. **Create or join an organization at [`with.sudomimus.com`](https://with.sudomimus.com)**. The developer self-serve portal is organization-based: applications and sectors live inside an organization, so you need one before you can create an application. Most accounts create their first organization on the spot (the form pre-fills a suggested name); if a teammate already runs one, have them invite you instead. The `/applications` and `/sectors` pages redirect to `/organizations` until you belong to one.
2. **Create your application** inside that organization. When you create the application you receive:
* The **`applicationAnchor`** — a stable, lowercase-kebab identifier (e.g. `my-app`), the public name of your application across the API.
* The **client-auth private key** — shown **once** at creation, used to sign `/establish` requests. Store it like any production secret.
* A per-application **Session JWKS URL** at `https://session-api.sudomimus.com/applications/{applicationAnchor}/jwks.json`, used to verify access and refresh tokens by `kid`. The portal’s Signing keys tab manages rotation; application creation does not return a one-off signing PEM.
3. **Add at least one [Return Rule](/en-us/application-rules/return-rules/)** of type `CALLBACK`, listing the hostnames you will redirect users back to. The concrete `callbackUrl` is supplied per inquiry on `/establish`; the rule just gates which hostnames are allowed.
4. **Add at least one [Authentication Rule](/en-us/application-rules/authentication-rules/)** (e.g. `PASSKEY_USERNAMELESS`, `PASSKEY_REASONED`, or `EMAIL_VERIFICATION`) and one [Realize Rule](/en-us/application-rules/realize-rules/) (e.g. `EMAIL` with `allowedEmails: ["*"]` for a public sign-up). Rules are **allowlist-only with default-deny** — an application with zero rules in any layer cannot be used.
5. **Take the application live.** New applications start in `DRAFT`. After configuring the rules and saving the credentials, an organization OWNER must choose **Go live** in the With portal to change it to `ACTIVE`. Saving rules does not activate it. Follow [Take an application live](/en-us/with-portal/application-lifecycle/) for readiness and activation checks. If `/establish` returns `ApplicationNotActive`, check the application lifecycle and the availability of its organization and sector before retrying.
## The four phases
[Section titled “The four phases”](#the-four-phases)
Every authentication round-trip through Connect has three phases, followed by the shared Session API refresh phase:
1. **Establish** — your application backend asks Connect to start an authentication session and gets back a session reference (`exposureKey` + `hiddenKey`).
2. **Authenticate** — your application sends the user to `via.sudomimus.com` with the `exposureKey`; the user completes a passkey or email-OTP challenge there.
3. **Redeem** — once `via.sudomimus.com` hands control back via your callback URL (with `exposure-key` + `confirmation-key` in the query string), your backend exchanges the three keys at Connect for a signed access token and refresh token.
4. **Refresh** — your backend calls Session API to exchange the refresh token for a fresh access token whenever the current one nears expiry.
See [the Connect flow](/en-us/connect/flow/) for the full request shapes and how Connect, `via.sudomimus.com`, and your application interact.
## Next steps
[Section titled “Next steps”](#next-steps)
[Connect flow](/en-us/connect/flow/)End-to-end curl, Node.js, Python, and Go examples for the Connect API.
[The three-key model](/en-us/connect/three-key-model/)How a single login proves itself across exposureKey, hiddenKey, and confirmationKey.
[Managing sessions](/en-us/guides/managing-sessions/)Refresh, introspect, logout, and revoke-all — the lifecycle after the initial login.
[Sudomimus CLI](/en-us/ai/cli/)A command-line control surface for AI assistants and local automation.
# What is Sudomimus
> A high-level overview of the Sudomimus identity provider and authentication platform.
**Sudomimus** is an identity provider and authentication platform designed to be embedded into web, desktop, native, and OIDC-compatible applications. It provides a unified way to:
* Authenticate end users via multiple methods — **passkeys (WebAuthn)**, **email one-time passwords**, **social sign-in** (Google, GitHub, Discord, Battle.net, X), **Steam** (in-game ticket or “Sign in with Steam”), and **AccessKey credentials** for headless clients — with more being added over time.
* Exchange short-lived **tokens** through the Connect flow, device authorization, native direct-issue, or standard OIDC.
* Act as a standard **OpenID Connect provider** for relying parties that prefer `authorization_code` + PKCE over the Connect protocol.
* Manage users by **domain** — [claim a domain you own](/en-us/domains-federation/adopt-a-domain/), then decide platform-wide how its users authenticate: allow, block, or [force them through your own identity provider](/en-us/domains-federation/sign-in-with-your-idp/).
* Apply a three-layer allowlist to **session admission and identity disclosure**. Integrating applications remain responsible for their own business roles, permissions, resources, and authorization policy.
## Why Sudomimus
[Section titled “Why Sudomimus”](#why-sudomimus)
Most applications end up reinventing the same authentication primitives: session storage, password resets, email verification, social login, MFA. Sudomimus separates the *identity* layer from the *application* layer, so the authentication surface lives in one place and your application only deals with verified tokens.
## Public integration surfaces
[Section titled “Public integration surfaces”](#public-integration-surfaces)
Sudomimus exposes these public surfaces to integrators:
| Domain | Audience | Purpose |
| --------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| `connect-api.sudomimus.com` | Application backend | The Connect protocol — establish, status-poll, redeem, info. |
| `session-api.sudomimus.com` | Applications holding refresh tokens | Ordinary application session lifecycle — refresh, introspect, logout, revoke. |
| `via.sudomimus.com` | End users in a browser | The hosted page that runs the user-facing authentication flow. |
| `device-api.sudomimus.com` | Public clients (CLIs, launchers, shared devices) | Device authorization — code confirmation and polling for public clients without a client secret. |
| `native-api.sudomimus.com` | Native clients (desktop apps, games, CLIs) | Steam, AccessKey, and PublicKey direct-issue — one-shot login for clients with no browser. |
| `oidc.sudomimus.com` | OIDC relying parties | Standard OpenID Connect provider — discovery, authorize, token, userinfo, JWKS. |
You’ll typically pick **one** of Connect, OIDC, device authorization, or native direct-issue. [Choose an integration path](/en-us/getting-started/choose-integration/) compares them.
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* Building a **web app with Connect**? Start with the [Quickstart](/en-us/getting-started/quickstart/), then the [Connect flow](/en-us/connect/flow/).
* Building a **CLI, launcher, or public client**? Go to [Device authorization](/en-us/device/flow/).
* Integrating a **desktop app, game, or client with Steam/AccessKey/PublicKey credentials**? Go to [Native integration](/en-us/native/overview/).
* Wiring up an **OIDC relying party**? Go to the [OIDC flow](/en-us/oidc/flow/).
* Want to understand the shared model first? Read [Authentication philosophy](/en-us/concepts/philosophy/) and [Accounts and credentials](/en-us/concepts/accounts-and-credentials/).
## Registering your application
[Section titled “Registering your application”](#registering-your-application)
Applications are created and managed through the developer portal at [`with.sudomimus.com`](https://with.sudomimus.com) — that is where you obtain your `applicationAnchor`, receive the application-creation client-auth private key, and configure the three layers of rules. The private key is not available from ordinary application reads; an unchanged uncertain-result retry can recover the exact create result for ten minutes. Quickstart links to the exact pages.
# Account deletion
> What an application sees after a user deletes their Sudomimus account, and how to end the local session cleanly.
A Sudomimus end-user can permanently delete their account at any time from the Privacy view on [`with.sudomimus.com`](https://with.sudomimus.com). This page is for **application developers** integrated with Sudomimus: it describes what your application observes when that happens and how to handle it gracefully.
The user-facing flow itself — including confirmation and policy text — is part of [the Privacy Policy](https://sudomimus.com/legal-hub/privacy).
## What your application sees
[Section titled “What your application sees”](#what-your-application-sees)
A deleted account is treated the same as a disabled account by every realize-time and refresh-time chokepoint: the difference is the error symbol and that the condition is irreversible.
| When | Error you receive | HTTP / OIDC mapping |
| ----------------------------------------------- | -------------------------------------- | ------------------- |
| `POST /redeem` on Connect | `AccountDeleted` | `403 Forbidden` |
| `POST /refresh` on Session API | `AccountDeleted` | `403 Forbidden` |
| `POST /direct-issue/steam-ticket` on Native API | `AccountDeleted` | `403 Forbidden` |
| `POST /direct-issue/access-key` on Native API | `AccountDeleted` | `403 Forbidden` |
| Session `GET /userinfo` or `GET /claim-state` | `invalid_token` | `401 Unauthorized` |
| Session `POST /introspect` | `status: "revoked"` | `200 OK` |
| `POST /token` (auth-code) on OIDC API | `invalid_grant` — “Account is deleted” | `400` |
| `POST /token` (refresh-token) on OIDC API | `invalid_grant` — “Account is deleted” | `400` |
| `GET /userinfo` on OIDC API | `invalid_token` — “Account is deleted” | `401` |
`AccountDeleted` is the permanent counterpart to `AccountDisabled`. An already-issued access token can still pass offline signature and expiry checks until its `exp`, but live Sudomimus endpoints reject it or report the session as revoked. Applications should end the local session as soon as any of these responses is observed.
## How to handle `AccountDeleted` in your app
[Section titled “How to handle AccountDeleted in your app”](#how-to-handle-accountdeleted-in-your-app)
Treat it the same way you treat `AccountDisabled` today, **with one difference**: do not surface a “your account is suspended, contact support” UI. A deleted account is gone by user choice; the appropriate UX is to drop the local session and direct the user toward a fresh sign-up if they want one.
A simple handling flow:
```
flowchart TD
Deleted["POST /refresh fails AccountDeleted"] --> Clear["Clear the local access cookie and SDK state"]
Clear --> SignedOut["Surface a signed-out screen"]
SignedOut -. optional .-> SignUp["Offer Sign up again Start a fresh POST /establish flow"]
```
What you should **not** do:
* Do not retry — the symbol is terminal, not transient.
* Do not auto-re-create an account on the user’s behalf. A new sign-up is a deliberate user action.
* Do not assume the user’s previous data carries over. Even if they sign up again with the same email, your application sees a brand-new `subject` (sector subject) with no historical link to the old one.
## Owning a live organization blocks deletion
[Section titled “Owning a live organization blocks deletion”](#owning-a-live-organization-blocks-deletion)
Applications and sectors are owned by an **organization**, not by an individual. A user cannot delete their account while they are the **only active `OWNER` of an organization that still holds a live resource**.
* An organization blocks deletion when you are its **only active `OWNER`** and it still holds either an **enabled** application or a **non-disabled** sector. Retiring those resources clears the block.
* A co-owned organization does not block deletion when another active `OWNER` remains to manage it.
* An organization whose resources are all already disabled never blocks; on deletion it simply becomes owner-less.
The erasure attempt fails with `AccountOwnsLiveOrganizations` (surfaced as HTTP `409 Conflict`); the response carries the blocking organizations’ anchors so the in-product flow can list exactly what to retire.
The full path to deletion is therefore a retire-then-delete cascade: in each organization you solely own, disable every application, then disable each now-empty sector (now permitted, because their applications are all disabled), then delete the account. (You may also disable the organization itself, but that is not required for deletion.)
This is deliberate. An organization — and the applications and sectors it owns — is infrastructure that holds other people’s data, so Sudomimus will not silently destroy it as a side effect of one developer’s personal account deletion. If you solely own an organization with live resources and are thinking about closing your account, plan a retire step for it first.
## Re-registration semantics
[Section titled “Re-registration semantics”](#re-registration-semantics)
A user who signs up again after deletion gets a **brand-new account**, even if they use the same email address. Applications receive new sector subjects, and Sudomimus does not carry over preferences, sign-in methods, grants, or sessions.
From your application’s perspective this is indistinguishable from a brand-new user. Your `subject`-keyed data store should treat the two as unrelated.
## Related
[Section titled “Related”](#related)
* [Managing sessions](/en-us/guides/managing-sessions/) — `/logout` and `/revoke-all` for ending sessions without deleting the account.
* [Privacy controls](/en-us/with-portal/privacy/) — the user-facing identifier rotation and account-erasure controls.
* [Privacy Policy — Deleting Your Account](https://sudomimus.com/legal-hub/privacy) — the user-facing version of this contract.
# Managing sessions
> The lifecycle endpoints after the initial login — refresh, introspect, logout, and revoke-all on the Session API.
A login is the start of a session, not the end of the integration. Session API provides four endpoints for the rest of the ordinary application session lifecycle: **`/refresh`**, **`/introspect`**, **`/logout`**, and **`/revoke-all`**. This page is the single reference for all of them.
For application code, prefer the Session SDK package for your language. See the [SDK overview](/en-us/sdk/overview/) before hand-writing these HTTP calls.
If you’re using the OIDC flow, see also [`/end-session` in the OIDC guide](/en-us/oidc/flow/#5-end-session) — it has a related but narrower purpose.
## At a glance
[Section titled “At a glance”](#at-a-glance)
| Endpoint | Authenticates with | Idempotent | Scope of effect |
| ------------------ | -------------------------- | -------------------------------------------------------------- | ------------------------------------------------------ |
| `POST /refresh` | The refresh token itself | No (rotates the refresh token — each token works exactly once) | One session |
| `GET /userinfo` | The access token as Bearer | Read-only | One session’s current shared profile |
| `POST /introspect` | The access token itself | Read-only | One session |
| `POST /logout` | The refresh token itself | Yes (calling twice returns `revoked: true` both times) | One session |
| `POST /revoke-all` | Client-auth JWT (RS256) | Yes | All sessions for an account, scoped to the calling app |
None of these endpoints require setting up new infrastructure — they reuse the keys you already have from your initial integration.
## `/refresh` — extend a session
[Section titled “/refresh — extend a session”](#refresh--extend-a-session)
Exchange a refresh token for a fresh access token **and a new refresh token**. Refresh is **strict rotation** ([OAuth 2.0 Security BCP §4.14.2](https://www.rfc-editor.org/rfc/rfc9700#section-4.14.2)): the presented signed version is consumed while the same logical ApplicationSession keeps its stable payload `sid`, stores a new `jti`, and increments payload `rotationVersion`. Store the new `refreshToken` and use it for the next refresh. Re-presenting a stale version is treated as compromise and terminally revokes that session. Near-simultaneous requests with the same version (for example, two browser tabs) are the exception: during the short grace window they adopt the exact winning version instead of advancing again or logging the user out. Reusing it after that window still triggers compromise, so always store and send the latest token.
```bash
curl -X POST https://session-api.sudomimus.com/refresh \
-H "Content-Type: application/json" \
-d '{ "refreshToken": "..." }'
```
Response:
```json
{
"accessToken": "",
"refreshToken": "",
"claims": {
"email": { "requirement": "REQUIRED", "state": "GRANTED" },
"firstName": { "requirement": "OPTIONAL", "state": "GRANTED" },
"lastName": { "requirement": "OFF", "state": "UNKNOWN" },
"staticAvatar": { "requirement": "SYNTHETIC_ONLY", "state": "UNKNOWN" },
"animatedAvatar": { "requirement": "OFF", "state": "UNKNOWN" }
}
}
```
Persist the rotated `refreshToken`, replacing the one you just used — the old one is now invalid. The `claims` block is the same per-claim view returned by `/redeem` — see [the `claims` block](/en-us/concepts/identity-claims/#the-claims-block) for how to read it.
**Auth**: none — possession of the refresh token is the credential.
The new access token’s TTL is the one resolved on the original `/redeem` (or `/direct-issue/*`, or OIDC `/token`). It is not re-resolved on refresh.
### Refresh can fail on claims
[Section titled “Refresh can fail on claims”](#refresh-can-fail-on-claims)
`/refresh` is not only a success-or-revoke endpoint. If, since the last token was minted, a **required** claim has stopped being satisfied — the developer escalated a claim policy from optional to required, or the user revoked a grant — the refresh is rejected with `ClaimConsentRequired` rather than minting a token missing that required claim.
Recovery depends on the client, because `/refresh` itself cannot collect consent:
* **Native clients** (Steam / AccessKey) recover by re-running the original direct-issue, which returns an [Errand](/en-us/native/claims-and-errand/) handoff for the user to grant consent.
* **Browser applications** recover by sending the user through an ordinary interactive login again.
This is rare in practice — it only happens when a policy or grant changes mid-session — but build your refresh path to surface a re-authentication prompt rather than treating every refresh failure as a hard logout.
## `/introspect` — is this token still valid?
[Section titled “/introspect — is this token still valid?”](#introspect--is-this-token-still-valid)
Ask Sudomimus about the current status of an access token. Use this when you want to invalidate sessions promptly across services — e.g. when a user clicks “log out everywhere” and you need other tabs or services to notice within a bounded time.
```bash
curl -X POST https://session-api.sudomimus.com/introspect \
-H "Content-Type: application/json" \
-d '{ "accessToken": "..." }'
```
Response:
```json
{
"status": "active",
"recommendedRecheckSeconds": 600
}
```
**`status`** is one of:
* `"active"` — the `sid` resolves to an ACTIVE, live-authority ApplicationSession.
* `"revoked"` — the session is terminally revoked or one of its authority bindings is no longer current.
* `"expired"` — the fixed ApplicationSession expiry has passed.
* `"not_found"` — the token is invalid or its `sid` does not resolve to a matching session.
**`recommendedRecheckSeconds`** is how long Sudomimus suggests you may cache the result before re-introspecting. It is always 600 seconds today. Treat the access token’s own `exp` claim as the upper bound on its usable lifetime; introspection is for catching *early* revocation.
**Auth**: none — the access token is self-authenticating. Anyone holding the token can ask whether it is still valid; nothing else is required.
### When to call introspect
[Section titled “When to call introspect”](#when-to-call-introspect)
Local signature verification covers correctness; introspection covers freshness. A reasonable pattern:
* Verify the access token’s signature and `exp` claim **on every request** (cheap, local).
* Call `/introspect` opportunistically — once per N minutes per session, on a background job, or when a user-visible state change suggests it.
You do not need to introspect on every request. Doing so would defeat the point of having a signed token in the first place.
Interactive demo
[Explore the session revocation lab](https://theater.sudomimus.com/session-revocation/). Compare offline verification, online checks, and refresh results after revocation. This local simulation does not perform real sign-ins or change your account.
## `/logout` — invalidate a single session
[Section titled “/logout — invalidate a single session”](#logout--invalidate-a-single-session)
Terminally revoke the ApplicationSession identified by one genuine refresh-token version. Its access tokens stop being reported as active by `/introspect`, and every later `/refresh` for that `sid` fails.
```bash
curl -X POST https://session-api.sudomimus.com/logout \
-H "Content-Type: application/json" \
-d '{ "refreshToken": "..." }'
```
Response:
```json
{ "revoked": true }
```
* `revoked: true` — the session was revoked (or was already terminal; calling twice is fine).
* `revoked: false` — the token is invalid or not found.
**Auth**: none — possession of the refresh token authorizes logging out that session ([RFC 7009](https://www.rfc-editor.org/rfc/rfc7009) style).
`/logout` does not recall access tokens cryptographically
A `/logout` call terminally revokes server-side session authority. An external verifier doing only signature and `exp` checks may still accept an already-issued access token until `exp`; call `/introspect` when an operation requires current live authority. Build your logout UX and recheck cadence with that bounded offline-validity window in mind.
## `/revoke-all` — revoke every session for an account
[Section titled “/revoke-all — revoke every session for an account”](#revoke-all--revoke-every-session-for-an-account)
Advance the account/application revocation authority, then best-effort revoke the currently enumerated application-session rows. Every older session becomes unusable even if cleanup does not enumerate its row. Use this for account-takeover incident response, “log me out of all devices”, or support-initiated session termination.
The account is identified by its **sector subject** — payload `sub` on the application’s access token and the value you key users on. Refresh tokens carry no user identifier.
```bash
curl -X POST https://session-api.sudomimus.com/revoke-all \
-H "Content-Type: application/json" \
-H "Authorization: SudomimusClientJWT $SUDOMIMUS_CLIENT_AUTH_JWT" \
-d '{ "subject": "sub_9SQ5535CRWNDDM2T" }'
```
Response:
```json
{ "revoked": true }
```
`revoked: true` acknowledges the request. Cleanup results are deliberately private because they are not the source of revocation authority. Unknown and out-of-sector subjects return the same acknowledgement so the endpoint does not reveal whether the subject exists.
**Auth**: a client-auth JWT with `aud = "sudomimus-session"`. The scope of the action is implicit — only sessions issued to that account *within the calling application* are touched. You cannot use one application’s client-auth key to revoke sessions in another application.
## Putting it together — a typical session lifecycle
[Section titled “Putting it together — a typical session lifecycle”](#putting-it-together--a-typical-session-lifecycle)
```
stateDiagram-v2
state "Active ApplicationSession" as Active
state "Refresh rotation" as Rotating
state "Revoked" as Revoked
state "Expired" as Expired
[*] --> Active: initial token issuance
Active --> Rotating: /refresh with current refresh token
Rotating --> Active: new access + rotated refresh
Rotating --> Active: previous version within grace adopts winner
Active --> Active: /introspect = active
Active --> Revoked: stale refresh reused after grace
Active --> Revoked: /logout or /revoke-all
Active --> Revoked: live authority invalidated
Active --> Expired: fixed session expiry
Revoked --> Revoked: /introspect = revoked
Revoked --> [*]
Expired --> [*]
```
For the OIDC variant of refresh, see [OIDC relying parties — Refresh](/en-us/oidc/flow/#4-refresh).
# Native claims and the Errand
> Choose a claim policy for native direct-issue and handle the browser Errand when required consent or profile data is missing.
Steam ticket, AccessKey, and PublicKey direct-issue authenticate without an application login page. That keeps the happy path short, but it also means the client cannot display a consent form or ask the user to complete missing profile data.
This page covers both sides of that constraint:
1. Choose claim policies that fit a non-interactive client.
2. Handle the **Errand** browser handoff when a required real claim cannot yet be issued.
The shared policy and consent model is documented in [Identity claims and sharing](/en-us/concepts/identity-claims/).
## Choose a native claim policy
[Section titled “Choose a native claim policy”](#choose-a-native-claim-policy)
| Policy | Native direct-issue behavior |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **Off** | Never requested. |
| **Optional** | Shared only if the user already granted it. Never blocks, so a native-only user may never be prompted. |
| **Required** | Guaranteed present with real data. Missing consent or data returns `403` with an Errand. |
| **Placeholder only** (`SYNTHETIC_ONLY`) | Guaranteed present as a stable placeholder. Never asks for real data, never blocks, and never creates an Errand. |
| **Fallback placeholder** (`SYNTHETIC_FALLBACK`) | Guaranteed present, using real data when granted and a stable placeholder otherwise. Never blocks and never creates an Errand. |
For most native integrations, prefer **`SYNTHETIC_ONLY`** or **`SYNTHETIC_FALLBACK`** over **`REQUIRED`** unless you specifically need verified real data.
* Need a stable name or email-shaped value, and a placeholder is acceptable without consent: use `SYNTHETIC_ONLY`.
* Want to let the user share real data when they choose, but keep direct-issue non-blocking with a placeholder fallback: use `SYNTHETIC_FALLBACK`.
* Need a real verified email for delivery or reconciliation: use `REQUIRED` and implement the Errand flow.
* Want real data when already granted but can continue without it: use `OPTIONAL`.
If every requested claim is `OFF`, `SYNTHETIC_ONLY`, or `SYNTHETIC_FALLBACK`, claim policy can never force direct-issue into a browser handoff.
Synthetic names are generated placeholders. Synthetic emails use a stable `…@proxy.sudomimus.email` address, and synthetic avatars use generated sector avatar images. Proxy delivery is best-effort, not guaranteed, and OIDC exposes synthetic email with `email_verified: false`.
For avatar URL scope, rotation, and caching behavior, see [Avatar claims and delivery](/en-us/user-generated-content/avatar-claims-and-delivery/).
## The Errand
[Section titled “The Errand”](#the-errand)
An **Errand** is short-lived account remediation, not token issuance. When direct-issue cannot satisfy a required claim, its `403` response gives the client a browser URL. The user completes consent or missing profile work there, then the client makes a new direct-issue attempt with the credential-specific proof described below.
Only two claim-gate reasons carry an Errand:
| `403` reason | Meaning | Browser work |
| -------------------------- | ----------------------------------------------------- | -------------------------------------------------------------- |
| `ClaimConsentRequired` | A required claim has not been granted. | Grant consent and, when necessary, first add the missing data. |
| `RequiredClaimDataMissing` | Consent exists, but the account lacks the real value. | Register an email or complete the missing name. |
Other `403` responses, such as rule denial or a disabled account, are terminal and do not include an Errand.
### Handoff response
[Section titled “Handoff response”](#handoff-response)
```json
{
"reason": "ClaimConsentRequired",
"claims": {
"email": { "requirement": "REQUIRED", "state": "UNKNOWN" },
"firstName": { "requirement": "OPTIONAL", "state": "UNKNOWN" },
"lastName": { "requirement": "OFF", "state": "UNKNOWN" },
"staticAvatar": { "requirement": "SYNTHETIC_ONLY", "state": "UNKNOWN" },
"animatedAvatar": { "requirement": "OFF", "state": "UNKNOWN" }
},
"errand": {
"errandKey": "ernd_...",
"url": "https://via.sudomimus.com/errand?key=ernd_...",
"expiresAt": "2026-06-10T12:30:00Z"
}
}
```
* Open `errand.url` in the user’s **system browser**.
* Treat `errandKey` as a bearer secret. It is also used to poll status.
* The Errand is single-use and expires after **30 minutes**.
### Client loop
[Section titled “Client loop”](#client-loop)
```
sequenceDiagram
autonumber
participant Client as Native client
participant Native as Native API
participant Browser as System browser
participant Via as via
Client->>Native: POST /direct-issue/...
Native-->>Client: 403 { reason, claims, errand }
Client->>Browser: Open errand.url
Browser->>Via: Complete the required tasks
Via-->>Browser: Done
loop Optional status polling
Client->>Native: GET /errand/{errandKey}/status
Native-->>Client: PENDING or COMPLETED
end
Note over Client: Prepare credential-specific proof (fresh Steam ticket or PublicKey assertion)
Client->>Native: POST /direct-issue/... once with that proof
Native-->>Client: Tokens if current issuance checks pass; otherwise an error
```
### Prepare the retry proof
[Section titled “Prepare the retry proof”](#prepare-the-retry-proof)
| Credential | Proof for each new direct-issue attempt |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Steam | Obtain a fresh ticket with `GetAuthTicketForWebApi`. The original ticket is replay-protected even when issuance returned an Errand. |
| AccessKey | Reuse the identifier and secret while the credential remains valid. |
| PublicKey | Sign a fresh assertion with a new `jti` and the hash of the current request body. Reuse the registered key, not the previous assertion. |
Apply these rules after `COMPLETED`, after `EXPIRED`, and when recovering from a refresh denial. Completion repairs account data or consent; token issuance still depends on current credential, application, and policy checks. Use the status endpoint to wait instead of repeatedly submitting credentials.
Polling is optional. A client may instead ask the user to confirm that they finished in the browser before retrying.
```bash
curl https://native-api.sudomimus.com/errand/ernd_.../status
# → { "status": "PENDING" }
# → { "status": "COMPLETED" }
# → { "status": "EXPIRED" }
```
Poll about every two seconds with a sensible overall timeout. `EXPIRED` deliberately covers unknown, malformed, consumed, and genuinely expired keys; prepare the credential-specific proof above and rerun direct-issue to obtain a fresh handoff. The status endpoint never issues tokens.
Direct-issue attempts that pass credential verification normally return the same live Errand when it has at least 15 minutes remaining and the required work has not changed. User progress therefore remains associated with one URL.
## Security behavior
[Section titled “Security behavior”](#security-behavior)
* **Consent only:** no additional sign-in is required because the credential holder already proved control of a token-minting credential.
* **Writing identity data:** the browser requires sign-in, and the signed-in account must match the account resolved from the Steam ticket, AccessKey, or PublicKey.
* Optional and synthetic claims never create an Errand.
* Session API `/refresh` and OIDC `/token` do not embed Errand handoffs. A native session blocked during refresh recovers by running direct-issue again.
## Related
[Section titled “Related”](#related)
* [Native integration](/en-us/native/overview/) — browser polling, Steam ticket, and AccessKey flows.
* [Identity claims and sharing](/en-us/concepts/identity-claims/) — the shared policy, grant, and inclusion model.
* [Avatar claims and delivery](/en-us/user-generated-content/avatar-claims-and-delivery/) — real and placeholder avatar URL behavior.
* [Tokens and verification](/en-us/concepts/tokens-and-verification/) — the tokens issued after the claim gate is satisfied.
# Native flows
> Integrate Steam, AccessKey, and PublicKey direct issuance for accounts, agents, and automations.
Native API provides three direct-issue methods. Choose Steam for a Steam game, AccessKey for an issued secret pair, or PublicKey for an Ed25519 key you control. AccessKey and PublicKey can authenticate an Account, Agent, or Automation.
| Credential | Endpoint |
| ------------ | --------------------------------- |
| Steam ticket | `POST /direct-issue/steam-ticket` |
| AccessKey | `POST /direct-issue/access-key` |
| PublicKey | `POST /direct-issue/public-key` |
Configure all three rule layers and credentials, then have an organization OWNER [take the application live](/en-us/with-portal/application-lifecycle/). The application must be `ACTIVE`, with available parent organization and sector. Every direct-issue method requires a `DIRECT_ISSUE` ReturnRule and an applicable Layer 2 rule for the account.
### Principal and credential admission
[Section titled “Principal and credential admission”](#principal-and-credential-admission)
Layer 1 must allow the exact method below. Native derives it from the credential’s bound principal; callers cannot choose another principal kind. These rules have empty payloads.
| Principal | AccessKey | PublicKey |
| ---------- | ------------------------------ | ------------------------------ |
| Account | `ACCESS_KEY_DIRECT` | `PUBLIC_KEY_DIRECT` |
| Agent | `AGENT_ACCESS_KEY_DIRECT` | `AGENT_PUBLIC_KEY_DIRECT` |
| Automation | `AUTOMATION_ACCESS_KEY_DIRECT` | `AUTOMATION_PUBLIC_KEY_DIRECT` |
## Browser polling
[Section titled “Browser polling”](#browser-polling)
For a confidential backend that can sign `/establish`, use [Connect browser polling](/en-us/connect/flow/#browser-polling). Public clients without that backend should use [Device authorization](/en-us/device/flow/).
## Steam direct-issue
[Section titled “Steam direct-issue”](#steam-direct-issue)
For games shipped through Steam, Sudomimus supports a silent login that does not open a browser at all. The user does not see a login prompt; their Steam identity is exchanged directly for a Sudomimus session.
```bash
curl -X POST https://native-api.sudomimus.com/direct-issue/steam-ticket \
-H "Content-Type: application/json" \
-d '{
"applicationAnchor": "my-game",
"steamTicketHex": "",
"steamAppId": 480
}'
```
The flow is:
1. The game calls Steamworks `ISteamUser::GetAuthTicketForWebApi("sudomimus")` — **not** `GetAuthSessionTicket`. The two are different ticket types and are not interchangeable. The identity string must be exactly `"sudomimus"` (case-sensitive); other values are rejected.
2. The game waits for the `GetTicketForWebApiResponse_t` callback before using the ticket.
3. The ticket bytes are hex-encoded and sent as `steamTicketHex` to `POST /direct-issue/steam-ticket`, together with `applicationAnchor` and `steamAppId`.
4. Sudomimus verifies the ticket with Steam, looks up or creates the account, and — on the happy path — returns `{ accessToken, refreshToken }` in one round trip. If the application requires consent or profile data the Steam account has not provided, this returns a `403` with an **Errand** handoff instead — see [When direct-issue needs consent or profile data](#when-direct-issue-needs-consent-or-profile-data).
5. The game calls `Steamworks.CancelAuthTicket(handle)` after receiving the tokens.
The Steam account is the source of identity; required consent or missing profile data may still need the browser Errand.
### Application configuration for Steam
[Section titled “Application configuration for Steam”](#application-configuration-for-steam)
The application must have:
* **Layer 1**: a `STEAM_TICKET` AuthenticationRule with `allowedSteamAppIds: number[]` containing this game’s Steam App ID.
* **Layer 2**: a rule that will match — typically `STEAM_ID` with `allowedSteamIds: ["*"]` for any verified Steam account, or a list of specific SteamID64 strings.
* **Layer 3**: a `DIRECT_ISSUE` ReturnRule.
A Steam-first account that has never linked an email needs a `STEAM_ID` (or `ACCOUNT_ALIAS` / `SECTOR_SUBJECT`) Layer 2 rule; an `EMAIL`-only Layer 2 will reject it.
This endpoint does **not** require a client-auth JWT — the Steam ticket itself attests both the user and the binary’s right to talk to the application.
> **Web counterpart.** The browser-side “Sign in with Steam” button uses the `STEAM_OPENID` Layer 1 method instead of `STEAM_TICKET`. Both paths land on the **same** per-user Steam identity, so a user who first signed in through a game can subsequently sign in through the web button (and vice versa) without any account-linking step. See [Authentication rules](/en-us/application-rules/authentication-rules/) for the `STEAM_OPENID` rule shape.
## AccessKey direct-issue
[Section titled “AccessKey direct-issue”](#accesskey-direct-issue)
For environments without a Steam ticket but where the target Sudomimus account is already known — CLI tools, custom launchers, headless services, automated test rigs. The “proof” is a Sudomimus-issued credential pair (`accessKeyIdentifier` + `accessKeySecret`) generated from the developer portal and handed out-of-band to the operator.
```bash
curl -X POST https://native-api.sudomimus.com/direct-issue/access-key \
-H "Content-Type: application/json" \
-d '{
"applicationAnchor": "my-cli-tool",
"accessKeyIdentifier": "acs_k_",
"accessKeySecret": "acs_t_<64-char lowercase hex>"
}'
```
Both credential strings carry mandatory prefixes:
* `acs_k_` — the public identifier, followed by a UUIDv4.
* `acs_t_` — the secret half, followed by 64 lowercase hex characters. It is returned by the create operation and an exact retry for up to ten minutes, but never by ordinary key reads.
The prefixes are part of the canonical form. They make the two halves visually distinguishable and let secret scanners match accidentally-committed credentials by literal substring.
### Application configuration for AccessKey
[Section titled “Application configuration for AccessKey”](#application-configuration-for-accesskey)
The application must have:
* **Layer 1**: the exact AccessKey AuthenticationRule for the credential principal from the table above (empty payload). Default-deny unless explicitly added.
* **Layer 2**: a rule matching the target account — `EMAIL`, `STEAM_ID`, `ACCOUNT_ALIAS`, or `SECTOR_SUBJECT`.
* **Layer 3**: a `DIRECT_ISSUE` ReturnRule.
**AccessKey credentials cannot create new accounts.** The credential is issued against an existing Sudomimus account; if that account is deleted, every credential bound to it is rejected at login time.
Credentials are managed from [`with.sudomimus.com`](https://with.sudomimus.com) under **Programmatic access → Access keys**; see [Manage access keys](/en-us/programmatic-access/access-keys/). Revocation is a soft delete (`revokedAt` timestamp); rotation = revoke + reissue. Expired credentials are not auto-evicted but are rejected at the handler.
This endpoint does not require a client-auth JWT either — the access-key secret is itself the credential. Embedding the client-auth private key in a distributable CLI would be reversible by any operator anyway.
## PublicKey direct-issue
[Section titled “PublicKey direct-issue”](#publickey-direct-issue)
[Register an Ed25519 public key](/en-us/programmatic-access/public-keys/) under **Programmatic access → Public keys**, choosing the Account, Agent, or Automation and its application/sector coverage. Keep the private key in your own system.
1. Serialize the request body, for example `{"applicationAnchor":"my-app"}`.
2. Sign an EdDSA JWT with header `alg: "EdDSA"`, `typ: "vnd.sudomimus.public-key-assertion+jwt"`, and `kid` equal to the registered `pky_...` credential identifier.
3. Set `iss` equal to `kid`, `aud` to `sudomimus-native-public-key`, current `iat`, `exp` at most 60 seconds later, a fresh random 128-bit base64url `jti`, and `requestHash` to the base64url SHA-256 digest of the exact request body bytes.
4. POST those same bytes to `https://native-api.sudomimus.com/direct-issue/public-key` with `Content-Type: application/json` and `Authorization: SudomimusPublicKeyJWT `.
Every new attempt requires a fresh assertion and `jti`. The private key authenticates issuance; the resulting access token remains a bearer token. Exact request and error shapes are in the [Native API reference](/en-us/api/native/).
## When direct-issue needs consent or profile data
[Section titled “When direct-issue needs consent or profile data”](#when-direct-issue-needs-consent-or-profile-data)
Direct-issue endpoints authenticate a credential in one request — they cannot pop a consent screen or ask the user to type in an email. So when an application requires a [claim](/en-us/concepts/identity-claims/) the user has not granted, or requires data the account does not have yet (a Steam account with no email, for instance), the call cannot just succeed. Instead it returns a `403` carrying an **[Errand](/en-us/native/claims-and-errand/)** — a short-lived browser side-trip where the user completes that work:
```json
{
"reason": "ClaimConsentRequired",
"claims": {
"email": { "requirement": "REQUIRED", "state": "UNKNOWN" },
"firstName": { "requirement": "OPTIONAL", "state": "UNKNOWN" },
"lastName": { "requirement": "OFF", "state": "UNKNOWN" },
"staticAvatar": { "requirement": "SYNTHETIC_ONLY", "state": "UNKNOWN" },
"animatedAvatar": { "requirement": "OFF", "state": "UNKNOWN" }
},
"errand": {
"errandKey": "ernd_...",
"url": "https://via.sudomimus.com/errand?key=ernd_...",
"expiresAt": "2026-06-10T12:30:00Z"
}
}
```
The `reason` is one of `ClaimConsentRequired` (the user must agree to share a required claim) or `RequiredClaimDataMissing` (consent is there, but the account data is not). Steam, AccessKey, and PublicKey direct-issue can return this handoff, but their retry credentials differ. To recover:
1. Open `errand.url` in the user’s **system browser**. The page walks the user through any sign-in, data entry, and consent that is owed.
2. Poll `GET /errand/{errandKey}/status` (`native-api`) every \~2 seconds until it reports `COMPLETED` — or just let the user tell your UI they’re done.
3. Retry direct-issue once with the appropriate proof: Steam must obtain a fresh ticket from `GetAuthTicketForWebApi`; AccessKey may reuse its still-valid identifier and secret; PublicKey must sign a fresh assertion with a new `jti`. Never replay the original Steam ticket, even if it returned an Errand. Current credential, application, rule, and claim checks still apply.
```bash
curl https://native-api.sudomimus.com/errand/ernd_.../status
# → { "status": "PENDING" } user still working in the browser
# → { "status": "COMPLETED" } done — retry with the credential-specific proof
# → { "status": "EXPIRED" } expired/consumed/unknown — re-run with the credential-specific proof
```
A `200` from a direct-issue endpoint also carries a `claims` block (the same shape as in the `403`), so even on success you can see which optional claims were shared and which were withheld. The full contract — the 30-minute lifetime, when an eligible retry reuses the same `errandKey`, and the two security tiers (consent-only vs. sign-in-required) — is in [The Errand](/en-us/native/claims-and-errand/).
## Tokens and Workload admission
[Section titled “Tokens and Workload admission”](#tokens-and-workload-admission)
Account access tokens use `typ: vnd.sudomimus.application-access+jwt`. Agent and Automation access tokens use `typ: vnd.sudomimus.workload-access+jwt` and add `act.sub`, the pairwise actor subject. In both cases `sub` identifies the owner account within the sector. Applications must explicitly admit the appropriate token type and enforce their own business permissions; credential scope does not grant those permissions.
Refresh tokens use `vnd.sudomimus.application-refresh+jwt` and contain neither owner nor actor subject. Fetch current owner profile claims from Session UserInfo. Follow [Tokens and verification](/en-us/concepts/tokens-and-verification/) for signature and authority checks.
# OIDC flow
> Integrate an OpenID Connect relying party with discovery, authorization code + PKCE, /token, /userinfo, and /end-session.
If you have an existing application that already speaks **OpenID Connect**, or you want to use an off-the-shelf OIDC library, you can integrate with Sudomimus as a standard OIDC relying party (RP). The Sudomimus OIDC provider lives at `oidc.sudomimus.com` and supports the **authorization code flow with PKCE**, the canonical modern OIDC integration shape.
Use this guide when:
* Your framework or platform has a first-class OIDC integration (Next-Auth, Spring Security, Keycloak adapter, etc.) and you want to slot Sudomimus in as the IdP.
* You’re integrating with a partner system that already expects an OIDC provider.
* You prefer the OIDC mental model (clients, scopes, ID tokens) to the Connect protocol.
If you’re starting fresh and just want the smallest custom integration, the [Connect protocol](/en-us/connect/flow/) is usually shorter.
The official SDKs are most useful for Connect, Session, Device, Native, and token verification helpers. See the [SDK overview](/en-us/sdk/overview/) if your OIDC application also verifies Sudomimus application access tokens or manages ApplicationSessions directly.
## Discovery
[Section titled “Discovery”](#discovery)
Sudomimus publishes a standard OIDC discovery document. Point your library at the issuer URL `https://oidc.sudomimus.com` and it will fetch the rest from there:
```bash
curl https://oidc.sudomimus.com/.well-known/openid-configuration
```
Sudomimus does not publish a separate OIDC OpenAPI schema. Use discovery for the deployed endpoint URLs and advertised capabilities, the OpenID Connect and OAuth standards for protocol semantics, and this guide for the supported Sudomimus profile and its platform-specific constraints. The generated OpenAPI reference covers the Connect, Session, Native, and Device product APIs instead.
The document advertises:
* **`response_types_supported`**: `["code"]` (authorization code flow only).
* **`authorization_response_iss_parameter_supported`**: `true` — authorization callbacks identify the provider according to [RFC 9207](https://www.rfc-editor.org/rfc/rfc9207.html).
* **`grant_types_supported`**: `["authorization_code", "refresh_token"]`.
* **`scopes_supported`**: `["openid", "email", "profile", "offline_access"]`.
* **`claims_supported`** includes `sub`, `email`, `email_verified`, `name`, `given_name`, `family_name`, `picture`, and `picture_animated`.
* **`claim_state_endpoint`** identifies Sudomimus’s provider-specific endpoint for live claim policy and consent metadata.
* **`id_token_signing_alg_values_supported`**: `["RS256"]`.
* **`code_challenge_methods_supported`**: `["S256"]` — PKCE is required for public clients and recommended for confidential clients; plain is not supported.
* **`token_endpoint_auth_methods_supported`**: `["private_key_jwt", "client_secret_basic", "client_secret_post", "none"]`.
* **`ui_locales_supported`**: `["en-US", "zh-CN"]`.
## Register your application
[Section titled “Register your application”](#register-your-application)
In [`with.sudomimus.com`](https://with.sudomimus.com), on the application you want to expose via OIDC:
1. Add a Layer 3 **OIDC** return rule:
```json
{
"returnMethod": "OIDC",
"payload": {
"redirectUris": ["https://app.example.com/oidc/callback"],
"postLogoutRedirectUris": ["https://app.example.com/"],
"allowedScopes": ["openid", "email", "profile", "offline_access"],
"tokenEndpointAuthMethod": "private_key_jwt"
}
}
```
2. Add the Layer 1 and Layer 2 rules you would for any other application — at least one authentication method (e.g. `PASSKEY_USERNAMELESS` or `PASSKEY_REASONED`) and at least one realize rule (e.g. `EMAIL` with the addresses or domain pattern you accept). The OIDC flow runs through the same authentication challenge as the rest of the platform.
3. **Choose a client authentication method**:
* **`private_key_jwt`** (recommended for confidential clients) — your RP holds a private key and signs a JWT assertion at `/token`. The signing key is your application’s client-auth key, the same key used by confidential Connect integrations to sign `/establish`.
* **`client_secret_basic`** (confidential clients) — your RP presents its shared secret in the HTTP `Authorization: Basic` header at `/token`.
* **`client_secret_post`** (confidential clients) — your RP sends its shared secret in the `/token` form body (`client_id` + `client_secret` parameters).
* **`none`** — only for public clients (SPAs, mobile apps without a backend). **PKCE is required.**
`client_secret_basic` and `client_secret_post` use the same application secret. Generate or rotate it from the application’s page in the With portal, then store it securely.
The application’s **`applicationAnchor`** is your `client_id`.
Before starting login, configure the rules and credentials for your chosen flow, then have an organization OWNER [take the application live](/en-us/with-portal/application-lifecycle/). New applications remain `DRAFT` until explicitly activated; login requires `ACTIVE` and available parent organization and sector.
## The OIDC flow
[Section titled “The OIDC flow”](#the-oidc-flow)
```
sequenceDiagram
autonumber
participant RP as Relying party
participant Browser as User browser
participant OIDC as OIDC provider
participant Via as via.sudomimus.com
RP-->>Browser: Redirect to /authorize state + nonce + PKCE challenge
Browser->>OIDC: GET /authorize
OIDC-->>Browser: Continue to hosted authentication
Browser->>Via: Authenticate and review consent
Via-->>Browser: Return to the registered redirect URI code + state + iss
Browser-->>RP: Authorization callback
RP->>OIDC: POST /token code + PKCE verifier + client authentication
OIDC-->>RP: ID token + access token optional refresh token
RP->>OIDC: GET or POST /userinfo Bearer access token
OIDC-->>RP: Scope- and consent-gated claims
```
### 1. Authorization request
[Section titled “1. Authorization request”](#1-authorization-request)
Redirect the user’s browser to `/authorize` with the standard OIDC parameters:
```text
https://oidc.sudomimus.com/authorize
?client_id=my-app
&redirect_uri=https%3A%2F%2Fapp.example.com%2Foidc%2Fcallback
&response_type=code
&scope=openid%20email%20profile
&ui_locales=zh-CN%20en-US
&state=
&nonce=
&code_challenge=
&code_challenge_method=S256
```
Required: `client_id`, `redirect_uri`, `response_type=code`, `scope` (must include `openid`). Public clients also require `code_challenge` and `code_challenge_method=S256`. Confidential clients may omit both PKCE parameters, but must still authenticate at `/token`. Empty, incomplete, or unsupported PKCE parameters are rejected. Optional but recommended: `state`, `nonce`.
The examples below use the recommended PKCE flow. Without PKCE, confidential OIDC clients must retain transaction-bound CSRF and code-injection protection, including the nonce-based checks and precautions described in [RFC 9700](https://www.rfc-editor.org/rfc/rfc9700.html#section-4.5.3.2). Client authentication alone does not provide those protections.
Use the optional `ui_locales` parameter when your application knows the user’s preferred interface languages. List BCP 47 tags in preference order, separated by spaces. Sudomimus uses the first supported value (`en-US` or `zh-CN`); unsupported values are ignored and never block sign-in. The hint applies only to this sign-in, and the user can still switch languages from the page.
#### Generate a valid PKCE pair
[Section titled “Generate a valid PKCE pair”](#generate-a-valid-pkce-pair)
Let your OIDC library generate PKCE values whenever possible. Create a fresh pair for every authorization attempt and keep the verifier with the pending login state until the callback arrives.
If you generate the values yourself:
* `code_verifier` must be 43–128 characters using only letters, digits, `-`, `.`, `_`, and `~`.
* `code_challenge` must be the unpadded base64url encoding of the verifier’s SHA-256 digest. An S256 challenge is exactly 43 characters and uses only letters, digits, `-`, and `_`.
* Send only `code_challenge` to `/authorize`. Send the original, unchanged `code_verifier` when exchanging the code at `/token`.
Do not reuse a verifier or copy a fixed example into production. Sudomimus rejects malformed values as well as verifier/challenge mismatches.
Once a challenge is sent, a matching verifier is mandatory even for confidential clients. If the authorization omitted PKCE, omit `code_verifier` at `/token` too: supplying one in that case is rejected to prevent downgrade attacks.
#### Choose the interaction behavior
[Section titled “Choose the interaction behavior”](#choose-the-interaction-behavior)
Most applications can omit `prompt` and `max_age`. Sudomimus may reuse a remembered login for the same application after checking current account, credential, application, rule, and consent authority. Reuse preserves the original proof time in `auth_time`; it does not make an older authentication fresh.
| Parameter | Behavior |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Omitted `prompt` | Reuse an eligible remembered login, or continue with interactive sign-in when needed. |
| `prompt=login` | Require fresh authentication. |
| `prompt=select_account` | Require fresh authentication so the user can choose an account. |
| `prompt=consent` | Show consent again, even when standing claim-sharing choices are already settled. |
| `prompt=none` | Attempt reuse without displaying the sign-in UI; return an interaction error if the request cannot complete silently. |
| `max_age=0` | Require fresh authentication. |
| Positive `max_age` | Permit reuse only when the original authentication is no older than this number of seconds and all current checks pass. |
`max_age` must be a non-negative decimal integer. `prompt` may combine `login`, `select_account`, and `consent` with spaces; `none` must appear alone.
For `prompt=none`, handle `login_required` when no eligible remembered authentication is available, `consent_required` when consent is needed, and `interaction_required` when required account data needs browser interaction. Start an interactive authorization request when the user is ready to complete that work. A valid `id_token_hint` alone does not establish a reusable login.
Request `offline_access` only when your application needs to refresh tokens after the user leaves or closes it. Sudomimus shows a separate, default-unchecked offline-session choice. If the user declines, sign-in still succeeds, but the returned scope omits `offline_access` and no refresh token is issued. Always use the returned `scope` and the presence of `refresh_token` as the result of that choice.
Sudomimus redirects the user to `via.sudomimus.com` where they authenticate via the methods allowed by your Layer 1 rules. After a successful authentication and realize, the browser returns to your `redirect_uri` with a `code`, the original `state`, and `iss=https://oidc.sudomimus.com`.
Authorization errors that occur after Sudomimus validates your registered callback return to that callback with `error`, `error_description`, the original `state`, and the same `iss`. Your OIDC library must compare `iss` exactly with the issuer saved for this login and reject a missing or different value before exchanging the code. Errors before callback validation are returned directly as JSON. Treat `server_error` as a temporary provider failure and let the user retry; do not cache protocol error responses.
### 2. Token exchange
[Section titled “2. Token exchange”](#2-token-exchange)
Authorization codes are single-use and valid for 60 seconds. Reusing a redeemed code with valid client authentication, redirect URI and PKCE is rejected and revokes only the session issued from that code, including its refresh tokens. If an exchange response is lost, start a new authorization flow instead of retrying the code. Other sign-in sessions are not revoked.
POST the authorization code to `/token`. The body is **`application/x-www-form-urlencoded`**, per OIDC:
* private\_key\_jwt
```bash
curl -X POST https://oidc.sudomimus.com/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$AUTH_CODE" \
--data-urlencode "redirect_uri=https://app.example.com/oidc/callback" \
--data-urlencode "code_verifier=$PKCE_VERIFIER" \
--data-urlencode "client_id=my-app" \
--data-urlencode "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
--data-urlencode "client_assertion=$CLIENT_ASSERTION_JWT"
```
The `client_assertion` is a JWT you sign with your application’s client-auth private key. Required claims: `iss = client_id`, `sub = client_id`, `aud = the exact token endpoint URL` (`https://oidc.sudomimus.com/token` in production), fresh `jti`, `iat`, `exp` (within 300s of `iat`). RS256.
* client\_secret\_basic
```bash
curl -X POST https://oidc.sudomimus.com/token \
-u "my-app:$CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$AUTH_CODE" \
--data-urlencode "redirect_uri=https://app.example.com/oidc/callback" \
--data-urlencode "code_verifier=$PKCE_VERIFIER"
```
The Basic username is your `client_id`, so do not repeat `client_id` in the form. This is the request shape used by standard OIDC libraries.
* client\_secret\_post
```bash
curl -X POST https://oidc.sudomimus.com/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$AUTH_CODE" \
--data-urlencode "redirect_uri=https://app.example.com/oidc/callback" \
--data-urlencode "code_verifier=$PKCE_VERIFIER" \
--data-urlencode "client_id=my-app" \
--data-urlencode "client_secret=$CLIENT_SECRET"
```
* none (PKCE)
```bash
curl -X POST https://oidc.sudomimus.com/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$AUTH_CODE" \
--data-urlencode "redirect_uri=https://app.example.com/oidc/callback" \
--data-urlencode "code_verifier=$PKCE_VERIFIER" \
--data-urlencode "client_id=my-app"
```
No client assertion is sent. PKCE (`code_verifier` matching the `code_challenge` from the authorization request) is the only client authentication.
Successful response (JSON):
```json
{
"access_token": "",
"token_type": "Bearer",
"expires_in": 10800,
"id_token": "",
"scope": "openid email profile"
}
```
* **`id_token`** — signed by Sudomimus’s platform-wide OIDC key; verify against `https://oidc.sudomimus.com/.well-known/jwks.json`. It carries minimal protocol claims (`iss`, `sub`, `aud`, `exp`, `iat`, `at_hash`, optional `nonce` and `auth_time`) plus authentication context (`amr`, `acr`). Profile claims come from `/userinfo`.
* **`access_token`** — signed by the application’s token-signing key, with the same minimal registered and session claims as a Sudomimus application access token. It carries no profile fields.
* **`refresh_token`** — included only if you requested `offline_access` and the user approved the offline session.
### 3. Userinfo
[Section titled “3. Userinfo”](#3-userinfo)
```bash
curl https://oidc.sudomimus.com/userinfo \
-H "Authorization: Bearer $ACCESS_TOKEN"
```
Returns the claims permitted by the granted scopes:
```json
{
"sub": "",
"email": "",
"email_verified": true,
"name": "",
"given_name": "",
"family_name": "",
"picture": "",
"picture_animated": ""
}
```
`sub` is the **sector subject** — the per-sector, application-visible identifier and the same value as the `id_token` `sub`. Use it as the user’s key. `/userinfo` accepts both `GET` and `POST`.
Prefer the `Authorization: Bearer` header for either method. POST also accepts one `access_token` in an `application/x-www-form-urlencoded` body. Do not combine the header and form token or repeat the form parameter; these requests return `400 invalid_request`, as do empty form tokens and query-string tokens. JSON and GET bodies cannot supply the token. Both supported transports enforce the same token expiration, live session, and claim-sharing rules.
The `picture` and private `picture_animated` values follow the sector-scoped avatar delivery contract; see [Avatar claims and delivery](/en-us/user-generated-content/avatar-claims-and-delivery/).
### Claim state
[Section titled “Claim state”](#claim-state)
When your application needs current policy and consent metadata, call the discovered `claim_state_endpoint` with the same Bearer access token. Both GET and POST are supported. The response never contains profile values:
```json
{
"sub": "",
"claims": {
"email": { "requirement": "OPTIONAL", "state": "GRANTED" },
"given_name": { "requirement": "OPTIONAL", "state": "GRANTED" },
"family_name": { "requirement": "OFF", "state": "UNKNOWN" },
"picture": { "requirement": "OFF", "state": "UNKNOWN" },
"picture_animated": { "requirement": "OFF", "state": "UNKNOWN" }
}
}
```
Only `email`-scope and `profile`-scope entries are returned. An `openid`-only session receives an empty `claims` object.
## Authentication context
[Section titled “Authentication context”](#authentication-context)
Every `id_token` includes:
| Claim | Meaning |
| ----- | ----------------------------------------------------------------------------------------------------------------------- |
| `amr` | Standard Authentication Methods References values, such as `["hwk", "user"]` for passkey or `["otp"]` for email OTP. |
| `acr` | Sudomimus-specific authentication context string, such as `urn:sudomimus:acr:passkey` or `urn:sudomimus:acr:email-otp`. |
If your RP needs phishing-resistant sign-in for a sensitive action, check `acr` for `urn:sudomimus:acr:passkey`. Federated upstreams such as Google, GitHub, Discord, Battle.net, X, Steam, and enterprise federation map to `amr: ["pwd"]`; use `acr` when you need to distinguish the upstream provider.
### 4. Refresh
[Section titled “4. Refresh”](#4-refresh)
If you requested `offline_access`, the user approved it, and you received a refresh token, exchange it at `/token`:
```bash
curl -X POST https://oidc.sudomimus.com/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "refresh_token=$REFRESH_TOKEN" \
--data-urlencode "client_id=my-app"
# plus client_assertion for confidential clients
```
Use the same client authentication method as the authorization-code exchange. For `client_secret_basic`, use `-u "my-app:$CLIENT_SECRET"` and omit the body `client_id`. For `client_secret_post`, include both body `client_id` and `client_secret`.
You can optionally pass `scope` to request a narrowed subset of the originally-granted scopes. Per [OIDC §12.1](https://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken), the ID token from a refresh does not include a new `nonce`.
### 5. End session
[Section titled “5. End session”](#5-end-session)
```text
https://oidc.sudomimus.com/end-session
?id_token_hint=
&client_id=my-app
&post_logout_redirect_uri=https%3A%2F%2Fapp.example.com%2F
&state=
```
`/end-session` signals the end of an OIDC session and redirects to the `post_logout_redirect_uri` (which must match one of the registered URIs exactly). When `id_token_hint` verifies and its `aud` matches this client, Sudomimus advances account/application session authority and revokes older ApplicationSessions for that account within this application.
A bad, forged, or stale hint does not block logout redirection; it simply means no application sessions are revoked. The hint may be expired, which lets an RP still drive logout after its local session has timed out.
Use Session API `/logout` when you hold a specific refresh token and want to revoke only that one session. Use Session API `/revoke-all` from a backend when you need application-wide account revocation without relying on an `id_token_hint`. See [Managing sessions](/en-us/guides/managing-sessions/).
## Using an OIDC library
[Section titled “Using an OIDC library”](#using-an-oidc-library)
Most modern OIDC libraries (Node’s `openid-client`, Python’s `authlib`, Java’s `nimbus-jose-jwt`, etc.) discover everything from the issuer URL and handle PKCE, JWKS, and token verification automatically. This Node example supports `openid-client` 6.x (tested with 6.8.6) and uses a public, PKCE-only client registered with `token_endpoint_auth_method: "none"`:
```js
import * as oidc from "openid-client";
const redirectUri = "https://app.example.com/oidc/callback";
const configuration = await oidc.discovery(
new URL("https://oidc.sudomimus.com"),
"my-app",
{
redirect_uris: [redirectUri],
response_types: ["code"],
token_endpoint_auth_method: "none",
},
oidc.None(),
{
execute: [oidc.enableNonRepudiationChecks],
},
);
export const startSignIn = async (savePendingSignIn) => {
const codeVerifier = oidc.randomPKCECodeVerifier();
const codeChallenge = await oidc.calculatePKCECodeChallenge(codeVerifier);
const state = oidc.randomState();
const nonce = oidc.randomNonce();
await savePendingSignIn({
codeVerifier,
state,
nonce,
});
return oidc.buildAuthorizationUrl(configuration, {
redirect_uri: redirectUri,
scope: "openid email profile",
code_challenge: codeChallenge,
code_challenge_method: "S256",
state,
nonce,
});
};
export const finishSignIn = async (
callbackUrl,
consumePendingSignIn,
createServerSideApplicationSession,
) => {
const pendingSignIn = await consumePendingSignIn();
if (pendingSignIn === undefined) {
throw new Error("OIDC sign-in state is missing or expired");
}
const tokens = await oidc.authorizationCodeGrant(
configuration,
callbackUrl,
{
pkceCodeVerifier: pendingSignIn.codeVerifier,
expectedState: pendingSignIn.state,
expectedNonce: pendingSignIn.nonce,
},
);
const claims = tokens.claims();
if (claims?.sub === undefined) {
throw new Error("The validated ID token did not contain sub");
}
const userinfo = await oidc.fetchUserInfo(
configuration,
tokens.access_token,
claims.sub,
);
await createServerSideApplicationSession({
userKey: claims.sub,
refreshToken: tokens.refresh_token,
});
return userinfo;
};
```
`savePendingSignIn` and `consumePendingSignIn` represent short-lived, server-side storage tied to the browser’s login attempt. Consume the verifier, state, and nonce once at callback time. `authorizationCodeGrant` validates the authorization response, including the RFC 9207 issuer from discovery and the expected state, as well as the ID-token nonce. The configured non-repudiation check additionally validates the ID-token signature through the discovered JWKS before `claims()` exposes those claims.
`createServerSideApplicationSession` represents your application’s own session store. Use the validated, application-scoped `sub` as the user key, keep any refresh token on the server, and send the browser only your application’s session cookie. Do not serialize the returned token response into that cookie.
## Token verification reminder
[Section titled “Token verification reminder”](#token-verification-reminder)
OIDC ID tokens are verified against the JWKS at `oidc.sudomimus.com/.well-known/jwks.json` — that’s the standard OIDC mechanism, and your library does it for you.
The `access_token` returned by `/token` does **not** use the platform-wide OIDC JWKS. It is a per-application access token of the same shape as the Connect flow: payload `sub` is the pairwise user key, `sid` is the ApplicationSession, and `jti` is this token instance. Verify it by `kid` against `https://session-api.sudomimus.com/applications/{applicationAnchor}/jwks.json`. If your OIDC library cannot use a separate JWKS for access tokens, treat the access token as opaque and use `/userinfo` for claims. See [Tokens and verification](/en-us/concepts/tokens-and-verification/).
# Manage access keys
> Create, rotate, and revoke access keys for your account, agents, and automations.
An access key is a long-lived credential for a service or command-line tool that signs in to a specific application without a browser. It can represent your account or belong to an [agent](/en-us/programmatic-access/agents/) or [automation](/en-us/programmatic-access/automations/) you created.
If a person launches the tool and can approve sign-in in a browser, [Device Authorization](/en-us/device/flow/) is usually a better fit.
## Before you create one
[Section titled “Before you create one”](#before-you-create-one)
Confirm that:
* you recognize the target application and it accepts access keys for the principal you selected;
* the agent or automation is active, if the key will belong to one; and
* you have a password manager, secret store, or protected runtime environment ready for the secret.
## Create an access key
[Section titled “Create an access key”](#create-an-access-key)
In [`with.sudomimus.com`](https://with.sudomimus.com):
1. Open **Programmatic access → Access keys**.
2. Select **Create access key** and enter the application anchor.
3. Choose your account, an agent, or an automation as the principal.
4. Review the profile information requested by the application and confirm your choices.
5. Create the key, then copy and store the secret immediately.
The secret is shown once. The portal will continue to show the key name, principal, and usage details, but it cannot reveal the secret later.
If the network drops during creation, keep the form unchanged and retry so the portal can recover the result of the same operation.
## Profile consent
[Section titled “Profile consent”](#profile-consent)
An application may request your name, email, or avatar. That information comes from the owning account even when an agent or automation signs in. Share only what the task needs.
If required profile data is missing, update your profile before trying again. A key cannot be created if you decline information the application requires.
## Rotate or revoke
[Section titled “Rotate or revoke”](#rotate-or-revoke)
For a routine rotation, create a replacement and update the service first. Revoke the old key after the service can sign in successfully with the new one.
Revoke a key immediately if it may have leaked. Revocation cannot be undone, and any service using that key will lose access.
## Related
[Section titled “Related”](#related)
* [Choose a programmatic access method](/en-us/programmatic-access/overview/)
* [Public-key sign-in](/en-us/programmatic-access/public-keys/)
* [Review and end sessions](/en-us/with-portal/session-security/)
# Manage agents
> Give context-aware software its own identity and control its lifecycle.
An agent is a good fit for software that reads context, selects tools, or decides its next step dynamically, such as a coding assistant, operations assistant, or autonomous task runner.
In Sudomimus, an agent is not another user account. It belongs to your account but has its own name, credentials, and sign-in history. Applications can use that distinction to tell you and your agent apart.
## Create an agent
[Section titled “Create an agent”](#create-an-agent)
In [`with.sudomimus.com`](https://with.sudomimus.com):
1. Open **Programmatic access → Agents**.
2. Select **Create**.
3. Enter a name and description. A useful name identifies both the job and where it runs.
4. Save the agent.
The agent still needs an [access key](/en-us/programmatic-access/access-keys/) or [public key](/en-us/programmatic-access/public-keys/) before it can sign in. The target application must also accept agent sign-in with that credential type.
## Suspend, resume, or revoke
[Section titled “Suspend, resume, or revoke”](#suspend-resume-or-revoke)
* **Suspend** an agent during maintenance, investigation, or a temporary shutdown. It cannot establish or refresh access while suspended.
* **Resume** it when it is ready to run again. Sessions from before the suspension do not come back automatically.
* **Revoke** it when the identity is no longer needed. Revocation cannot be undone.
If the agent should stop using only one application, revoke the credentials that cover that application instead of shutting down the whole agent.
## Good operating habits
[Section titled “Good operating habits”](#good-operating-habits)
* Use a separate agent for each clear responsibility.
* Use separate agents and credentials for development, test, and production.
* The agent still acts on behalf of your account. Limit its profile data and application access to what the job needs.
* Review [Session security](/en-us/with-portal/session-security/) regularly to see where the agent has signed in.
Use an [automation](/en-us/programmatic-access/automations/) instead when the job follows a fixed workflow and trigger.
# Manage automations
> Give scheduled and event-driven workflows their own identity and credentials.
An automation is a good fit for a job that follows known rules, such as a scheduled backup, CI job, release pipeline, or synchronization triggered by an event.
It belongs to your account but has its own name, credentials, and sign-in history. Shutting down one automation does not affect your personal sign-in or an unrelated programmatic task.
## Create an automation
[Section titled “Create an automation”](#create-an-automation)
In [`with.sudomimus.com`](https://with.sudomimus.com):
1. Open **Programmatic access → Automations**.
2. Select **Create**.
3. Use a name that identifies both the job and environment, such as “Production release” or “Nightly backup.”
4. Note its trigger, runtime location, or owner in the description, then save it.
Next, create an [access key](/en-us/programmatic-access/access-keys/) or register a [public key](/en-us/programmatic-access/public-keys/) for the automation. The target application must accept automation sign-in with that credential type.
## Manage its lifecycle
[Section titled “Manage its lifecycle”](#manage-its-lifecycle)
* **Suspend** the automation during maintenance or an investigation.
* Before you **resume** it, check that its trigger will not replay stale or unsafe work.
* **Revoke** it when the automation is permanently retired. Revocation cannot be undone.
If you only need to remove access to one application, revoke the credentials that cover that application instead of shutting down the entire automation.
## Good operating habits
[Section titled “Good operating habits”](#good-operating-habits)
* Give each pipeline or job its own automation and credential.
* Use separate identities and credentials for test and production.
* Store credentials in a secret manager or protected runtime environment, never in a script or repository.
* Rotate credentials when the runtime platform or responsible team changes.
* Review [Session security](/en-us/with-portal/session-security/) and confirm that recent applications and activity times are expected.
Use an [agent](/en-us/programmatic-access/agents/) when the software chooses tools and next steps from context.
# Programmatic access
> Choose an identity and credential for command-line tools, services, agents, and automations.
Programmatic access is for software that runs without a browser or should not depend on a person signing in each time. In the With portal, you manage two separate choices:
* **Who is acting:** your account, an agent, or an automation.
* **How it signs in:** an access key or a public key whose private half stays with you.
Neither choice grants permission inside an application. The application still decides which sign-in methods it accepts and what the signed-in actor may do.
## Decide whether you need a programmatic credential
[Section titled “Decide whether you need a programmatic credential”](#decide-whether-you-need-a-programmatic-credential)
```
flowchart TD
Start[Software needs to sign in] --> Browser{Can a user approve the sign-in in a browser at runtime?}
Browser -->|Yes| Device[Prefer Device Authorization]
Browser -->|No| Actor{Does it need its own identity and independent lifecycle?}
Actor -->|No| Account[Use the account as the principal]
Actor -->|Yes| Behavior{How does it work?}
Behavior -->|Chooses its next step from context| Agent[Create an agent]
Behavior -->|Runs a defined flow on a schedule or event| Automation[Create an automation]
Account --> Credential{Choose a credential}
Agent --> Credential
Automation --> Credential
Credential -->|Simpler setup| AccessKey[Access key]
Credential -->|Keep the private key local| PublicKey[Public-key sign-in]
```
Device Authorization is usually a better fit for a CLI or desktop tool launched by a person. It avoids keeping a long-lived programmatic credential. Unattended services, agents, and automations are better candidates for an access key or public key.
## Agent or automation
[Section titled “Agent or automation”](#agent-or-automation)
| Choose | When it fits |
| ----------------------------------------------------- | --------------------------------------------------------------------------------- |
| [Agent](/en-us/programmatic-access/agents/) | Software that reads context, selects tools, or decides its next step dynamically. |
| [Automation](/en-us/programmatic-access/automations/) | A defined workflow triggered by a schedule, webhook, or other known event. |
Both belong to your account and use the same credential types. Keeping them separate makes ownership and lifecycle easier to understand: suspending a deployment automation does not affect your personal account or an unrelated agent.
Interactive demo
[Explore Agent and Automation identities](https://theater.sudomimus.com/workload-identity/). Pause one actor and observe the effect on the owner account and other actors. This local simulation does not perform real sign-ins or change your account.
## Access key or public key
[Section titled “Access key or public key”](#access-key-or-public-key)
| Choose | What to expect |
| ----------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| [Access key](/en-us/programmatic-access/access-keys/) | Quick to create and easy to integrate. The secret is shown once and must be stored safely. |
| [Public key](/en-us/programmatic-access/public-keys/) | The private key stays with you. Best when your environment already manages keys and signing. |
Choose between an access key and public-key sign-in based on what the application supports, how you store credentials, and whether your software can sign requests.
## Routine checks
[Section titled “Routine checks”](#routine-checks)
* Use names that identify the job and where it runs, such as “Release assistant” or “Nightly backup.”
* Give each service its own credential instead of sharing one across environments.
* Revoke credentials you no longer use. Suspend the agent or automation when the shutdown is temporary.
* Review [Session security](/en-us/with-portal/session-security/) regularly and confirm that the applications and actors are expected.
# Use public-key sign-in
> Register an Ed25519 public key while keeping the private key in your own system.
Public-key sign-in is a good fit for a service that already has reliable key storage and signing support. You generate an Ed25519 key pair in your own system and register only the public half with Sudomimus. The private key stays with you.
If you mainly want the quickest setup, an [access key](/en-us/programmatic-access/access-keys/) is usually easier.
## Generate a key pair in the portal
[Section titled “Generate a key pair in the portal”](#generate-a-key-pair-in-the-portal)
The With portal can generate the key pair on macOS, Windows, and Linux:
1. Open **Programmatic access → Public keys**, then select **Create**.
2. Select **Generate key pair on this device**.
3. Download and store the private-key file. The portal enables saving only after the download.
4. The public JWK is filled in automatically.
Generation happens in the current browser. Only the public key is submitted to Sudomimus.
## Generate a key pair from the command line
[Section titled “Generate a key pair from the command line”](#generate-a-key-pair-from-the-command-line)
If OpenSSL is installed, generate an Ed25519 private key and its matching SPKI public-key PEM directly:
### macOS and Linux
[Section titled “macOS and Linux”](#macos-and-linux)
Run this in Terminal:
```bash
openssl genpkey -algorithm Ed25519 -out sudomimus-ed25519.private.pem
openssl pkey -in sudomimus-ed25519.private.pem -pubout -out sudomimus-ed25519.public.pem
chmod 600 sudomimus-ed25519.private.pem
```
### Windows
[Section titled “Windows”](#windows)
Run this in PowerShell:
```powershell
openssl genpkey -algorithm Ed25519 -out sudomimus-ed25519.private.pem
openssl pkey -in sudomimus-ed25519.private.pem -pubout -out sudomimus-ed25519.public.pem
```
These commands create private- and public-key PEM files. Paste the contents of `sudomimus-ed25519.public.pem` into the With portal; the portal converts it to the public JWK used by Sudomimus in the current browser. Place the private-key file in the service that will use it, and never paste or upload the private key.
The portal also continues to accept an existing exact Ed25519 public JWK `{ "kty": "OKP", "crv": "Ed25519", "x": "..." }`.
## Register a public key
[Section titled “Register a public key”](#register-a-public-key)
Prepare the application or sector anchors the key should cover. Then, in [`with.sudomimus.com`](https://with.sudomimus.com):
1. Open **Programmatic access → Public keys**.
2. Select **Create** and enter a name that identifies its purpose.
3. Choose your account, an agent, or an automation as the principal.
4. Generate a key pair on the current device or paste an existing Ed25519 public-key PEM/JWK. Never paste the private key.
5. Enter the application or sector anchors that may use the key, and add an expiry if needed.
6. Save the key, then configure the service to sign in with the matching private key.
The target application must support public-key sign-in. The application developer or organization administrator provides the required application or sector anchors.
## Application and sector coverage
[Section titled “Application and sector coverage”](#application-and-sector-coverage)
* An **application anchor** limits the key to that application.
* A **sector anchor** covers applications in that sector, including applications added later.
Coverage determines where the key may be used to sign in. It does not grant permissions inside those applications. Coverage cannot be edited after registration. To change it, register a replacement, switch the service, and revoke the old key.
## Storage and rotation
[Section titled “Storage and rotation”](#storage-and-rotation)
* Keep the private key in an operating-system keychain, hardware security device, or protected key service.
* Never paste it into the With portal, a chat, a ticket, or a source repository.
* Use separate keys for separate environments so a test-system leak does not affect production.
* If the private key is lost or may have leaked, revoke the public key entry and register a replacement immediately.
## Related
[Section titled “Related”](#related)
* [Choose a programmatic access method](/en-us/programmatic-access/overview/)
* [Manage access keys](/en-us/programmatic-access/access-keys/)
* [Agents](/en-us/programmatic-access/agents/) and [automations](/en-us/programmatic-access/automations/)
# C# SDK
> Install and use the official Sudomimus .NET packages.
The C# SDK publishes .NET packages for the Connect, Session, Native, and Token surfaces. Device authorization does not currently have a C# package; use the [Device API reference](/en-us/api/device/) directly for that flow.
## Packages
[Section titled “Packages”](#packages)
| Package | Use it for |
| ------------------- | --------------------------------------------------------------------------------------------------- |
| `Sudomimus.Connect` | Connect inquiry lifecycle and localized application metadata; token verification uses Session JWKS. |
| `Sudomimus.Session` | Refresh, introspection, logout, revoke-all, and helper token stores. |
| `Sudomimus.Native` | Steam ticket and AccessKey direct-issue. |
| `Sudomimus.Token` | Standalone token parsing and verification helpers. |
Install the packages your integration needs:
```bash
dotnet add package Sudomimus.Connect
dotnet add package Sudomimus.Session
```
## Connect
[Section titled “Connect”](#connect)
```csharp
using Sudomimus.Connect;
var client = new ConnectClient(new ConnectClientOptions
{
ClientAuth = new ConnectClientAuthWithKey
{
ApplicationAnchor = "your-app-anchor",
PrivateKeyPem = File.ReadAllText("client-auth.pem"),
},
});
var inquiry = await client.EstablishAsync(new EstablishRequest
{
ApplicationAnchor = "your-app-anchor",
});
```
`/establish` requires a client-auth JWT with audience `sudomimus-connect`. Configure `ConnectClientOptions.ClientAuth` to let the SDK sign it, or provide your own signer.
## Sessions
[Section titled “Sessions”](#sessions)
```csharp
using Sudomimus.Session;
var session = new RotatingSessionClient(
new SessionClient(),
new InMemoryTokenStore());
await session.SeedAsync(new TokenPair
{
AccessToken = accessToken,
RefreshToken = refreshToken,
});
var newAccessToken = await session.RefreshAsync();
await session.LogoutAsync();
```
`RevokeAllAsync` is an application-backend operation and requires client-auth signing with audience `sudomimus-session`.
## Token Verification
[Section titled “Token Verification”](#token-verification)
Token verification is independent of Connect. Resolve the token’s `kid` from the per-application Session JWK Set at `GET /applications/{applicationAnchor}/jwks.json`, cache it according to `Cache-Control`, and use `Sudomimus.Token` for parsing and signature verification. Connect `/info` only returns localized application metadata.
After verification, use payload `sub` as the application-visible user key, payload `sid` as the logical session id, and payload `jti` as the bearer-instance id. Access tokens contain no profile fields; refresh tokens contain no user identifier and add `rotationVersion`. Fetch current profile data from Session `/userinfo`. Signature-only verification cannot observe later logout, so use Session introspection for live-authority decisions.
## Source
[Section titled “Source”](#source)
[.NET packages](https://github.com/sudomimus/sudomimus/tree/master/sdks/csharp/src)Source and package README files.
[SDK overview](/en-us/sdk/overview/)How the official SDKs map to Sudomimus APIs.
# Java SDK
> Install and use the official Sudomimus Java packages.
The Java SDK is a Gradle multi-module project targeting JDK 17. The token module is available as an alpha package today; Connect, Session, and Native modules are planned.
## Packages
[Section titled “Packages”](#packages)
| Module | Maven coordinates | Use it for | Status |
| --------- | --------------------------------- | --------------------------------------------------------------------- | ------- |
| `token` | `com.sudomimus:sudomimus-token` | Parse and verify Sudomimus access, refresh, and OIDC ID tokens. | Alpha |
| `connect` | `com.sudomimus:sudomimus-connect` | Connect inquiry lifecycle: establish, status poll, redeem, and info. | Planned |
| `session` | `com.sudomimus:sudomimus-session` | Refresh-token lifecycle: refresh, introspect, logout, and revoke-all. | Planned |
| `native` | `com.sudomimus:sudomimus-native` | Steam ticket and AccessKey direct-issue. | Planned |
Add the token module from your package registry once published:
```kotlin
dependencies {
implementation("com.sudomimus:sudomimus-token")
}
```
## Token Verification
[Section titled “Token Verification”](#token-verification)
Use the token module in services that receive Sudomimus JWTs and need local verification. Resolve the token’s `kid` from the configured application’s Session JWK Set at `GET /applications/{applicationAnchor}/jwks.json`; do not derive the URL from an untrusted audience. Cache the set according to its HTTP response headers and refresh once when a `kid` is unknown.
The verification contract matches the other SDKs: parse the JWT, require RS256 and the expected token kind and audience, require a non-empty `kid`, check expiration, select that JWK, and verify the RSA-SHA256 signature.
## Source
[Section titled “Source”](#source)
[Java modules](https://github.com/sudomimus/sudomimus/tree/master/sdks/java)Source and module README files.
[SDK overview](/en-us/sdk/overview/)How the official SDKs map to Sudomimus APIs.
# Choose an SDK
> Official Sudomimus SDKs, how they map to the public APIs, and when to use each language package.
Sudomimus SDKs are strongly typed clients generated from the public OpenAPI 3.1 contracts in [`sudomimus/sudomimus-spec`](https://github.com/sudomimus/sudomimus-spec). They wrap the raw HTTPS APIs with request and response types, JSON serialization, client-auth signing, token parsing, Session JWKS caching, signature verification helpers, and structured API errors.
Use the SDK when you are building an application integration. Use the API reference when you need the exact wire contract for debugging, unsupported languages, or generated clients of your own.
## API Coverage
[Section titled “API Coverage”](#api-coverage)
| API | SDK responsibility |
| ------------- | -------------------------------------------------------------------------------------------------------- |
| Connect API | Start browser login inquiries, status-poll them, redeem them for tokens, and fetch application metadata. |
| Session API | Rotate refresh tokens, introspect sessions, log out one session, or revoke all sessions for a subject. |
| Device API | Start device-code authorization and exchange the device code after user approval. |
| Native API | Exchange Steam tickets or AccessKey credentials for ordinary Sudomimus tokens. |
| Token helpers | Parse and verify Sudomimus access and refresh JWTs. |
## Choose A Language
[Section titled “Choose A Language”](#choose-a-language)
[TypeScript / JavaScript](/en-us/sdk/typescript/)@sudomimus/connect, session, device, native, and token packages for Node.js and browser-capable runtimes.
[C#](/en-us/sdk/csharp/)Sudomimus.Connect, Session, Native, and Token packages for .NET applications.
[Java](/en-us/sdk/java/)JDK 17 modules under com.sudomimus, starting with the token verifier.
## Package Model
[Section titled “Package Model”](#package-model)
Each SDK package follows one Sudomimus API surface. Install the package for the flow you start with, then add the Session package for refresh-token lifecycle work after tokens are issued.
* **Connect** is the usual web-application login flow. It needs your application’s client-auth private key because `/establish` requires client-auth signing.
* **Device** is for public clients such as CLIs, launchers, TV apps, and terminals. It does not require a client secret.
* **Native** is for native credentials such as Steam Web API auth tickets and Sudomimus AccessKeys.
* **Session** is for everything that happens after initial issue: refresh, introspection, logout, and revoke-all.
* **Token** is for services that only verify access or refresh tokens and do not call an API.
The API walkthroughs still explain the underlying protocol:
[Connect integration](/en-us/connect/flow/)Browser login, status polling, callback, and redeem.
[Device authorization](/en-us/device/flow/)Device-code login for public clients.
[Native integration](/en-us/native/overview/)Steam ticket and AccessKey direct-issue.
[Managing sessions](/en-us/guides/managing-sessions/)Refresh, introspection, logout, and revoke-all.
## Source
[Section titled “Source”](#source)
[SDK monorepo](https://github.com/sudomimus/sudomimus)Source, README files, issues, and release tags for every official SDK package.
[OpenAPI contracts](https://github.com/sudomimus/sudomimus-spec)The authoritative public schemas the SDKs generate from.
# TypeScript SDK
> Install and use the official @sudomimus TypeScript packages.
The TypeScript SDK is split into small packages under the `@sudomimus/*` scope. The packages are usable from Node.js and browser-capable runtimes when the surrounding flow is appropriate for that environment.
## Packages
[Section titled “Packages”](#packages)
| Package | Use it for |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `@sudomimus/connect` | Connect inquiry lifecycle: `establish`, `statusPoll`, `redeem`, `info`; `info` returns localized application metadata. |
| `@sudomimus/session` | Refresh-token rotation, introspection, logout, revoke-all, and helper token stores. |
| `@sudomimus/device` | Device authorization: `deviceAuthorize`, `deviceToken`, and automatic polling helpers. |
| `@sudomimus/native` | Steam ticket and AccessKey direct-issue. |
| `jose` | Standards-based JWT parsing and cryptographic verification. |
Install only the packages your integration needs:
```bash
pnpm add @sudomimus/connect @sudomimus/session
npm install @sudomimus/connect @sudomimus/session
yarn add @sudomimus/connect @sudomimus/session
```
## Connect
[Section titled “Connect”](#connect)
```ts
import { ConnectClient, RETURN_METHOD } from "@sudomimus/connect";
const client = new ConnectClient({
clientAuth: {
applicationAnchor: "your-app-anchor",
privateKeyPem: process.env.SUDOMIMUS_CLIENT_AUTH_PRIVATE_KEY!,
},
});
const inquiry = await client.establish({
applicationAnchor: "your-app-anchor",
returnMethods: [{ type: RETURN_METHOD.STATUS_POLL, payload: {} }],
});
const status = await client.statusPoll({
exposureKey: inquiry.exposureKey,
hiddenKey: inquiry.hiddenKey,
});
if (status.status === "REALIZED") {
const tokens = await client.redeem({
exposureKey: inquiry.exposureKey,
hiddenKey: inquiry.hiddenKey,
confirmationKey: status.confirmationKey,
});
}
```
`/establish` requires a client-auth JWT with audience `sudomimus-connect`. Passing `clientAuth` lets the SDK sign those requests internally.
## Sessions
[Section titled “Sessions”](#sessions)
```ts
import {
InMemoryTokenStore,
RotatingSessionClient,
SessionClient,
} from "@sudomimus/session";
const store = new InMemoryTokenStore();
const session = new RotatingSessionClient(new SessionClient(), store);
await session.seed({
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
});
const accessToken = await session.refresh();
await session.logout();
```
`revokeAll` is an application-backend operation and requires client-auth signing with audience `sudomimus-session`.
## Device Authorization
[Section titled “Device Authorization”](#device-authorization)
```ts
import { DeviceClient, DeviceTokenApiError } from "@sudomimus/device";
const device = new DeviceClient({ baseUrl: "https://device-api.sudomimus.com" });
const auth = await device.deviceAuthorize({ applicationAnchor: "your-app-anchor" });
console.log(auth.userCode, auth.verificationUriComplete);
let intervalSeconds = auth.interval;
while (true) {
await new Promise((resolve) => setTimeout(resolve, intervalSeconds * 1000));
try {
const tokens = await device.deviceToken({ deviceCode: auth.deviceCode });
break;
} catch (error) {
if (error instanceof DeviceTokenApiError) {
if (error.error === "authorization_pending") continue;
if (error.error === "slow_down") {
intervalSeconds = error.interval ?? intervalSeconds + 5;
continue;
}
}
throw error;
}
}
```
Use `@sudomimus/session` after device authorization succeeds; the Device API returns ordinary Sudomimus access and refresh tokens.
## Token Verification
[Section titled “Token Verification”](#token-verification)
Token verification is independent of Connect. Fetch the application’s JWK Set from Session API at `GET /applications/{applicationAnchor}/jwks.json`, cache it according to `Cache-Control`, and select the exact key named by the token’s `kid`. Refresh the set once on an unknown `kid`. Use a standards-based JOSE implementation such as `jose` for JWT parsing and cryptographic verification; do not use Connect `/info` as a key source.
After successful verification, read the application-visible user key from payload `sub`, the logical session from payload `sid`, and the bearer instance from payload `jti`. Access tokens contain no profile fields; refresh tokens contain no user identifier and add only `rotationVersion`. Fetch current profile data from Session `/userinfo`. Offline verification cannot observe a later logout or authority change, so call Session `/introspect` when an operation requires live session state.
For supported token types and Account versus Workload admission, see [Tokens and verification](/en-us/concepts/tokens-and-verification/).
## Source
[Section titled “Source”](#source)
[TypeScript packages](https://github.com/sudomimus/sudomimus/tree/master/sdks/typescript/packages)Source and package README files.
[SDK overview](/en-us/sdk/overview/)How the official SDKs map to Sudomimus APIs.
# Avatar claims and delivery
> How Sudomimus exposes real and generated avatars through UserInfo while keeping delivery URLs scoped to an application sector.
Sudomimus has two avatar identity claims, `STATIC_AVATAR` and `ANIMATED_AVATAR`, alongside email, first name, and last name. The general policy and consent model is covered in [Identity claims and sharing](/en-us/concepts/identity-claims/); this page focuses on the avatar-specific delivery contract.
When an avatar claim is emitted, UserInfo returns `picture` for the static avatar and `picture_animated` for the animated avatar. Sudomimus scopes those URLs to the account and the application’s sector.
## Where avatar appears
[Section titled “Where avatar appears”](#where-avatar-appears)
| Surface | Field | When it appears |
| ------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Session `/userinfo` | `picture` | When the static avatar claim resolves to a real or placeholder value. |
| Session `/userinfo` | `picture_animated` | When the animated avatar claim resolves to a real or placeholder value. Falls back to `picture` when the selected avatar has no animation. |
| OIDC `/userinfo` | `picture` | When `profile` scope is granted and the static avatar claim resolves to a real or placeholder value. |
| OIDC `/userinfo` | `picture_animated` | When `profile` scope is granted and the animated avatar claim resolves to a real or placeholder value. Falls back to `picture` when the selected avatar has no animation. |
| Token-issuing response envelope | `claims.staticAvatar`, `claims.animatedAvatar` | Always included in the `claims` block so the client can see the application’s policy and the user’s grant state. |
The URL is safe to use as an image source. Do not parse it, infer identity from it, or treat it as a stable user identifier. Your user key is payload `sub` on an application access token, or standard `sub` on an OIDC ID token.
## Real vs placeholder avatars
[Section titled “Real vs placeholder avatars”](#real-vs-placeholder-avatars)
Both avatar claims follow the same claim policy enum as the other identity claims:
| Policy | Avatar behavior |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OFF` | No matching avatar field is emitted. |
| `OPTIONAL` | Emits the user’s account avatar only after the user grants the avatar claim. Otherwise it is omitted. |
| `REQUIRED` | Emits the user’s account avatar after the user grants the avatar claim. Non-interactive issue points reject instead of minting a token without it. |
| `SYNTHETIC_ONLY` | Always emits the sector placeholder avatar. It never asks for or shares the user’s account avatar. |
| `SYNTHETIC_FALLBACK` | Emits the user’s account avatar when granted; otherwise emits the sector placeholder avatar. It never blocks login. |
The user’s account avatar is the real avatar claim value. It may be an uploaded image or Sudomimus’s generated account avatar fallback, but it is still the account-level avatar the user controls. Applications default to a static placeholder avatar (`staticAvatar = SYNTHETIC_ONLY`) and animated avatar sharing is off by default (`animatedAvatar = OFF`).
The placeholder avatar is different: it is generated for the `(account, sector)` pair and stored with the sector subject’s placeholder identity. Two applications in the same sector can therefore see the same placeholder avatar for a user; applications in different sectors receive unrelated placeholder identities.
## Static and animated projections
[Section titled “Static and animated projections”](#static-and-animated-projections)
Account avatars have a static URL and an animated URL. The static projection is the safest default for normal profile UI. The animated projection is for products that intentionally support motion, such as richer profile cards or game/social surfaces.
If the selected avatar has no animation, the private `picture_animated` claim falls back to the static projection. That means clients can render the animated field when they support animation without first checking whether the source was animated.
## Sector-scoped delivery URLs
[Section titled “Sector-scoped delivery URLs”](#sector-scoped-delivery-urls)
Even when the claim resolves to the user’s account avatar, the value the application sees is a sector-scoped delivery URL. That matters for privacy:
* Treat the URL as opaque; do not parse identifiers out of it.
* Different sectors see different delivery URLs for the same account avatar.
* If the user rotates their sector subject, Sudomimus also regenerates the placeholder identity and avatar delivery handles for that sector.
* If a previously granted avatar claim stops being granted, Sudomimus rotates the real-avatar delivery handle for that account and sector.
Applications should store the user’s current avatar URLs only as display profile data. Do not use them for login, account merge, fraud checks, allow-lists, or cross-application correlation.
## Updates, revocation, and caching
[Section titled “Updates, revocation, and caching”](#updates-revocation-and-caching)
Claim grants are read live by UserInfo. The next `/userinfo` response may add, remove, or change the avatar field when:
* the user grants or revokes either avatar claim;
* the developer changes the application’s claim policy;
* the user changes their account avatar;
* the user rotates the sector subject;
* a `SYNTHETIC_FALLBACK` claim switches between real and placeholder because the grant changed.
Treat `picture` and `picture_animated` as replaceable profile fields. When you receive a new value, update your stored display avatar. When a field is absent, keep your own default avatar or clear the previously imported one according to your product rules.
Revocation removes the real avatar from future UserInfo responses and rotates Sudomimus’s sector delivery handle. It cannot recall copies already downloaded by the application or cached outside Sudomimus.
## OIDC behavior
[Section titled “OIDC behavior”](#oidc-behavior)
OIDC `/userinfo` maps avatar claims to `picture` and the private `picture_animated` claim. Both fields are controlled by the avatar claim outcome and the `profile` scope:
```json
{
"sub": "",
"picture": "",
"picture_animated": ""
}
```
If `profile` was not granted, both OIDC avatar fields are absent even when the claim policy would otherwise emit an avatar, and no avatar delivery is materialized for that token issuance. If the policy emits a placeholder avatar, the OIDC values follow the same sector-scoped placeholder delivery contract.
## Integration checklist
[Section titled “Integration checklist”](#integration-checklist)
* Request the avatar claim only when your product needs to display a user image.
* Use `SYNTHETIC_ONLY` when a stable generated avatar is enough and you do not need real profile data.
* Use `SYNTHETIC_FALLBACK` when you prefer the user’s real avatar but need non-blocking login.
* Use `REQUIRED` only when a real account avatar is product-critical and your client can handle claim-gate recovery.
* Use the static field for ordinary profile UI; use the animated field only on surfaces that intentionally support motion.
* Key users by `subject` / `sub`, never by avatar URLs.
* Handle the avatar field being missing or changing between UserInfo requests.
## Related
[Section titled “Related”](#related)
* [Avatar uploads](/en-us/user-generated-content/avatar-uploads/) - the account-side upload and processing flow.
* [Identity claims and sharing](/en-us/concepts/identity-claims/) - the policy, grant, and `claims` block model.
* [Tokens and verification](/en-us/concepts/tokens-and-verification/) - how tokens and UserInfo work together.
* [Pairwise identity](/en-us/concepts/pairwise-identity/) - why sector-scoped identifiers and placeholder identities exist.
* [Native claims and the Errand](/en-us/native/claims-and-errand/) - how native direct-issue handles required claims.
# Avatar review lifecycle
> What happens after a user uploads an avatar, and what applications should expect while review is pending.
Uploaded avatars are user-controlled media, so Sudomimus separates **account preview** from **application delivery**.
The user may see a newly uploaded image on their own account surface while it is waiting for review. Applications continue to receive the current approved avatar, or a generated fallback, until the upload is approved.
## States
[Section titled “States”](#states)
| State | What the user sees | What applications receive |
| ---------- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `PENDING` | The account surface can show the uploaded preview and mark it as waiting for review. | The previous approved avatar, or the generated fallback. |
| `APPROVED` | The upload becomes the account avatar. | Future `/userinfo` responses can include the approved avatar when claim policy and consent allow it. |
| `REJECTED` | The upload should be shown as rejected or replaced by the fallback/current avatar. | No change. Applications keep receiving the previous approved avatar or fallback. |
## Application delivery
[Section titled “Application delivery”](#application-delivery)
Applications do not fetch account-avatar review state directly. They see avatar data only through the claim system:
* Session and OIDC `/userinfo` can include `picture` and `picture_animated`.
* Avatar fields appear only when the application’s claim policy and the user’s grant allow them.
When an upload is approved, applications see the new avatar in the next `/userinfo` response that includes the avatar claim. Treat those URLs as replaceable profile fields, not permanent identifiers.
## UI guidance
[Section titled “UI guidance”](#ui-guidance)
* Show pending uploads only in account-management UI, not as if they were already published to applications.
* Keep a fallback ready for rejected uploads.
* Do not promise that an uploaded avatar is visible to applications until it is approved.
* If your product supports motion, use the animated URL; otherwise render the static URL.
## Related
[Section titled “Related”](#related)
* [Avatar uploads](/en-us/user-generated-content/avatar-uploads/) - upload constraints and intent flow.
* [Avatar claims and delivery](/en-us/user-generated-content/avatar-claims-and-delivery/) - how applications receive avatar URLs.
# Avatar uploads
> Change your account avatar in the With portal and understand how review affects application delivery.
Manage your avatar on the [With account page](https://with.sudomimus.com/account/#avatar). The portal handles upload, cropping, processing, and review status.
## Change your avatar
[Section titled “Change your avatar”](#change-your-avatar)
1. Sign in to the With portal and open your account’s avatar section.
2. Choose an image file and follow the crop and upload controls.
3. Wait for processing and check the displayed review state. An accepted upload still needs moderation before applications can receive it.
4. Use the avatar menu to reset to a generated avatar when that action is available.
Follow the file-size and format limits shown in the portal. Upload acceptance depends on the actual image, not just its filename. Processing may reject malformed or unsupported images.
## Animation and delivery
[Section titled “Animation and delivery”](#animation-and-delivery)
The portal supports static images and eligible animated GIF/WebP uploads. Processing normalizes the image; applications receive WebP delivery images rather than the original file. Use the static avatar when animation is inappropriate, and respect the user’s motion preferences.
## Review states
[Section titled “Review states”](#review-states)
* **Pending:** the account page can show your uploaded preview while review is pending. Applications continue to receive the approved avatar or generated fallback allowed by claim policy.
* **Approved:** the uploaded avatar becomes eligible for sharing through the normal claim policy and consent checks.
* **Rejected:** the upload is not shared with applications. Check the portal’s current avatar and review message before uploading a replacement.
See [Avatar review lifecycle](/en-us/user-generated-content/avatar-review-lifecycle/) for review behavior.
## For application developers
[Section titled “For application developers”](#for-application-developers)
Direct users to the With portal to change their avatar. The portal cannot be embedded, and its account-management API is reserved for first-party clients; ordinary application access tokens cannot call it.
Obtain shared avatar URLs through Session or OIDC UserInfo, following [Avatar claims and delivery](/en-us/user-generated-content/avatar-claims-and-delivery/). Treat those URLs as display data: do not parse them, use them as account identifiers, or depend on uploaded originals. Availability follows the application’s claim policy, the user’s consent, and current review state.
# User-generated content
> How Sudomimus handles user-provided profile media, generated avatars, moderation, and avatar delivery to applications.
Sudomimus user-generated content currently centers on account avatars: users can keep a generated avatar, upload their own image, and decide which applications may receive a static or animated avatar claim.
The system separates three concerns:
* **Account media** is controlled by the user in the With portal.
* **Processing and moderation** normalize uploaded media into safe public WebP projections.
* **Application delivery** uses claim policy, user consent, and sector-scoped opaque URLs.
[Avatar uploads](/en-us/user-generated-content/avatar-uploads/)The upload intent flow, file constraints, processing states, and moderation behavior.
[Avatar review lifecycle](/en-us/user-generated-content/avatar-review-lifecycle/)What pending, approved, and rejected uploads mean for account pages and applications.
[Avatar claims and delivery](/en-us/user-generated-content/avatar-claims-and-delivery/)How applications receive static and animated avatar URLs through Session or OIDC UserInfo.
## Account avatars
[Section titled “Account avatars”](#account-avatars)
Each account has one current account avatar. It can come from:
| Source | Meaning |
| ----------- | --------------------------------------------------------------------------------------- |
| `GENERATED` | A Sudomimus-generated account avatar. This is the default and can be reset at any time. |
| `UPLOAD` | A user-uploaded image accepted by the upload pipeline. |
The account avatar is not automatically shared with every application. Applications receive it only when their claim policy requests `STATIC_AVATAR` or `ANIMATED_AVATAR` and the user’s grant allows the real avatar. Otherwise, applications may receive a sector placeholder avatar or no avatar field, depending on policy.
## Processing model
[Section titled “Processing model”](#processing-model)
Uploaded originals are private upload candidates. After the user completes an upload intent, Sudomimus validates the uploaded object, creates a new avatar asset, and enqueues asynchronous normalization. Public avatar delivery uses normalized WebP files, not the original upload.
The normalized output is square, `512x512`, and has a static projection. Animated inputs can also produce an animated WebP projection. If an avatar has no animation, animated delivery falls back to the static URL.
## Related
[Section titled “Related”](#related)
* [Identity claims and sharing](/en-us/concepts/identity-claims/) - the developer policy and user grant model.
* [Avatar uploads](/en-us/user-generated-content/avatar-uploads/) - the account-side upload flow.
* [Avatar review lifecycle](/en-us/user-generated-content/avatar-review-lifecycle/) - what review state means before an upload reaches applications.
* [Avatar claims and delivery](/en-us/user-generated-content/avatar-claims-and-delivery/) - the application-side delivery contract.
# Take an application live
> Prepare a new application, take it live, disable it, or bring it back later.
Every new application starts as a **Draft**. A draft can be configured from the With portal, but it cannot start sign-ins or issue sessions.
## Prepare the application
[Section titled “Prepare the application”](#prepare-the-application)
Before going live, complete the readiness checklist on the application page:
1. Add at least one [authentication rule](/en-us/application-rules/authentication-rules/) for the sign-in methods you support.
2. Add at least one [realize rule](/en-us/application-rules/realize-rules/) for the people who may sign in.
3. Add at least one [return rule](/en-us/application-rules/return-rules/) for how the result reaches your application.
4. Save the client-auth private key and any OIDC secret your backend needs.
5. Review the application name, sign-in appearance, claim policy, and callback URLs.
The template picker can create a starting set of rules for common web, desktop, game, and OIDC integrations. Review the generated rules before saving them; a template is only a starting point.
## Go live
[Section titled “Go live”](#go-live)
When the checklist is complete, an organization owner can choose **Go live**. This changes the application from `DRAFT` to `ACTIVE`.
Going live is a separate confirmation. Saving rules does not activate the application automatically. Activation can be blocked if the organization or sector is disabled, or if the organization has reached its application limit.
## Disable an application
[Section titled “Disable an application”](#disable-an-application)
Use **Disable application** when you are retiring an integration or responding to a security incident. For self-service organizations, the person disabling it must be the organization’s sole owner.
After disablement:
* new sign-ins and token refreshes stop;
* live UserInfo and introspection checks stop accepting the old sessions;
* an access token may still pass offline signature and expiry checks until its `exp`;
* the application no longer counts toward the organization’s active application limit.
Coordinate the change with your application. Clear its local sessions and stop accepting the old integration before treating retirement as complete.
## Bring it back
[Section titled “Bring it back”](#bring-it-back)
An owner can reactivate a disabled application. Its existing configuration remains available, but activation is checked against the current organization, sector, and quota state.
Applications are not deleted and do not return to Draft. If you are replacing an integration permanently, create a new application with a new `applicationAnchor`.
## Related
[Section titled “Related”](#related)
* [Organizations and applications](/en-us/with-portal/organizations-and-applications/) - create and configure application resources.
* [Configuration templates](/en-us/application-rules/templates/) - starting rules for common integrations.
* [Managing sessions](/en-us/guides/managing-sessions/) - how applications refresh and revoke sessions.
# Review data sharing
> See what each application may receive, revoke access, and review your decision history.
Open [`with.sudomimus.com`](https://with.sudomimus.com), then go to **Account → Data sharing**.
## Current sharing
[Section titled “Current sharing”](#current-sharing)
The **Current sharing** section shows which applications may receive your real email, name, static avatar, or animated avatar through UserInfo. It also distinguishes real information from generated placeholder identity.
Choose **Revoke** to clear your sharing choices for an application. Optional information stops appearing in new UserInfo responses immediately. If the application requires some information, you will be asked again the next time you sign in.
Revoking a grant cannot delete copies an application already stored. If you also want to stop the application’s ongoing access, review its sessions in [Session security](/en-us/with-portal/session-security/).
## Decision history
[Section titled “Decision history”](#decision-history)
The **Decision history** section records what was presented and what you chose during a sign-in or consent flow. You can download a receipt as JSON for your own records.
A receipt records your decision. It does not prove that an application retrieved or retained the information. History begins when receipt recording became available; earlier choices are not reconstructed.
OIDC receipts may also show requested scopes, granted scopes, and whether you allowed offline access.
## Related
[Section titled “Related”](#related)
* [Identity claims and sharing](/en-us/concepts/identity-claims/) - how application policy and user consent work together.
* [Privacy controls](/en-us/with-portal/privacy/) - rotate identifiers or delete the account.
# Organizations and applications
> Configure organizations, members, applications, sectors, keys, claim policy, and rules in the With portal.
Organizations own applications and sectors. If you are building with Sudomimus for a team, create or join an organization first, then manage applications inside it.
## Create an organization
[Section titled “Create an organization”](#create-an-organization)
In [`with.sudomimus.com`](https://with.sudomimus.com), open the developer area and create an organization. The creator becomes the first owner.
Keep at least two active owners for production organizations. Some retirement and deletion operations protect the last owner, so a sole owner should use a long-term account.
## Invite members
[Section titled “Invite members”](#invite-members)
Members are invited by account alias. The user can find their alias in their account area.
| Role | Use it for |
| ------ | ----------------------------------------------------------------------------- |
| Viewer | Read-only inspection of applications, sectors, domains, and rules. |
| Admin | Day-to-day configuration: applications, rules, keys, domains, and connectors. |
| Owner | Membership, role changes, and retirement operations. |
Use the lowest role that lets someone do their job.
## Register an application
[Section titled “Register an application”](#register-an-application)
Create an application inside an organization when you are ready to integrate a product, service, game, or tool.
New applications start in `DRAFT`. Configure all three rule layers, review the readiness checklist, and then ask an organization owner to choose **Go live**. See [Application lifecycle](/en-us/with-portal/application-lifecycle/) for the complete flow.
After creation, record:
* **`applicationAnchor`** - the public client identifier used by Connect, OIDC, Device, Native, and SDK setup.
* **Client-auth private key** - shown when you create the application and used by confidential backends to sign `/establish`, Session `/revoke-all`, and OIDC `private_key_jwt`.
* **OIDC client secret** - needed only when your OIDC return rule uses `client_secret_basic` or `client_secret_post`. Generate or rotate it from the application page.
Store private keys and client secrets as production secrets. If a key or secret leaks, rotate it from the application page and update your backend.
Application creation and client-auth rotation results also show the matching public key. You can copy it as original PEM or DER Base64; unlike the private half, it is not secret.
If creation or rotation ends with an unknown network result, retry the unchanged form. The portal preserves the operation identifier and can recover the exact committed result for ten minutes. A committed rotation still invalidates the old key immediately; recovery does not create an overlap window.
## Manage token-signing keys
[Section titled “Manage token-signing keys”](#manage-token-signing-keys)
Open the application’s **Signing Keys** tab to inspect the complete signing-key lifecycle and copy the canonical Session JWKS URL. Viewers can inspect and copy each key as original PEM, DER Base64, or JWK with Base64URL members, and download public PEM and JWK files. Admins and owners can:
* Prepublish one replacement key so cached verifiers can discover its `kid`.
* Activate it after the displayed cache-warm boundary. The previous key enters RETIRING and remains published for the maximum token lifetime.
* Cancel a pending key, or emergency-revoke a retiring key when accepting its outstanding tokens is a greater risk than breaking them.
Emergency revocation removes the key from fresh Session JWKS responses immediately. Verifiers that already cached it may continue accepting its signatures for the remainder of their advertised cache lifetime, up to 300 seconds after their last successful fetch. Treat that bounded convergence window as part of incident response.
Token consumers should cache the Session JWKS and select the exact key named by the JWT `kid`. The application creation result and Credentials tab do not expose a singular token-signing public key.
## Configure claims
[Section titled “Configure claims”](#configure-claims)
On the application page, configure claim policy for email, first name, last name, static avatar, and animated avatar.
Ask only for claims your product actually needs. If a claim is optional, design your UI to work when the user denies it. If a claim is required, be ready to handle consent or missing-data recovery.
## Configure the sign-in experience
[Section titled “Configure the sign-in experience”](#configure-the-sign-in-experience)
Use the **Appearance** tab to preview the sign-in card, choose how sign-in methods are arranged, and set the application name shown to users.
Authentication entry is configured separately. `USER_CHOICE` always shows the sign-in card. `AUTO_WHEN_SINGLE` can continue automatically when exactly one eligible passkey or consumer sign-in option is available; email entry, multiple choices, cancellations, and retries still show the card.
The same tab lets you add an application homepage, privacy policy, and optional terms of service. Each URL must use a verified domain owned by the organization. If that domain is later released or loses verification, Sudomimus stops showing the affected link until you repair it.
## Configure rules
[Section titled “Configure rules”](#configure-rules)
Every application needs all three rule layers:
* **Layer 1** - which sign-in methods are allowed.
* **Layer 2** - which identities may log in.
* **Layer 3** - how the result is delivered.
The common setup for a web application is passkey or email OTP, an `EVERYONE` or email-based realize rule, and a `CALLBACK` return rule.
## Sectors and pairwise identity
[Section titled “Sectors and pairwise identity”](#sectors-and-pairwise-identity)
A sector groups applications that should see the same pairwise subject for a user. Use one sector when products intentionally share an account identity. Use separate sectors when products should not be able to correlate users through Sudomimus identifiers.
## Configure security webhooks
[Section titled “Configure security webhooks”](#configure-security-webhooks)
Use the **Webhooks** tab to send signed application security events to your backend. See [Application webhooks](/en-us/with-portal/webhooks/) for endpoint, signature, retry, and duplicate-delivery guidance.
## Retire resources
[Section titled “Retire resources”](#retire-resources)
Applications and sectors are disabled rather than deleted. An application may move from `DRAFT` to `ACTIVE`, then between `ACTIVE` and `DISABLED`; it never returns to Draft. Disable an application before decommissioning it, and make sure clients stop using its keys and tokens.
Account deletion can be blocked when you are the sole owner of an organization with live applications or sectors. Retire the resources or add another active owner before deleting the account.
## Related
[Section titled “Related”](#related)
* [Configuration templates](/en-us/application-rules/templates/) - starting points for common application types.
* [Application lifecycle](/en-us/with-portal/application-lifecycle/) - go-live and retirement behavior.
* [Application webhooks](/en-us/with-portal/webhooks/) - receive signed security events.
* [Layer 3 — Return rules](/en-us/application-rules/return-rules/) - callbacks, polling, device code, direct-issue, and OIDC.
* [Account deletion](/en-us/guides/account-deletion/) - how organization ownership can block erasure.
# Use the With portal
> What account holders and organization members manage in with.sudomimus.com.
The With portal at [`with.sudomimus.com`](https://with.sudomimus.com) is where Sudomimus users manage their own account and, when they belong to an organization, configure applications.
Use it for:
* Account profile, avatar, sign-in methods, external identifiers, data sharing, and privacy controls.
* Access keys, public keys, agents, and automations for software that runs without a browser.
* Organization membership, applications, sectors, domains, federation connectors, and application rules.
External applications should still integrate through Connect, OIDC, Device Authorization, Native direct-issue, and Session API. The With portal is the first-party control surface for humans configuring Sudomimus.
[Manage sign-in methods](/en-us/with-portal/sign-in-methods/)Add passkeys, link providers, enroll email OTP, and remove credentials safely.
[Programmatic access](/en-us/programmatic-access/overview/)Choose an identity and credential for command-line tools, services, agents, and automations.
[Session security](/en-us/with-portal/session-security/)Review recent application sessions and end access you no longer trust.
[Data sharing](/en-us/with-portal/data-sharing/)Review current sharing, revoke grants, and download decision receipts.
[Privacy controls](/en-us/with-portal/privacy/)Rotate identity boundaries, review our privacy commitment, and permanently delete your account.
[Organizations and applications](/en-us/with-portal/organizations-and-applications/)Create organizations, invite members, register applications, rotate keys, and configure rules.
[Application webhooks](/en-us/with-portal/webhooks/)Send signed application security events to your HTTPS endpoint.
## Account area
[Section titled “Account area”](#account-area)
Every signed-in user has an account area. Start here when you want to:
* Update your display name or avatar.
* Add or remove sign-in methods.
* Inspect and copy your account alias, sector subjects, and placeholder identities.
* Review which applications can receive your profile claims.
* Rotate an account alias or sector subject from the dedicated Privacy view.
* Manage access keys and public keys for your account, agents, and automations.
* Review and end application sessions.
* Review current data sharing and download decision receipts.
* Check account deletion blockers before erasing the account.
Account actions affect your own Sudomimus account. They do not grant access to another user’s data.
Start with [Programmatic access](/en-us/programmatic-access/overview/) when a service, command-line tool, agent, or automation needs to sign in without a browser.
## Developer area
[Section titled “Developer area”](#developer-area)
The developer area is available to every signed-in user. If you do not belong to an organization yet, the organization list is empty and you can create your first one there.
| Resource | What you manage |
| ---------------------- | -------------------------------------------------------------------------------------------------------- |
| Organizations | Name, members, roles, retirement state, and owned resources. |
| Applications | Application anchors, credentials, appearance, legal links, claim policy, rules, webhooks, and lifecycle. |
| Sectors | Pairwise subject grouping for applications. |
| Domains and federation | Verified domains, login policy, and enterprise OIDC or SAML connectors. |
Roles matter. Viewers can inspect resources, admins can change most configuration, and owners control membership and retirement operations.
## Related
[Section titled “Related”](#related)
* [Choose an integration path](/en-us/getting-started/choose-integration/) - which public protocol your application should use.
* [Application rules](/en-us/application-rules/overview/) - the three rule layers you configure on an application.
* [Application lifecycle](/en-us/with-portal/application-lifecycle/) - prepare, activate, disable, or reactivate an application.
* [Domains and SSO](/en-us/domains-federation/overview/) - domain adoption, login policy, and enterprise federation.
# Privacy controls
> Rotate account and sector identifiers, understand Sudomimus privacy commitments, and permanently delete an account.
Open [`with.sudomimus.com`](https://with.sudomimus.com), then go to **Account → Privacy** when you want to change an identity boundary or permanently erase your account. The separate **External identifiers** view remains the place to inspect and copy your current account alias, sector subjects, and placeholder identities.
## Your data is not a product
[Section titled “Your data is not a product”](#your-data-is-not-a-product)
Sudomimus never sells your personal information or shares your identity details with third-party applications unless you choose or authorize it. A small number of service providers process only the information needed to operate Sudomimus, under our instructions.
Use **Data sharing** to review and revoke the real profile claims each application can receive. The complete legal terms, including service providers and third-party sign-in, are in the [Privacy Policy](https://sudomimus.com/legal-hub/privacy).
## Rotate an account alias
[Section titled “Rotate an account alias”](#rotate-an-account-alias)
Your account alias is the handle you can share out of band when an application owner needs to place you on an allow-list. Applications never receive it.
Rotation permanently replaces the alias. Any allow-list that contains the old value stops matching you, and the old value cannot be restored. The confirmation dialog requires the exact current alias before it submits the change.
## Rotate a sector identifier
[Section titled “Rotate a sector identifier”](#rotate-a-sector-identifier)
A sector subject is the `sub` applications in one sector use to recognize you. Rotating it deliberately ends that identity continuity:
* Refreshable sessions for that sector can no longer continue.
* Applications in the sector see you as a new user on the next sign-in.
* The sector’s placeholder name, proxy email, and avatar identity are regenerated so they cannot link the old and new subjects.
Choose the sector carefully and type its exact sector anchor in the confirmation dialog. The operation cannot be undone.
## Delete an account
[Section titled “Delete an account”](#delete-an-account)
Account deletion is permanent. The Privacy view runs a precheck before showing the final confirmation. Deletion is blocked while you are the sole active owner of an organization that still has an enabled application or non-disabled sector; retire those resources or add another active owner first.
The flow erases account data and revokes refreshable access. Already-issued access tokens remain valid only until their normal short expiration. See [Account deletion](/en-us/guides/account-deletion/) for the integration behavior applications observe.
## Related
[Section titled “Related”](#related)
* [Review data sharing](/en-us/with-portal/data-sharing/) - see current grants, revoke them, and download decision receipts.
* [Session security](/en-us/with-portal/session-security/) - review and end application sessions.
* [Privacy & pairwise identity](/en-us/concepts/pairwise-identity/) - why aliases, sector subjects, and rotation exist.
* [Identity claims and sharing](/en-us/concepts/identity-claims/) - how users control real profile claims.
* [Manage sign-in methods](/en-us/with-portal/sign-in-methods/) - add or remove credentials without deleting the account.
# Review and end sessions
> See where your account is signed in and end access you no longer trust.
Open [`with.sudomimus.com`](https://with.sudomimus.com), then go to **Account → Session security**. This page lists your recent Sudomimus application sessions, including the application, sign-in channel, authentication method, start time, last refresh, and expiry.
Use the application filter when you only want to review one product.
When an agent or automation signs in, the page also shows that actor. This lets you distinguish your own activity from access performed by a specific agent or automation.
## End access
[Section titled “End access”](#end-access)
You can:
* end one session;
* end every session for one application; or
* end all application sessions for your account.
Ending all sessions also ends the current With session, so you will need to sign in again.
## What ending a session does
[Section titled “What ending a session does”](#what-ending-a-session-does)
The selected session can no longer refresh its credentials or use Sudomimus endpoints that check live session status. It may take a moment for the application to update its own screen.
An application may also keep a separate local session. Sudomimus cannot delete that application-owned cookie or local data directly. An already-issued access token may continue to pass offline signature and expiry checks until it expires.
If you do not recognize a session, end it and then review your [sign-in methods](/en-us/with-portal/sign-in-methods/). Remove any passkey or linked account you no longer control.
If the unfamiliar session belongs to an agent or automation, review that principal and its credentials as well. Ending one session does not revoke the access key or public key it used. Suspend the principal or revoke the credential from [Programmatic access](/en-us/programmatic-access/overview/) if it may be compromised.
## Related
[Section titled “Related”](#related)
* [Data sharing](/en-us/with-portal/data-sharing/) - review what each application may receive.
* [Programmatic access](/en-us/programmatic-access/overview/) - manage agents, automations, and their credentials.
* [Managing sessions](/en-us/guides/managing-sessions/) - application-side session APIs.
# Manage sign-in methods
> Add passkeys, link providers, enroll email OTP, and remove credentials from the With portal.
Sign-in methods are the ways your Sudomimus account can prove it is you. One account can have several methods: passkeys, email OTP, Google, GitHub, Discord, Battle.net, X, Steam, and enterprise federation.
Open [`with.sudomimus.com`](https://with.sudomimus.com), then go to **Account → Sign-in methods**.
## Add a passkey
[Section titled “Add a passkey”](#add-a-passkey)
Passkeys must be created on `via.sudomimus.com`, because browsers bind passkeys to a relying-party domain.
From the With portal:
1. Open **Sign-in methods**.
2. Enter a name for the passkey, such as `Work laptop` or `Security key`.
3. Choose **Add passkey**.
4. Follow the browser’s passkey prompt on `via.sudomimus.com`.
5. Return to the With portal and confirm the success message.
You can add more than one passkey. Use names that help you recognize the device later.
## Link a provider
[Section titled “Link a provider”](#link-a-provider)
Provider linking starts in the With portal and finishes on `via.sudomimus.com` or the upstream provider.
Use this when you want the same account to accept another provider, such as Google or GitHub. If the provider account is already linked elsewhere, the portal will stop the link instead of silently moving it.
Sudomimus also stops a link when its verified email would put the account under conflicting organization sign-in policies. Use a separate Sudomimus account or contact support if the organizations cannot align those policies.
## Enroll email OTP
[Section titled “Enroll email OTP”](#enroll-email-otp)
Each verified email can have its own email-code sign-in method. In **Verified emails**, choose **Enable email code login** beside the address you want to use. Use an address you control and expect to keep receiving.
Email ownership and email-code sign-in are related but not identical: a verified email can be part of your profile without being enabled for sign-in.
You can also add another email and choose which verified address is primary. Before removing the primary email, set another verified address as primary. If an email is still used by a sign-in method, remove that method before removing the address.
## Resolve an SSO authority conflict
[Section titled “Resolve an SSO authority conflict”](#resolve-an-sso-authority-conflict)
The page shows a red warning when your verified emails belong to organizations that require different enterprise sign-in providers. While that warning is active, the account cannot start another sign-in or refresh access. Access that was already issued lasts only until its normal expiry.
Ask the relevant organization administrator or Sudomimus support to align the policies, remove a conflicting non-primary email, or separate the identities into different accounts. The warning disappears automatically once the conflict is resolved.
## Remove a sign-in method
[Section titled “Remove a sign-in method”](#remove-a-sign-in-method)
Remove credentials you no longer control, such as an old passkey or a provider account you no longer use.
Before removing a method:
* Make sure at least one other sign-in method remains.
* Avoid removing the only method you can use on your current device.
* Remove lost or shared-device passkeys promptly.
## Related
[Section titled “Related”](#related)
* [Accounts and credentials](/en-us/concepts/accounts-and-credentials/) - how accounts, emails, and authentication methods relate.
* [Layer 1 — Authentication rules](/en-us/application-rules/authentication-rules/) - which sign-in methods an application can allow.
# Configure application webhooks
> Receive signed application security events at an HTTPS endpoint.
Application webhooks notify your backend about supported security events. They are useful for reacting quickly, but they are not a replacement for JWKS, introspection, or the normal Sudomimus APIs when you need current authority.
The first supported event is `APPLICATION_SIGNING_KEY_ACTIVATED`.
## Create an endpoint
[Section titled “Create an endpoint”](#create-an-endpoint)
Open the application in [`with.sudomimus.com`](https://with.sudomimus.com), then choose **Webhooks**.
1. Enter a name and a public HTTPS endpoint.
2. Create the webhook and copy its signing secret immediately.
3. Store the secret in your backend’s secret manager.
4. Implement signature verification and duplicate handling before the event can trigger a production action.
An application can have up to five endpoints. Viewer members can inspect endpoints and delivery history; Admin and Owner members can create, update, disable, delete, and rotate them.
The signing secret is shown only when the endpoint is created or the secret is rotated. If you lose it, rotate it and update your backend.
## Verify every request
[Section titled “Verify every request”](#verify-every-request)
Sudomimus sends these headers:
* `Sudomimus-Webhook-Id`
* `Sudomimus-Webhook-Timestamp`
* `Sudomimus-Webhook-Signature`
The signature is `v1=` followed by the Base64URL HMAC-SHA256 of:
```text
.
```
Verify the signature before parsing the JSON body. Compare signatures in constant time, reject timestamps more than five minutes from your server clock, and keep the webhook ID so duplicate deliveries do not repeat the same action.
## Respond and retry safely
[Section titled “Respond and retry safely”](#respond-and-retry-safely)
Return any `2xx` status after accepting the event. Sudomimus retries timeouts, network failures, `408`, `429`, and `5xx` responses. Other `4xx` responses stop delivery for that event.
Delivery is at least once, so the same webhook ID can arrive more than once. Make event handling idempotent.
The Webhooks tab shows recent status, attempt count, HTTP status, and a safe failure summary. It does not store or display your response body.
## Endpoint requirements
[Section titled “Endpoint requirements”](#endpoint-requirements)
Webhook endpoints must use public HTTPS on the standard TLS port. Redirects, URL credentials, fragments, private network addresses, loopback hosts, and cloud metadata addresses are rejected.
Disable an endpoint before planned maintenance if you do not want pending events sent to it. Updating the URL or rotating the secret also prevents older pending deliveries from being sent with stale configuration.
## Related
[Section titled “Related”](#related)
* [Application lifecycle](/en-us/with-portal/application-lifecycle/) - take an application live or disable it.
* [Manage token-signing keys](/en-us/with-portal/organizations-and-applications/#manage-token-signing-keys) - the event currently delivered by webhooks.