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
issclaim and exposes a JWKS endpoint. - An access token with the
systemscope 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:
| Field | Required | Description |
|---|---|---|
issuer | Yes | The exact iss claim value in tokens from this IdP. ThunderID matches incoming tokens against this value to identify which IdP issued them. |
jwksEndpoint | Yes | The IdP's JWKS URL. ThunderID fetches public signing keys from this URL to verify token signatures. |
tokenExchangeEnabled | Yes | Set to true to allow this IdP's tokens to be exchanged. |
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.
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:
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:
{
"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:
- Extracts the
issclaim and matches it against registered IdPs withtokenExchangeEnabled=true. - Fetches the matching IdP's JWKS and verifies the token signature.
- Checks the token's
audclaim. ThunderID accepts the token whenaudcontains this ThunderID instance's own issuer. - Validates
exp(andnbfif present) with the configured clock-skew leeway. - Extracts the
subclaim 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:
| Request | Resulting scope |
|---|---|
scope supplied | The requested scopes, narrowed to whatever the mapping actually authorizes |
scope omitted | Nothing. 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
subis 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'suserAttributesconfiguration. - Token exchange is available to all applications that have the token exchange grant type. No per-application IdP restriction applies.
Next Steps
- Manage Identity Providers: Create, update, and delete identity providers using the API.
- Connect an IdP to an Application: Use an IdP for redirect-based sign-in flows.
- Token Exchange: Learn about the RFC 8693 token exchange grant type in ThunderID.