Home

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.

What you receive

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

  1. Expose discovery metadata at /.well-known/oauth-authorization-server (RFC 8414) or /.well-known/openid-configuration, including token_endpoint and jwks_uri

  2. Support the JWT Bearer grant: accept grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer

  3. Validate the ID-JAG in the order below. The order is load-bearing, not stylistic

  4. Resolve the SAML subject from sub_id, cross-checked against the registered connection

  5. Issue an access token and nothing else, with no refresh token

  6. Advertise the grant profile: "authorization_grant_profiles_supported": ["urn:ietf:params:oauth:grant-profile:id-jag"]


Validation order

Plain Text
Step 2 must precede step 3

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.

JavaScript

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:

  1. Key the cache on (iss, jti), so two registered IdPs can never collide in it whatever their jti scheme.
  2. 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.
  3. A missing jti or non-numeric exp is a hard rejection, never a fallback to "accept without replay protection". Without a usable exp the cache cannot tell "inside the validity window" from "never seen", which disables the protection silently.

Token endpoint request

http
Accept both client authentication methods

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:

TesterMethod
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

JSON

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.

No refresh token

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.

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.