---
title: Layer 3 — Return rules
description: Configure how authentication results reach the application —
  browser callbacks, native status polling, one-time REVEAL, native
  direct-issue, or OIDC.
editUrl: true
head: []
template: doc
sidebar:
  order: 4
  hidden: false
  attrs: {}
pagefind: true
draft: false
---

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

:::tip[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.

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

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

| 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`) or AccessKey (`POST /direct-issue/access-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. |

## 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

`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

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

:::caution[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

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

`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 and AccessKey).

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

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

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

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)

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

<CardGrid>
<LinkCard
    title="The three-layer rules model"
    description="The overall picture — allowlist + default-deny, evaluation order, and how the three layers compose."
    href="/en-us/application-rules/overview/"
/>
<LinkCard
    title="Layer 1 — Authentication rules"
    description="Which authentication methods are offered to the user."
    href="/en-us/application-rules/authentication-rules/"
/>
<LinkCard
    title="Layer 2 — Realize rules"
    description="The post-authentication identity check, using email allowlists."
    href="/en-us/application-rules/realize-rules/"
/>
<LinkCard
    title="OIDC relying parties"
    description="End-to-end OIDC flow that uses the OIDC return rule."
    href="/en-us/oidc/flow/"
/>
</CardGrid>