---
title: Tokens and verification
description: The full JWT claim reference for access tokens, refresh tokens, and
  OIDC ID tokens — what each carries, how to verify, TTL bounds, and how the
  email claim is selected.
editUrl: true
head: []
template: doc
sidebar:
  order: 6
  hidden: false
  attrs: {}
pagefind: true
draft: false
---

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

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

| Token | Issued by | Signed with | Verified via | Carries |
|---|---|---|---|---|
| **Access token** | Connect (`/redeem`, `/refresh`, `/direct-issue/*`, `/token`) | The **application's** token-signing private key | `POST /info` → `applicationPublicKey` | `subject`, `firstName`, `lastName?`, `emailAddress?` |
| **Refresh token** | Connect (`/redeem`, `/direct-issue/*`, `/token`) | The **application's** token-signing private key | Same as access token | `subject` |
| **OIDC ID token** | OIDC (`/token`) | A **platform-wide** OIDC signing key | `oidc.sudomimus.com/.well-known/jwks.json` | `sub`, `iss`, `aud`, `exp`, `iat`, `nonce?`, `auth_time?`, `email?`, `name?` |

All three are JWTs signed with **RS256** (RSA-2048).

:::note[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, which is how you tell *why* an optional claim like `emailAddress` is absent. See [the `claims` block](/en-us/concepts/identity-claims/#the-claims-block).
:::

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

Your application verifies signatures against its own **token-signing public key**, fetched from `POST /info`. The recommended flow is:

1. On startup, call `POST /info` with your `applicationAnchor` and cache the returned `applicationPublicKey`.
2. For each incoming request, parse the JWT, check `kty === "Access"` in the header (or `"Refresh"` for refresh tokens), check `exp`, and verify the signature against the cached key.
3. After the application key is rotated (via the developer portal), clear the cache and refetch.

The official SDKs do all of this for you — see [`verifyAccessToken` in the SDK reference](/en-us/reference/sdks/).

There is **no JWKS for Connect tokens.** Verification is per-application by design: it means a relying party only needs to know one key (the one tied to its own `applicationAnchor`), and a compromise of one application's key cannot affect any other.

### The `kty` claim

Sudomimus access and refresh JWTs carry a custom `kty` field in the JWT header to make the token type explicit:

- Access tokens: `kty: "Access"` (exact case)
- Refresh tokens: `kty: "Refresh"` (exact case)

Reject any access-token check that finds `kty: "Refresh"` (or vice versa) — the official SDKs do this for you, but if you verify manually, this is one of the easier mistakes to miss.

### 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 refresh TTL is then raised, if necessary, so that it is always at least as long as the access TTL.

### Access token claims

Sudomimus access and refresh tokens carry every **standard JWT claim** (`kty`, `iss`, `aud`, `sub`, `iat`, `exp`) in the **JWT header**, and only the **application-specific claims** (`subject`, `firstName`, `lastName`, `emailAddress`) in the **body**. If you verify by hand, read `iss` / `aud` / `exp` from the header, not the payload — the official SDKs do this for you.

```json
// JWT header
{
  "alg": "RS256",
  "kty": "Access",
  "iss": "sudomimus.com",
  "aud": "<applicationAnchor>",
  "sub": "<refreshTokenIdentifier>",
  "iat": <epoch>,
  "exp": <epoch>
}
```

```json
// JWT body
{
  "subject": "<sector subject>",
  "firstName": "<string>",
  "lastName": "<string, optional>",
  "emailAddress": "<string, optional>"
}
```

| Claim | Meaning |
|---|---|
| `kty` | Always `"Access"`. Reject any token where this differs. |
| `subject` | 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. The raw account UUID is never put in a token. |
| `firstName`, `lastName` | The user's display name. `lastName` is optional. |
| `emailAddress` | Verified email associated with this login. Selection rule below. Omitted when the account owns no verified email (e.g. Steam-only or AccessKey-only accounts). |
| `iss` | Always `"sudomimus.com"`. |
| `aud` | The `applicationAnchor` of the application this token was issued for. |
| `sub` | The identifier of the **refresh token** this access token was minted from. Useful for revocation correlation; not a user identifier. |
| `iat`, `exp` | Standard JWT issued-at / expiration, in seconds since epoch. |

### Refresh token claims

The same header/body split applies — standard claims in the header, the sector subject in the body.

```json
// JWT header
{
  "alg": "RS256",
  "kty": "Refresh",
  "iss": "sudomimus.com",
  "aud": "<applicationAnchor>",
  "iat": <epoch>,
  "exp": <epoch>
}
```

```json
// JWT body
{
  "subject": "<sector subject>"
}
```

The `subject` here is the same sector subject carried on the access token. It is **informational only** — Sudomimus identifies a refresh token by its internal handle, not by reading this body — so applications should not key revocation or storage off it.

The refresh token body deliberately omits display name and email — those are looked up fresh from the account row each time a new access token is minted, so the values in your access tokens stay current as the user updates their profile.

### How `emailAddress` is selected

The `emailAddress` claim on access tokens is chosen at issuance time by a single deterministic rule:

1. **If the user logged in with email-OTP**, the claim is exactly the email they typed (and proved they own by entering the correct verification code).
2. **Otherwise** — passkey, Steam ticket, AccessKey, or any other login method — the claim is the account's **primary email** from the `EmailIdentity` table.
3. **If the account owns no verified email at all** (e.g. a Steam-only account with no linked address), the claim is omitted entirely. Your verification code should treat `emailAddress` as optional.

The "primary email" defaults to the first verified email the account acquired, and the user can change it from Sign-in methods in the With portal. Once an access token has been minted, refreshing it (via `POST /refresh`) preserves the original `emailAddress` value, so applications can safely cache it across refreshes within a single session.

## 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": "<sector subject>",
  "aud": "<client_id>",
  "exp": <epoch>,
  "iat": <epoch>,
  "nonce": "<from /authorize, if provided>",
  "auth_time": <epoch, if applicable>,
  "email": "<if 'email' scope granted>",
  "name": "<if 'profile' scope granted>"
}
```

| Claim | Meaning |
|---|---|
| `iss` | Always `"https://oidc.sudomimus.com"`. |
| `sub` | The **sector subject** — the same per-(account × sector) value carried as `subject` on the paired access token, **not** the raw account UUID. 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. |
| `email` | The user's email — same value as on the paired access token. Only present when the `email` scope was granted. |
| `name` | The user's full name, constructed as `firstName lastName`. Only present when the `profile` scope was granted. |

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

OIDC `/token` also returns an `access_token` whose shape is **identical** to the Connect access token described above — same `kty`, same per-application signing key, same claims. The difference is only in the issuance path; once issued, your application verifies it exactly the same way (`POST /info` → cached public key).

See [OIDC relying parties](/en-us/oidc/flow/) for the full RP flow, including `/userinfo` and `/end-session`.

## When to use which

- **Connect protocol** (access / refresh tokens via `POST /info`): 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

<LinkCard
    title="OIDC relying parties"
    description="The full RP integration — discovery, /authorize, /token, /userinfo, /end-session, scopes."
    href="/en-us/oidc/flow/"
/>

<LinkCard
    title="Managing sessions"
    description="The session lifecycle after the initial login — /refresh, /introspect, /logout, /revoke-all."
    href="/en-us/guides/managing-sessions/"
/>

<LinkCard
    title="SDK reference"
    description="Official SDKs that do `verifyAccessToken` (signature, kty, exp) for you."
    href="/en-us/reference/sdks/"
/>