Skip to content

Tokens and verification

View as Markdown

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.

TokenIssued bySigned withVerified viaCarries
Access tokenConnect (/redeem, /refresh, /direct-issue/*, /token)The application’s token-signing private keyPOST /infoapplicationPublicKeysubject, firstName, lastName?, emailAddress?
Refresh tokenConnect (/redeem, /direct-issue/*, /token)The application’s token-signing private keySame as access tokensubject
OIDC ID tokenOIDC (/token)A platform-wide OIDC signing keyoidc.sudomimus.com/.well-known/jwks.jsonsub, iss, aud, exp, iat, nonce?, auth_time?, email?, name?

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

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.

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.

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.

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.

DefaultMinimumMaximum
Access token3 hours (10800s)60 seconds7 days (604800s)
Refresh token30 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.

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.

// JWT header
{
"alg": "RS256",
"kty": "Access",
"iss": "sudomimus.com",
"aud": "<applicationAnchor>",
"sub": "<refreshTokenIdentifier>",
"iat": <epoch>,
"exp": <epoch>
}
// JWT body
{
"subject": "<sector subject>",
"firstName": "<string>",
"lastName": "<string, optional>",
"emailAddress": "<string, optional>"
}
ClaimMeaning
ktyAlways "Access". Reject any token where this differs.
subjectThe 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, lastNameThe user’s display name. lastName is optional.
emailAddressVerified email associated with this login. Selection rule below. Omitted when the account owns no verified email (e.g. Steam-only or AccessKey-only accounts).
issAlways "sudomimus.com".
audThe applicationAnchor of the application this token was issued for.
subThe identifier of the refresh token this access token was minted from. Useful for revocation correlation; not a user identifier.
iat, expStandard JWT issued-at / expiration, in seconds since epoch.

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

// JWT header
{
"alg": "RS256",
"kty": "Refresh",
"iss": "sudomimus.com",
"aud": "<applicationAnchor>",
"iat": <epoch>,
"exp": <epoch>
}
// 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.

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.

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:

{
"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>"
}
ClaimMeaning
issAlways "https://oidc.sudomimus.com".
subThe 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.
audThe OIDC client_id of the relying party.
exp, iatStandard JWT lifetimes, in seconds since epoch.
nonceEchoed from the relying party’s /authorize request on initial issuance. Not echoed on refresh-token grants, per OIDC core 1.0 §12.1.
auth_timeWhen the user actually authenticated, in seconds since epoch. Preserved across refresh-token grants.
emailThe user’s email — same value as on the paired access token. Only present when the email scope was granted.
nameThe 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 /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 for the full RP flow, including /userinfo and /end-session.

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