OAuth 2.0 Token Exchange (RFC 8693) allows a trusted client to exchange a user token issued to another trusted client, and to act on behalf of that already authenticated user against a different resource. The client presents an existing user token to the Token (/oidc/token) endpoint and receives a new user access token. Mosaic preserves the user as the subject, records the exchanging client as the actor, and recomputes authorization for the target resource.
Typical integrations include:
- Agentic workflows where a backend client orchestrates calls across APIs using the same user context
- Backend-for-frontend (BFF) and API gateway patterns that retarget a user token at a downstream API
- Microservice chains where each service obtains a token for the next resource
For example, a chat app authenticates the user and receives an access token intended for an MCP server. MCP servers typically act as a proxy: the MCP server exchanges that token with Mosaic for a new token targeted at a downstream API. The user stays the subject. The MCP server is the actor; the chat app is the source client.
The token exchange flow differs from delegated access flow, where one user consents so another user can act on their behalf. Token exchange delegates between clients while the user subject stays the same.
Token exchange always involves two clients in the same Mosaic application:
- Source client: the client the presented token was issued to. It controls which other clients may exchange those tokens.
- Exchanging client: the client that authenticates to
/oidc/token, presents the existing token, and receives the new token. It must be allowed to initiate token exchange.
The user authenticates with the chat app. The chat app obtains a user access token from Mosaic (Step 2). The original access token includes:
{ "sub": "user", "client_id": "chat-app", "scope": "read:mcp" }The chat app then presents that token to the MCP server.
The MCP server authenticates to Mosaic and exchanges the token for a new user access token targeted at the API (Step 3). When Mosaic accepts the token exchange request, it:
Authenticates the exchanging client using the client’s configured authentication method.
Checks that the exchanging client is allowed to perform the exchange flow.
Validates the presented access token and resolves the source client from that token (
client_id).Checks that the source client allows delegation for this exchanging client.
Checks that the exchanging client is allowed to request the target resource.
Grants the intersection of requested scopes and scopes allowed for the exchanging client. Scopes the client is not allowed to have are dropped rather than rejected. Mosaic never grants
openidoroffline_accesson an exchanged token, even when requested (meaning Mosaic never issues an ID token or refresh token for token-exchanging clients).Recomputes authorization for the subject user and the target resource. Permissions and scopes that applied only to the previous resource are omitted.
Issues a new user access token whose
client_idis the exchanging client, and records that client in theactclaim. The exchanged access token includes:{ "sub": "user", "client_id": "mcp-server", "scope": "read:resource", "act": { "sub": "mcp-server", "act": { "sub": "chat-app" } } }
The MCP server calls the API with the exchanged token.
Delegation is evaluated at each exchange. If the chat app allows the MCP server and the MCP server allows another client, that client can exchange a token currently issued to the MCP server even if the chat app does not list it. The chat app does not need to allow that client unless it presents a token whose current client_id is the chat app.
If you don't already have a Mosaic application, create one first (see Create Mosaic applications) and add clients.
Configure both sides of the trust relationship in Applications > your app > Clients. Edit each OIDC authentication client, open Advanced > Token exchange.
| Field | Who configures it | Description |
|---|---|---|
| Initiate token exchange | Exchanging client | Allows this client to call /oidc/token with the token exchange grant. Disabled by default. |
| Delegate tokens issued by this client | Source client | Controls which clients may exchange user access tokens issued to this client. Disabled by default (none). |
When Delegate tokens issued by this client is enabled, choose who may exchange:
- All: every client in the same application, including dynamically registered clients
- Specific clients: manually registered clients you select, and optionally Client Authorities (so clients registered through those authorities can exchange)
For the complete client field list, see Configure OIDC clients. Note that any changes to token exchange settings (including enabling or disabling exchange, or changing which clients may exchange) take up to 5 minutes to propagate.
Also confirm:
- Both clients belong to the same application.
- The exchanging client includes the target resource under Resources, if you will pass
resourcein the request. See Manage resources. - The source client issues JWT access tokens signed with
RS256orPS256. Mosaic rejects opaque source tokens.
Token exchange starts from a user access token already issued to the source client. You can obtain that token from any Mosaic API that returns a user access token—for example, an OIDC authorization code flow.
After the user authenticates, the source client exchanges the authorization code for tokens:
curl -i -X POST \
https://api.transmitsecurity.io/oidc/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d client_id=CHAT_APP_CLIENT_ID \
-d client_secret=CHAT_APP_CLIENT_SECRET \
-d code=CODE \
-d grant_type=authorization_code \
-d redirect_uri=REDIRECT_URIUse the access_token from the response as subject_token in Step 3. Pass it from the source client to the exchanging client over your backend channel.
From the exchanging client's backend, send a POST request to the Token (/oidc/token) endpoint with grant_type set to urn:ietf:params:oauth:grant-type:token-exchange and the user access token from Step 2. Authenticate as the exchanging client using its configured method (client secret, private key JWT, or mTLS).
| Field | Description |
|---|---|
grant_type | Must be urn:ietf:params:oauth:grant-type:token-exchange. |
subject_token | The user access token to exchange. It must have been issued to a different client. An access token issued to the exchanging client itself, client-credentials tokens, ID tokens, and tokens with no user subject are rejected. |
subject_token_type | (Optional) Type of the presented token. Defaults to urn:ietf:params:oauth:token-type:access_token. Only access tokens are supported. |
resource | (Optional) Target resource URI. This is what selects the audience of the new token. The URI must be configured as a resource and allowed for the exchanging client. Mosaic does not use audience to select the target. |
scope | (Optional) Space-delimited scopes to request. Mosaic grants the intersection of this list and the scopes allowed for the exchanging client. openid and offline_access are never granted. |
curl -i -X POST \
https://api.transmitsecurity.io/oidc/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
-d client_id=MCP_SERVER_CLIENT_ID \
-d client_secret=MCP_SERVER_CLIENT_SECRET \
-d subject_token=USER_ACCESS_TOKEN \
-d subject_token_type=urn:ietf:params:oauth:token-type:access_token \
-d resource=https://api.example.com \
-d scope=read:resourceA successful response returns a new user access token. Mosaic does not return an ID token. If the granted scope differs from the requested scope, the response includes the scope that was actually granted.
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 3600,
}Notes:
Token exchange never returns a refresh token. If the request includes
offline_access, Mosaic drops it from the granted scope and still returns an access token.If the exchanging client is not allowed to use this grant, Mosaic returns
unauthorized_client.If the presented token is invalid, the source client does not allow this exchanging client, or the subject cannot be resolved in the exchanging client's application, Mosaic returns
invalid_grantorinvalid_request.If the exchanging client is not allowed to request the target resource, Mosaic returns
invalid_target.A client cannot exchange an access token issued to itself. Mosaic rejects that request with
invalid_grant. Cross-application exchange is rejected.Specifying
actor_token/actor_token_typein the request isn't supported. A request that includesactor_tokenis rejected withinvalid_request.A subject token that expires during the exchange is rejected with
invalid_grantand the descriptionsubject token has expired.
The exchanged token is a new authorization context, not a copy of the presented token.
| Claim | Value |
|---|---|
sub | Preserved from the presented token (the user). |
client_id | The exchanging client. |
aud | The target resource when resource is requested. |
scope | Recomputed for the exchanging client and target resource. Never includes openid or offline_access. |
act | Nested actor chain. The outermost sub is the current exchanging client. The innermost entry is always the source client—the client the original token was issued to, which never performed an exchange itself. Intermediate entries are previous exchanging clients. This innermost source-client entry is a Mosaic extension to RFC 8693. |
Example after the MCP server exchanges the chat app token:
{
"sub": "user",
"client_id": "MCP_SERVER_CLIENT_ID",
"aud": "https://api.example.com",
"scope": "read:resource",
"act": {
"sub": "MCP_SERVER_CLIENT_ID",
"act": {
"sub": "CHAT_APP_CLIENT_ID"
}
}
}This records that:
- the user remains the subject
- the MCP server is the current actor
- the chat app is the client the original token was issued to
An exchanged token is not an OIDC identity token. Mosaic does not return an ID token or a refresh token from the exchange, does not grant openid or offline_access, and does not return user profile claims from UserInfo for an exchanged token. Validate the access token as described in Validate tokens, or call Introspection when you need a server-side activity check.
The following RFC 8693 scenarios are not supported:
- Exchanging SAML assertions, ID tokens, or JWT assertions from external identity providers
- Issuing token types other than OAuth 2.0 access tokens (for example, exchanging an access token for an ID token)
- Delegating across applications or exchanging a token minted for Mosaic management APIs (Admin Portal, tenants, clients, export/import)
- Passing
actor_token(rejected withinvalid_request)
SAML clients cannot participate; they have no /oidc/token endpoint.
The act chain is limited to 10 levels, so at most 9 consecutive exchanges. A subject token at the maximum depth is rejected with invalid_request and the description subject token actor chain is at the maximum depth of 10.