Documentation
Documentation
Own Auth Server (SAML)
If you run your own authorization server, it redeems the ID-JAG and issues the access token. On the SAML path this is where nearly all the protocol-specific work lives. Your resource server barely notices the difference, but your auth server does.
A JWT Bearer grant carrying an ID-JAG. It has both a sub (the bare SAML NameID) and a structured sub_id. Resolve on sub_id, because the raw NameID is only unique within its issuer. You never receive the SAML assertion itself. See Overview for the sub_id shape.
Requirements
-
Expose discovery metadata at
/.well-known/oauth-authorization-server(RFC 8414) or/.well-known/openid-configuration, includingtoken_endpointandjwks_uri -
Support the JWT Bearer grant: accept
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer -
Validate the ID-JAG in the order below. The order is load-bearing, not stylistic
-
Resolve the SAML subject from
sub_id, cross-checked against the registered connection -
Issue an access token and nothing else, with no refresh token
-
Advertise the grant profile:
"authorization_grant_profiles_supported": ["urn:ietf:params:oauth:grant-profile:id-jag"]
Validation order
Verify the signature first and you have a forgery path: an attacker stands up their own IdP, signs their own ID-JAG so it validates cleanly against their own JWKS, and sets sub_id.issuer to your customer's SAML issuer. The signature checks out, and you mint an access token for someone else's tenant.
Deriving trust from a key you fetched because the token told you to is not trust. Bind iss to a connection you registered out of band, then verify against that connection's key.
Resolving the subject
Once the token is trusted: validate the sub_id shape, cross-check it against the connection you resolved from iss, then look up the user on (issuer, nameid, sp_name_qualifier when present), never on nameid alone (why). An issuer the connection doesn't know is a rejection, not a new tenant.
Every rejection above is an invalid_grant. Error Codes lists the reasons the playground's own Authorization Server returns, which are the same set.
Replay protection
jti is required on an ID-JAG. Record redeemed values until exp passes and reject repeats. Three rules that are easy to get backwards:
- Key the cache on
(iss, jti), so two registered IdPs can never collide in it whatever theirjtischeme. - Resolve the identity before recording the
jti. If resolution fails no token was issued, so recording it anyway burns an otherwise-valid ID-JAG the moment the user is provisioned or the binding is fixed, blocking a legitimate retry for no security benefit. - A missing
jtior non-numericexpis a hard rejection, never a fallback to "accept without replay protection". Without a usableexpthe cache cannot tell "inside the validity window" from "never seen", which disables the protection silently.
Token endpoint request
The two playground testers do not agree on how they present client credentials, so support both or one of them will fail against your server:
| Tester | Method |
|---|---|
| Test Your Resource App (Beta) | client_secret_basic, via Authorization: Basic base64(id:secret) |
| Test Your Resource App (guided) | client_secret_post, credentials in the form body |
Accepting either is also what RFC 6749 expects of a confidential client. Note that some OAuth frameworks reject a request that supplies credentials in the header and the body simultaneously, so read one or the other, not both.
Expected response
Forward sub_id (and email, when present) into the access token so your resource server can resolve the same user without repeating the SAML cross-check.
Issue an access token only (§4.4.3). A refresh token gives the client durable access the IdP can no longer revoke, because on expiry it would simply refresh instead of returning to the IdP. Let the ID-JAG's short lifetime force that round trip.
This is not the same as the Refresh Token in the requesting app's flow: the IdP issues that one in Step 2 and consumes it in Step 3 to mint ID-JAGs (see SAML Overview). That token belongs to the IdP. The rule here is that your auth server must not issue one of its own.
Next step
Redeem a real ID-JAG from your own IdP against your auth server and export a conformance log with the Testing Guide. Every rejection above surfaces as HTTP 400 with invalid_grant; see Error Codes.
On this page