Skip to main content

Token Exchange with External Identity Providers

ThunderID can exchange a token issued by an external identity provider (IdP) for a ThunderID-issued access token. This lets a client that authenticates users via an external OIDC provider, such as Asgardeo, Google, Azure AD, or Keycloak, obtain a ThunderID-scoped access token without requiring users to sign in again.

This uses RFC 8693 Token Exchange. For general information about the token exchange grant type, see Token Exchange.

When to Use​

Use this when:

  • A client authenticates users via an external OIDC provider and needs a ThunderID-scoped access token to call ThunderID-protected APIs.
  • You want to centralize token issuance through ThunderID while delegating user authentication to an upstream IdP.

This is different from the trusted issuer feature. Trusted issuer lets ThunderID accept and validate tokens from a central authorization server directly, without issuing new tokens. Token exchange accepts an external token and issues a new ThunderID token in return.

Prerequisites​

  • ThunderID is running. See Get Started.
  • An external OIDC identity provider that issues JWTs with an iss claim and exposes a JWKS endpoint.
  • An access token with the system scope for the setup API calls.

Step 1: Register the External Identity Provider​

Register the external IdP in ThunderID as an OIDC connection and enable it for token exchange. An OIDC connection is a single typed resource, so the redirect-flow fields (clientId, clientSecret, redirectUri, authorizationEndpoint, tokenEndpoint) are required even when the IdP is used for token exchange only. Three additional fields enable token exchange:

FieldRequiredDescription
issuerYesThe exact iss claim value in tokens from this IdP. ThunderID matches incoming tokens against this value to identify which IdP issued them.
jwksEndpointYesThe IdP's JWKS URL. ThunderID fetches public signing keys from this URL to verify token signatures.
tokenExchangeEnabledYesSet to true to allow this IdP's tokens to be exchanged.
bash
curl -X POST https://thunderid.example.com/connections/oidc \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Asgardeo",
"clientId": "my-external-client-id",
"clientSecret": "my-external-client-secret",
"redirectUri": "https://thunderid.example.com/gate/callback",
"authorizationEndpoint": "https://api.asgardeo.io/t/myorg/oauth2/authorize",
"tokenEndpoint": "https://api.asgardeo.io/t/myorg/oauth2/token",
"issuer": "https://api.asgardeo.io/t/myorg/oauth2/token",
"jwksEndpoint": "https://api.asgardeo.io/t/myorg/oauth2/jwks",
"tokenExchangeEnabled": true
}'

Step 2: Create an OAuth Application with the Token Exchange Grant​

Create an OAuth application and include urn:ietf:params:oauth:grant-type:token-exchange in its allowed grant types. This is the client that will perform exchanges on behalf of users.

bash
curl -X POST https://thunderid.example.com/applications \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Token Exchange Client",
"inboundAuthConfig": [{
"type": "oauth2",
"config": {
"clientId": "te_client",
"clientSecret": "te_secret",
"grantTypes": ["urn:ietf:params:oauth:grant-type:token-exchange"],
"tokenEndpointAuthMethod": "client_secret_basic"
}
}]
}'

Step 3: Exchange the External Token​

Send the external IdP token to ThunderID's token endpoint using the token exchange grant type:

bash
curl -X POST https://thunderid.example.com/oauth2/token \
-u 'te_client:te_secret' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
-d 'subject_token=<JWT_FROM_EXTERNAL_IDP>' \
-d 'subject_token_type=urn:ietf:params:oauth:token-type:jwt' \
-d 'resource=https://api.example.com/app'

A successful response returns a ThunderID-issued access token:

json
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token"
}

The issued token's sub claim contains the external user's subject identifier from the original token. For permission-bearing requests, its aud claim is the explicit resource server identifier or the configured defaultResourceServer. An OIDC-only or scopeless request without resource uses the application's default audience (token.accessToken.defaultAudience), or the client_id when it is unset.

How ThunderID Validates the External Token​

When processing an external token for exchange, ThunderID:

  1. Extracts the iss claim and matches it against registered IdPs with tokenExchangeEnabled=true.
  2. Fetches the matching IdP's JWKS and verifies the token signature.
  3. Checks the token's aud claim. ThunderID accepts the token when aud contains this ThunderID instance's own issuer.
  4. Validates exp (and nbf if present) with the configured clock-skew leeway.
  5. Extracts the sub claim and non-standard claims as user attributes.

If any step fails, the exchange returns HTTP 400 with error=invalid_request and an "Invalid subject_token" message.

Attribute Configuration​

Attribute configuration for token exchange uses the same attributeConfiguration structure as federated sign-in, including Authorization Mapping. See Add an OIDC Identity Provider for the full reference.

When the IdP connection has Authorization Mapping configured, it decides the granted scope instead of the external token's own scope claim:

RequestResulting scope
scope suppliedThe requested scopes, narrowed to whatever the mapping actually authorizes
scope omittedNothing. An omitted request is not treated as "grant everything the mapping allows" here — unlike the jwt-bearer grant, which does bound an omitted request by the assertion's own scope claim, token exchange has no equivalent claim on the subject token to fall back to, since an arbitrary external token's own scope claim is never authoritative here either way.

Without Authorization Mapping configured, the granted scope narrows the requested scope against the external token's own scope claim, same as any other token exchange.

Limitations​

  • Only JWT tokens are supported. Opaque tokens (for example, GitHub access tokens) cannot be exchanged.
  • The external user's sub is passed through as-is. No local user lookup or just-in-time provisioning occurs.
  • External token scopes are narrowed by the existing intersection logic, or by the connection's Authorization Mapping when one is configured (see Attribute Configuration above). External claims can be renamed to local attributes via the IdP's attributeConfiguration; the resulting attributes are still filtered by the application's userAttributes configuration.
  • Token exchange is available to all applications that have the token exchange grant type. No per-application IdP restriction applies.

Next Steps​

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.