Skip to main content

Issue Identity Assertions (ID-JAG)

Configure your ThunderID application or agent so it can exchange a signed-in user's ID token for an Identity Assertion Authorization Grant (ID-JAG), a signed JWT that an external authorization server accepts as proof of the user's identity when issuing its own access token. This guide walks through the full flow: enabling ID-JAG issuance on the client, obtaining an ID token, exchanging it, and presenting the resulting assertion to an external server.

When to Use This​

Use this when your application's or agent's users need to access an external, ID-JAG-aware service, such as an MCP server or a partner API, without signing in there a second time. The external authorization server trusts the assertion your application or agent obtained from ThunderID and issues its own access token based on it.

Prerequisites​

  • A running ThunderID deployment. See Get Started.
  • Console access.
  • ID-JAG issuance allowed on the deployment. It is on by default. If oauth.id_jag.issuance.enabled is false, no client can request an ID-JAG. The token exchange grant, urn:ietf:params:oauth:grant-type:token-exchange, must also be in oauth.allowed_grant_types. See the server configuration.
  • A client that signs the user in and uses a confidential client type. Public clients cannot issue identity assertions. Use one of the following:
    • An application that supports the authorization code grant. An application registered from a Client ID Metadata Document can issue them only when its document declares the private_key_jwt authentication method. For such a client, token exchange serves only identity assertion requests.
    • An agent with Delegated mode on and a grant that signs the user in: authorization code or CIBA. The subject token for the exchange is the user's ID token issued to this agent, so an agent that holds only token exchange or JWT bearer cannot obtain one, because neither grant yields an ID token.

How Issuance Works​

An application or agent that has signed a user in to ThunderID exchanges the user's ID token for an ID-JAG, then presents that ID-JAG to an external authorization server to obtain an access token for an external API.

Enable Identity Assertions​

If the deployment's discovery document does not list the token exchange grant, the Console shows a hint and does not let you turn on the Identity assertions (ID-JAG) toggle. You can still turn off one that is already on. The Console cannot detect oauth.id_jag.issuance.enabled: false, so it still lets you turn the toggle on in that case, and identity assertion requests then fail with invalid_target.

  1. In the Console, navigate to Applications.
  2. Select the application.
  3. Open the Advanced tab.
  4. Find the Identity assertions (ID-JAG) card and turn on the toggle.
  5. Under Allowed audiences, add the identifiers of the external authorization servers this application can request assertions for. Each assertion targets exactly one of these audiences, and the entries must exactly match the audience value your application sends in the exchange request.
  6. Set Assertion validity in seconds. The default is 300.
note

The toggle is disabled for public clients. Turn off Public client on the application first.

note

Turning on identity assertions also enables the token exchange grant type on the client. When you configure the client through the API, a declarative resource, or an import instead, include the urn:ietf:params:oauth:grant-type:token-exchange grant type and at least one allowed audience, with no blank values. ThunderID rejects an enabled configuration that lacks either with 400 Bad Request.

Get an ID Token​

Run the authorization code flow with the openid scope to obtain an ID token for the signed-in user. An agent runs the same flow with its own client ID and client secret, through its authentication flow. See Agent Access on Behalf of Users for the agent sign-in call. An agent that holds the CIBA grant can instead request the ID token through Backchannel Authentication (CIBA) with the openid scope.

The authorization code flow uses PKCE. ThunderID requires it from a client that has PKCE required on (Require PKCE for an agent), and rejects the authorization request with invalid_request without it. The Console turns it on for agents, MCP clients that act on behalf of a user, and most application templates. Clients registered from a Client ID Metadata Document always have it on. ThunderID also verifies PKCE from a client that does not require it, so any client can send it.

Export the client_id, client_secret, and registered redirect URI from your application's or agent's Console entry, since the following steps need them:

bash
export CLIENT_ID=<your client id>
export CLIENT_SECRET=<your client secret>
export REDIRECT_URI=<your registered redirect uri>
note

These examples authenticate the client with a client secret. A client that uses private_key_jwt, such as one registered from a Client ID Metadata Document, sends client_id, client_assertion_type, and client_assertion in each token request instead of -u. See Client Authentication Methods.

  1. Create a PKCE code verifier and its S256 code challenge:
bash
export CODE_VERIFIER=$(openssl rand -hex 32)
export CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')
echo "$CODE_CHALLENGE"
  1. Send the user's browser to the authorize endpoint:
