Home

Documentation

Documentation

Bring Your Own Resource (SAML)

Your app is the resource server in the Cross App Access flow. It receives access tokens from requesting apps, validates them against an Authorization Server's JWKS, and serves protected data, either as a REST API or an MCP server.

When the requesting app federates its users with SAML 2.0 instead of OIDC, your resource server's job is almost unchanged. You never see a SAML assertion, never parse XML, and never talk to the SAML IdP. What changes is how the token identifies the user.

You are not processing SAML

The SAML assertion is consumed upstream by the Identity Provider, which converts it into an ID-JAG. By the time anything reaches you it is an ordinary OAuth access token. The only difference is the shape of its subject claim.


Where your app fits

The SAML path has five steps rather than four. The extra one is the second token exchange at the IdP (see SAML Overview for the requesting app's side).

Steps 1–3 always happen upstream at the customer's IdP; nothing of yours is involved. Step 4 belongs to whichever Authorization Server issues the access token: the playground's, or yours if you run your own (Own Auth Server). Your resource server participates only in Step 5: validate the access token, resolve the user, return data.


Two subject claims, not one

On the SAML path the subject is carried in an extra structured claim, sub_id. Which token you are looking at matters, because sub means something different in each:

TokenWho issues itsubsub_id
ID-JAGThe customer's IdPThe bare SAML NameID✅ present
Access tokenThe Authorization ServerAn opaque, AS-local user id. With the playground AS, {provider}:saml-user-<uuid>✅ forwarded unchanged

sub is never missing, so nothing looks broken, but it is the wrong key either way. On the ID-JAG it is the raw NameID, unique only within its issuer (see below). On the access token it is opaque and AS-local: collision-free, but carrying no enterprise identity and not stable. The playground regenerates it per process, so users persisted against it are orphaned on restart.

sub_id means the same thing on both tokens, and is the only claim that identifies the actual enterprise user.

The sub_id object

The structured subject, per §3.2.1. Identical on the ID-JAG and on the access token that follows it:

JSON

Always present: format (the literal saml-nameid), issuer (the SAML IdP's entity ID), nameid.

Only if the assertion carried it: nameid_format, name_qualifier, sp_name_qualifier, sp_provided_id. The IdP omits what the assertion did not have, so never assume one of these exists, including nameid_format.


Why sub alone silently collides

A NameID is not globally unique

A SAML NameID is unique only within its issuer, and when it is SP-scoped, only within a given sp_name_qualifier as well. Nothing stops two unrelated customer organizations from each federating a user whose NameID is alex.chen@example.com.

Resolve on the NameID alone and both organizations' requests land on the same record. There is no signature failure, no invalid_grant, no 403. Just one tenant reading another tenant's data.

A single issuer can also reuse a NameID under a different sp_name_qualifier, which is why the qualifier belongs in the key whenever it is present.


The correct resolution key

Resolve the user on the composite identity, per §3.2.2:

Plain Text

Model it as a composite unique constraint rather than a concatenated string, so the database enforces the invariant for you:

SQL
`NULL` breaks unique indexes

NULLS NOT DISTINCT needs PostgreSQL 15+. Elsewhere NULL is not equal to itself, so a unique index over a nullable sp_name_qualifier will not stop duplicate rows. Either store the absent qualifier as a consistent sentinel, or add a partial unique index for the IS NULL case. Apply the same rule on read (details).

email is conditional

The IdP sets email 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. Use it when present, fall back to nameid, and never key on it.


What you'll set up

Choose how the access token gets issued, then configure your server to validate it:

StepPlayground Auth (recommended)Own Auth Server
1–3. SAML SSO + token exchangesPlayground IdPPlayground IdP
4. JWT Bearer GrantPlayground Auth Server (https://auth.resource.xaa.dev)Your Auth Server
5. API CallYour Resource ServerYour Resource Server

Your resource server validates tokens issued by the playground's Authorization Server, which resolves the SAML identity for you and forwards sub_id into the access token.

Plain Text

Best for: verifying your token validation and user mapping without running your own auth server.

Own Auth Server

The ID-JAG is addressed to your authorization server, which must validate it, cross-check the SAML identity, and issue its own access token. This is where most of the SAML-specific work lives.

See Own Auth Server for the full validation order and the security reasoning behind it.


Where to go next

Your resource server needs everything the OIDC path requires (bearer tokens, JWKS signature validation, iss/aud/exp checks, scope enforcement, a /health endpoint, CORS), plus resolving the user from sub_id:

For IdP-side configuration (creating the SAML app, assigning users, registering the agent), see Enabling Cross App Access for SAML-Based Resource Apps.

These materials and any recommendations within are not legal, privacy, security, compliance, or business advice. These materials are intended for general informational purposes only and may not reflect the most current security, privacy, and legal developments nor all relevant issues. You are responsible for obtaining legal, security, privacy, compliance, or business advice from your own lawyer or other professional advisor and should not rely on the recommendations herein.

Presented byOkta Developer

Copyright © 2026 Okta. All rights reserved.