Implementing Token Exchange
On this page
When an OAuth client calls an API there is often a chain of requests to multiple services. Sharing the client's access token to all upstream services may disclose too many privileges. To customize tokens during end-to-end API flows you can use OAuth 2.0 token exchange from the RFC8693 standard. The article on the OAuth Token Exchange Flow explains the protocol and its use cases. This tutorial explains how to use the the Curity Identity Server to get up and running with token exchange and take control over the scopes and claims in exchanged tokens.
Register a Token Exchange Client
To use token exchange you need a registered OAuth client. Use the Admin UI to navigate to Profiles → Token Service → Clients and create a client if required. Assign the following properties:
- The token exchange capability
- A client credential
- The scopes that the client needs
In particular, a token exchange client must have access to scopes that it will send in access tokens to upstream APIs. The following example registration is for an Orders service that uses token exchange before calling an Invoicing service with the exchanged access token:
Clients can have a custom property that you can reference later in the authorization server's token exchange logic. Doing so enables different token exchange logic for distinct sets of OAuth clients. Assign custom properties under the client's Application tab:
Implement a Token Procedure
Token exchange use cases can require flexibility and the ability to apply custom logic at runtime. Implement token exchange logic in the Curity Identity Server using a JavaScript Token Procedure. It is also possible to implement advanced procedures with a Token Procedure Plugin in Java or Kotlin.
In the Admin UI, navigate to Profiles → Token Service → Endpoints → Token Endpoint → OAuth Token Exchange to create a JavaScript token procedure. From the dropdown select New procedure and give it a name. The following code shows a simple token procedure that issues requested scopes that the token exchange client has access to.
function result(context) {var tokenData = context.getPresentedSubjectToken();var presentedDelegation = context.getPresentedSubjectTokenDelegation();var requestedScope = context.request.getFormParameter('scope');if (!requestedScope) {throw exceptionFactory.badRequestException('invalid_request', 'Missing scope in a token exchange request');}var scopes = requestedScope.split(' ');var fullContext = context.getInitializedContext(context.subjectAttributes(),context.contextAttributes(),tokenData.get('aud'),scopes);var issuedAccessToken = fullContext.getDefaultAccessTokenJwtIssuer().issue(tokenData, presentedDelegation);return {scope: tokenData.scope,claims: tokenData.claims,access_token: issuedAccessToken,token_type: 'bearer',expires_in: secondsUntil(tokenData.exp),issued_token_type: 'urn:ietf:params:oauth:token-type:access_token',};}
A token exchange procedure receives an uninitialized context that you need to initialize to load all the attributes for the current token exchange procedure. To get the full context, call context.getInitializedContext
. Supply any audience or scope values for the new access token. Then use a token issuer from the full context to issue the token.
Send a Token Exchange Request
To make a token exchange request and get a new access token, the OAuth client sends an HTTP request that is an OAuth Token Exchange Message. The following example token exchange request from a hypothetical Orders service reduces the scope of its received access token before it calls an upstream Invoices service.
curl -X POST https://login.example.com/oauth/v2/oauth-token \-H 'Content-Type: application/x-www-form-urlencoded' \-d 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \-d 'client_id=orders-api-client' \-d 'client_secret=myS3cret' \-d 'subject_token=eyJraWQiOiI4ODE...' \-d 'subject_token_type=urn:ietf:params:oauth:token-type:access_token' \-d 'scope=invoicing'
Advanced Logic
The Curity Identity Server also supports more sophisticated solutions such as specifying a new audience in the exchanged token or including custom parameters in the token exchange logic. In the following example, the Orders API is only given a trigger_invoicing
scope so only has partial invoicing privileges. In addition, a custom transaction_id
value is sent to issue as a claim in the exchanged access token:
curl -X POST https://login.example.com/oauth/v2/oauth-token \-H 'Content-Type: application/x-www-form-urlencoded' \-d 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \-d 'client_id=orders-api-client' \-d 'client_secret=myS3cret' \-d 'subject_token=eyJraWQiOiI4ODE...' \-d 'subject_token_type=urn:ietf:params:oauth:token-type:access_token' \-d 'scope=trigger_invoicing'-d 'transaction_id=1234'
The following token procedure applies custom logic only for particular clients assigned the custom_token_exchange
property. The custom logic binds a secure value (the transaction ID) to the new access token and changes the token lifetime to 24 hours in the future. This might provide the invoicing API with context that it needs and, if the invoicing operation runs asynchronously, ensures that the new access token has a long enough lifetime.
function result(context) {var tokenData = context.getPresentedSubjectToken();var presentedDelegation = context.getPresentedSubjectTokenDelegation();var scopes = tokenData.get('scope').split(' ');if (context.client.properties['custom_token_exchange'] == 'true') {var requestedScope = context.request.getFormParameter('scope');if (!requestedScope) {throw exceptionFactory.badRequestException('invalid_request', 'Missing scope in a token exchange request');}scopes = requestedScope.split(' ');var transactionId = context.request.getFormParameter('transaction_id');if (!transactionId) {throw exceptionFactory.badRequestException('invalid_request', 'Missing transaction_id in a token exchange request');}var date = new Date();date.setDate(date.getDate() + 1);tokenData.exp = Math.round(date.getTime());tokenData.transaction_id = transactionId;}var fullContext = context.getInitializedContext(context.subjectAttributes(),context.contextAttributes(),tokenData.get('aud'),scopes);var issuedAccessToken = fullContext.getDefaultAccessTokenJwtIssuer().issue(tokenData, presentedDelegation);return {scope: tokenData.scope,claims: tokenData.claims,access_token: issuedAccessToken,token_type: 'bearer',expires_in: secondsUntil(tokenData.exp),issued_token_type: 'urn:ietf:params:oauth:token-type:access_token',};}
Although you can customize tokens in interesting ways, always take care to think through security to avoid introducing new attack vectors. When user consent is active, ensure that APIs never operate on user resources outside of the user's consent.
Token Exchange in Microservices
In microservice architectures, token exchange enables APIs to call each other frequently while providing close control over API authorization. Most commonly, use downscoping to reduce the privileges of the original access token. It is also possible to use upscoping so that the original client does not need access to all scopes in the end to end API flow. To use upscoping a token exchange client only needs sufficient access to trigger a call to the next API in the chain.
Microservices can maintain a cache of exchanged tokens to keep the number of requests to the authorization server manageable. For example, a cache could contain a SHA256 hash of the incoming access token mapped to the exchanged access token. Future requests to the source API with the same access token could then execute without the overhead of a token exchange request.
You can even use token exchange for asynchronous communication between microservices that use a message broker. The Securing API Events using JWTs code example provides some example token exchange code, to secure a connection between two Node.js APIs that communicate using Apache Kafka.
Conclusion
Token exchange allows upstream APIs to receive a different access token to the incoming one from the client. Typically the exchanged token has lower privileges, though it is possible to either downscope or upscope tokens. In the Curity Identity Server you use token procedures to implement custom logic that controls the scopes and claims of exchanged tokens. Token exchange logic is restricted to OAuth clients and centralized to provide visibility to security teams. You can read more about token exchange in the Token Service Admin Guide.
Join our Newsletter
Get the latest on identity management, API Security and authentication straight to your inbox.
Start Free Trial
Try the Curity Identity Server for Free. Get up and running in 10 minutes.
Start Free Trial