text
https://thunderid.example.com/oauth2/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=openid&state=xyz&code_challenge=$CODE_CHALLENGE&code_challenge_method=S256

Replace $CLIENT_ID, $REDIRECT_URI, and $CODE_CHALLENGE with their real values before pasting this URL into a browser; shell variables do not expand there. For the assertion to carry identity claims such as email, also request the scopes that release them, for example scope=openid%20email. See Identity Claims.

  1. After the user signs in (and consents, if consent is enabled for the application), ThunderID redirects the browser to $REDIRECT_URI with a code query parameter. Capture it from the callback URL and export it:
bash
export CODE=<code from the callback URL>
  1. Exchange the code for an ID token, sending the verifier that produced the code challenge:
bash
curl -X POST https://thunderid.example.com/oauth2/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=authorization_code' \
-d "code=$CODE" \
-d "redirect_uri=$REDIRECT_URI" \
-d "code_verifier=$CODE_VERIFIER"
  1. Export the ID token from the response:
bash
export ID_TOKEN=<id_token from the response>

Exchange It for an ID-JAG​

Send the ID token to the token endpoint with the token exchange grant type, requesting an ID-JAG for the external authorization server's audience:

bash
curl -X POST https://thunderid.example.com/oauth2/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
-d requested_token_type=urn:ietf:params:oauth:token-type:id-jag \
-d subject_token="$ID_TOKEN" \
-d subject_token_type=urn:ietf:params:oauth:token-type:id_token \
-d audience=https://external-as.example.com

A successful response returns the ID-JAG:

json
{
"access_token": "eyJhbGciOi...",
"token_type": "N_A",
"expires_in": 300,
"issued_token_type": "urn:ietf:params:oauth:token-type:id-jag"
}
note

token_type is N_A per RFC 8693: the returned token is a grant to present to the external authorization server, not a Bearer token to call an API with directly.

Export the access_token value as the ID-JAG for the next steps:

bash
export ID_JAG=<access_token from the response>

Inspect the Assertion​

The access_token field is a JWT. Decode its payload to see the claims:

bash
python3 -c 'import base64,sys;s=sys.argv[1].split(".")[1];print(base64.urlsafe_b64decode(s+"="*(-len(s)%4)).decode())' "$ID_JAG"

The header sets typ to oauth-id-jag+jwt:

json
{
"alg": "RS256",
"kid": "isP0lckl...",
"typ": "oauth-id-jag+jwt"
}

The payload carries the assertion's claims:

json
{
"aud": "https://external-as.example.com",
"client_id": "my_client_id",
"exp": 1780000300,
"iat": 1780000000,
"iss": "https://thunderid.example.com",
"jti": "f47ac10b-...",
"nbf": 1780000000,
"sub": "a1b2c3d4-..."
}
  • iss: the ThunderID issuer that signed the assertion.
  • sub: the local user id of the signed-in user.
  • aud: the single audience requested in the exchange.
  • client_id: the application or agent that requested the assertion.
  • exp, iat, nbf, jti: standard timing and uniqueness claims.

The assertion also carries a scope claim when the exchange request included a scope parameter, and a resource claim when it included one or more resource parameters, as a string for a single value or an array for several. It also carries the user's identity claims from the ID token. See Identity Claims.

Present It to the External Server​

Send the ID-JAG to the external authorization server's token endpoint using the jwt-bearer grant. The external server validates the assertion and issues its own access token, governed entirely by that server's own configuration and policy:

bash
curl -X POST https://external-as.example.com/token \
-d grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer \
-d assertion="$ID_JAG"

Identity Claims​

An external authorization server often needs more than sub to find the user's account, for example the user's email address. ThunderID copies identity claims from the ID token you exchange into the assertion: each claim on the client's ID token attribute list. To choose the claims, select them as ID token attributes:

  1. In the Console, navigate to Applications.
  2. Select the application.
  3. Open the Token tab.
  4. Under Issued to, select User.
  5. Open the ID token & user info tab, then the ID token tab.
  6. Expand Allowed attributes and select the attributes the external server needs, for example email.
  7. Click Save.

