Documentation
Documentation
REST API Requirements (SAML)
What your REST API must do to accept access tokens whose identity came from a SAML assertion.
Signature verification, iss/aud/exp checks, scope enforcement, CORS and the health endpoint are identical to the OIDC path, because the access token is an ordinary RFC 9068 JWT either way. Rather than repeat them, this page covers the one thing that differs: resolving the user. For the multi-language JWT middleware, CORS configuration and health-endpoint requirements, see REST API Setup (OIDC). All of it applies unchanged.
Checklist
- Accept
Authorization: Bearer <token>on protected endpoints - Validate the JWT signature against the Auth Server's JWKS
- Verify
iss,aud, andexp - Enforce scopes per endpoint
- Resolve the user from
sub_id, notsub← the SAML-specific step - Expose a
/healthendpoint returning HTTP 200 - Allow CORS from the playground origins, or use Proxy Mode
Steps 1–4, 6 and 7 are covered in REST API Setup (OIDC). Step 5 is below.
Token claims
The access token your server receives:
| Claim | Value |
|---|---|
iss | https://auth.resource.xaa.dev |
aud | Your resource server URL, exactly as registered (trailing slash preserved as-is) |
sub | An opaque AS-local user id. With the playground AS, {provider}:saml-user-<uuid>. Not a durable key, see below |
sub_id | The structured SAML subject: { format: "saml-nameid", issuer, nameid, … }, forwarded unchanged from the ID-JAG. This is your user key. |
email | Present only when the NameID is an email address. See email is conditional |
scope | Space-separated granted scopes |
app_org | Provider (tenant) name that authenticated the user |
exp | Expiry (Unix timestamp) |
The header uses typ: at+jwt (RFC 9068).
The AS mints sub itself, so it is collision-free, and keying on it will not merge two tenants. But it carries no enterprise identity and is not stable: the playground generates it per process, so it changes when the AS restarts and any rows keyed against it are orphaned. Resolve on sub_id; treat sub as an opaque request-scoped handle.
Resolving the user
After your existing middleware has validated the token, map sub_id to a local user on the composite identity (issuer, nameid, sp_name_qualifier when present).
Four details that are easy to get wrong:
- Compare the qualifier with NULL-safe equality, not
=. A plain=never matches aNULLqualifier, so every request from an IdP that omits it provisions a fresh duplicate. UseIS NOT DISTINCT FROM(PostgreSQL, SQLite),<=>(MySQL), or branch on null explicitly. - Pick one representation for "no qualifier", either
NULLor a sentinel, and use it on both write and read. Two representations coexisting is the same duplicate-user bug by another route. A unique index over a nullable column also does not prevent duplicates on most databases (details). - Make JIT provisioning idempotent. Two concurrent first requests both miss the lookup; without an upsert, one fails or you get two rows.
- Reject a malformed
sub_idrather than falling through to thesubbranch.
email is conditional
email is set only when the NameID is an email address, meaning nameid_format is absent or names an email format. A persistent or transient NameID arrives with no email claim, which is why every sample above falls back to nameid. Never make it a required column, and never key on it.
Next step
Ready to test? See Testing Guide to run the five-step SAML flow against your server, or Own Auth Server if you issue the access tokens yourself.
On this page