Identity chaining (ID-JAG)
When a user has already authenticated at an identity provider your organization trusts, that provider can assert them to us directly and your service can redeem the assertion for an access token. No redirect, no consent screen, no second sign-in — the authentication already happened upstream.
This is the Identity Assertion JWT Authorization Grant,
redeemed under the RFC 7523 jwt-bearer grant.
When this applies
Section titled “When this applies”Use it when the user is already authenticated somewhere else and you need a token here without interrupting them. If the user is present in a browser, use authorization code + PKCE instead — it is the stronger flow, because the user sees and approves what is happening.
| Identity chaining | Authorization code | |
|---|---|---|
| User present | No | Yes |
| Consent screen | No | Yes |
| Who vouches | The trusted IdP | The user, directly |
| Creates users | Never | On first sign-in, per policy |
Trust the provider first
Section titled “Trust the provider first”Nothing is accepted from an issuer the organization has not named. Requires sso:write:
POST /v1/organizations/org_01J…/trusted-issuersAuthorization: Bearer sk_live_…
{ "issuer": "https://idp.acme.com", "name": "Acme corporate IdP", "jwks_uri": "https://idp.acme.com/.well-known/jwks.json", "allowed_domains": ["acme.com"], "max_auth_age": 3600}{ "id": "ti_8f3b2c…", "issuer": "https://idp.acme.com", "name": "Acme corporate IdP", "jwks_uri": "https://idp.acme.com/.well-known/jwks.json", "status": "active", "allowed_domains": ["acme.com"], "allowed_scopes": [], "max_auth_age": 3600}issueris matched against the assertion’sissexactly, and must be https.allowed_domainsbounds which email domains this provider may assert. Empty means any domain — the subject still has to be an existing user in the org.allowed_scopescaps what is reachable through this provider. Empty means the client’s own scopes are the only limit.max_auth_agerejects an assertion whoseauth_timeis older than that many seconds. Omit it and freshness is bounded only by the assertion’s ownexp.
GET the same path to list, DELETE /v1/organizations/:orgId/trusted-issuers/:id to remove one.
Setting status to inactive stops verification without losing the configuration.
This is security-critical configuration. A compromised issuer here compromises every user it is allowed to assert. It is deliberately separate from SSO connections: an SSO connection lets an IdP authenticate a human in a visible browser redirect, while a row here lets it mint tokens silently. An org may reasonably want the first without the second.
Redeem an assertion
Section titled “Redeem an assertion”POST /tokenAuthorization: Basic <base64(client_id:client_secret)>Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=<the ID-JAG>&resource=https://tools.acme.com/mcp{ "access_token": "eyJ…", "token_type": "Bearer", "expires_in": 3600, "scope": "openid profile"}The token’s sub is the local user id, not the IdP’s — the assertion names a person, and what
comes back is that person as this platform knows them.
Accepted on a tenant issuer only, e.g. https://acme.oauth.work/token. The trust
list and the user both belong to an organization, and the apex has neither.
What the assertion must carry
Section titled “What the assertion must carry”The IdP mints it by token exchange, requesting urn:ietf:params:oauth:token-type:id-jag.
| Claim | Requirement |
|---|---|
typ header | oauth-id-jag+jwt. An ID token or access token presented here is refused on this alone. |
iss | Exactly the trusted issuer. |
aud | This tenant’s issuer. |
client_id | Your client id here. An assertion minted for another client is not redeemable by you. |
email | A user who already exists in this org, in an allowed domain. |
jti | Fresh — assertions are single-use. |
exp, iat | Required. 60s of clock skew is tolerated. |
auth_time | Required when the provider has a max_auth_age; absent then, it fails. |
Optional scope and resource claims narrow the grant further.
What it cannot do
Section titled “What it cannot do”Three limits are worth stating plainly, because each is a thing operators expect and do not get:
- It never creates users. An assertion naming someone the org does not have is refused. JIT provisioning would let one compromised provider populate a tenant with users of its choosing; use SCIM or the management API to create people deliberately.
- It never reaches a deprovisioned user. SCIM flipping
scim_activeis how an org offboards someone, and a federated assertion does not route around it. - It never widens authority. The granted scope is the intersection of what you request, what the assertion carries, what your client is allowed, and what the org permits that provider.
Confidential clients only. A public client authenticates by presenting its client id, which is not a
secret — and with a public client the client_id binding in the assertion proves nothing, so a
leaked assertion would be redeemable by anyone who read it.
Errors
Section titled “Errors”Everything wrong with an assertion is invalid_grant, and the description does not say which check
failed. That is deliberate: distinguishable errors would let a caller probe which issuers a tenant
trusts and which people it employs.
{ "error": "invalid_grant", "error_description": "assertion does not name a known user" }invalid_client (401) means client authentication failed, or the client is public.
invalid_scope means nothing survived the intersection above.
Every redemption writes a token_issued audit event carrying the asserting issuer and
the assertion’s subject, so a chained token can be traced back to the provider that vouched for it.
Discovery
Section titled “Discovery”{ "grant_types_supported": ["…", "urn:ietf:params:oauth:grant-type:jwt-bearer"], "authorization_grant_profiles_supported": ["urn:ietf:params:oauth:grant-profile:id-jag"]}The profile is advertised separately from the grant type on purpose: RFC 7523 assertions in general
are not accepted here, only assertions carrying typ: oauth-id-jag+jwt from a trusted issuer.