While identity assertions are on, the ID token tab shows a note that these attributes also go into the assertions. Through the API, set them in token.idToken.userAttributes of the client's OAuth configuration.

  • A claim appears only when the ID token carries it. The ID token carries an attribute that has a value on the user's profile, when a scope granted at sign-in releases it or the sign-in request asks for it with the claims parameter. The client's scope-to-claim mapping decides which claims each scope releases, and by default the email scope releases email. See Claims & Scopes. The scope parameter of the exchange request does not add identity claims.
  • roles and groups are never copied. They name the user's roles and groups in ThunderID, not at the external service.
  • A claim the assertion defines itself is never copied: iss, sub, sub_id, aud, client_id, jti, exp, iat, nbf, resource, scope, authorization_details, act, tenant, aud_tenant, aud_sub, cnf, auth_time, acr, and amr. Neither are nonce and sid, which ThunderID adds to ID tokens itself.
  • Every allowed audience receives the same claims. To send fewer, select fewer attributes for the ID token.
  • Upgrading from a version that sent only sub for the user changes existing clients without any configuration change: their assertions start to carry these claims. Before you upgrade, review each client's ID token attributes and allowed audiences.
  • The values are the ones in the ID token. An attribute you add to the list, or a change to the user's profile, reaches the assertion only through a newly issued ID token, so sign the user in again after such a change. An attribute you remove stops appearing in assertions at once, because each exchange applies the current list.

For example, with email among the ID token attributes, the email scope granted at sign-in and releasing email, and an email address on the user's profile, the payload also includes:

json
{
"email": "alex@example.com"
}

Request Parameters​

ParameterRequiredValue
grant_typeYesurn:ietf:params:oauth:grant-type:token-exchange
requested_token_typeYesurn:ietf:params:oauth:token-type:id-jag
subject_tokenYesAn ID token issued by ThunderID to the requesting application or agent. Access tokens, refresh tokens, external ID tokens, and ID-JAGs are rejected.
subject_token_typeYesurn:ietf:params:oauth:token-type:id_token
audienceYes, exactly oneIdentifier of the external authorization server. Must exactly match an entry in the client's allowed audiences (string equality, no wildcards or URL normalization).
resourceNo, repeatableAbsolute URI without a fragment. Embedded verbatim in the grant's resource claim.
scopeNoSpace-delimited. Embedded verbatim in the grant's scope claim. Not checked against the client's or subject token's scopes; the external server enforces its own policy. Does not add identity claims.

Response​

FieldValue
access_tokenThe ID-JAG JWT
token_typeN_A (RFC 8693: the value is not a usable access token type; the ID-JAG must not be sent as a Bearer token)
expires_inThe configured assertion validity, default 300 seconds
scopeThe scope embedded in the grant
issued_token_typeurn:ietf:params:oauth:token-type:id-jag

No refresh token is issued.

The Issued JWT​

The header sets typ to oauth-id-jag+jwt.

ClaimValue
issThunderID issuer
subLocal user id
audThe single requested audience
client_idThe requesting client
exp / iat / nbf / jtiStandard timing and uniqueness claims
scopeOptional, present when a scope was requested
resourceOptional, present when a resource was requested
Identity claimsOptional, the ID token's allowed attributes that the ID token carries. See Identity Claims.

Troubleshooting​

ErrorDescription textCause
invalid_requestMissing required parameter: subject_token (or subject_token_type)Parameter absent
invalid_requestID-JAG requests require subject_token_type urn:ietf:params:oauth:token-type:id_tokenWrong subject token type
invalid_requestsubject_token must be an ID token issued to this clientSubject token is not a valid self-issued ID token
invalid_requestsubject_token audience does not match the authenticated clientID token was issued to a different client
invalid_targetThe client is not permitted to request ID-JAGsID-JAG issuance turned off for the deployment (oauth.id_jag.issuance.enabled is false), ID-JAG disabled on the client, or no allowed audiences
invalid_targetThe audience parameter is required for ID-JAG requests / Exactly one audience is required for ID-JAG requests / The requested audience is not permitted for this clientAudience missing, repeated, or not in the allow-list
invalid_targetInvalid resource parameter: must be an absolute URI (or must not contain a fragment component)Malformed resource
invalid_clientID-JAG requests require a confidential clientPublic client
unauthorized_clientThe client is not authorized to use this grant typeToken exchange grant not enabled on the client
unsupported_grant_typeUnsupported grant typeThe token exchange grant is not in the deployment's oauth.allowed_grant_types
server_errorFailed to process token requestInternal failure, including unavailable revocation status

Explore with AI

ThunderID LogoThunderID Logo

Product

DocsAPIsSDKs
© Copyright Linux Foundation Europe.For web site terms of use, trademark policy and other project policies please see https://linuxfoundation.eu/en/policies.