Beta

Create a tenant

A new tenant starts with its own users, OAuth settings, audit history and logs. You are its first Tenant Admin.

BTL Admin

Following a token exchange

It is 09:02 UTC on 1 October. The printer asks the photo API for the photos in album 42, presenting the access token it received at 09:00. The photo API validates the token, confirms the album is yours, and needs the first file from the storage service.

Everything is in place for an exchange. The photo API is registered as client photo-api, and the photo service's policy lets that client exchange tokens issued for the photo API for tokens to use at the storage service. What follows is every message, in order.

Sending the request

All domains, credentials, and tokens in these examples are fictional.

POST /token HTTP/1.1
Host: auth.photos.example
Authorization: Basic cGhvdG8tYXBpOmRlbW8tb25seS1ub3QtYS1yZWFsLXNlY3JldA==
Content-Type: application/x-www-form-urlencoded

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange
&subject_token=eyJhbGciOiJFUzI1NiIsImtpZCI6InBob3Rvcy0yMDI2LTA5IiwidHlwIjoiYXQrand0In0.eyJpc3Mi...
&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token
&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token
&resource=https%3A%2F%2Fstorage.photos.example
&scope=storage.read

The body is form-encoded, and its line breaks are for display. The Basic header represents photo-api:demo-only-not-a-real-secret. HTTP Basic keeps the example short. Inside a platform, the photo API would more often authenticate with a workload identity or mutual TLS, as Service-to-service access suggested.

grant_type selects token exchange. subject_token is the printer's token, shortened here, and subject_token_type says it is an access token from this server. requested_token_type asks for an access token in return. It is optional, and included to make the request explicit.

resource names where the new token will be used, as in Selecting the intended resource. scope names the access needed there, in the storage service's own terms. The storage service defines scopes such as storage.read for reading stored files, so the request does not ask for photos.read, which means something only at the photo API.

The authorization server authenticates the photo API, validates the printer's token, and applies its exchange policy. Trust and validation sets out those checks in order.

Reading the response

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
  "access_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6InBob3Rvcy0yMDI2LTA5IiwidHlwIjoiYXQrand0In0.eyJpc3Mi...",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "Bearer",
  "expires_in": 60
}

The new token arrives in access_token, shortened here. The field keeps its familiar name so that token exchange fits the ordinary token response, but the token inside it does not have to be an access token.

Two fields describe the token, and they answer different questions. issued_token_type is required and says what was issued, using the same identifiers as the request: here, an access token. token_type is also required and says how to use the token at an API, in the same sense as in every other token response. Bearer means present it as it is.

expires_in is recommended rather than required, and gives one minute. scope is missing because the server issued exactly what was asked for. It may leave scope out only in that case, so its absence here means storage.read. There is no refresh token, which is usual when one short-lived credential is exchanged for another.

Some exchanges issue something that is not used at an API at all. Suppose the photo API asked for requested_token_type=urn:ietf:params:oauth:token-type:jwt, to present as an assertion at another authorization server, the way Lantern's portal presented one in JWT and SAML assertion grants. The response would then read:

{
  "access_token": "eyJhbGciOiJFUzI1NiIs...",
  "issued_token_type": "urn:ietf:params:oauth:token-type:jwt",
  "token_type": "N_A",
  "expires_in": 300
}

N_A says that no OAuth token type applies, because this token is not meant to be sent to an API.

Inside the new token

Decoded, the storage token reads:

Header:
{
  "alg": "ES256",
  "kid": "photos-2026-09",
  "typ": "at+jwt"
}

Claims:
{
  "iss": "https://auth.photos.example",
  "sub": "user-2048",
  "aud": "https://storage.photos.example",
  "client_id": "photo-api",
  "scope": "storage.read",
  "iat": 1790845320,
  "exp": 1790845380,
  "jti": "demo-token-id-storage-1",
  "act": {
    "sub": "photo-api"
  }
}

Compare it with the printer's token. The issuer and subject are unchanged: the photo service issued it, and it concerns user-2048. The audience is now https://storage.photos.example, matching the resource the photo API named. client_id is photo-api, because that is the client that requested this token, and act records the photo API as the party acting for you. The scope is the storage service's, and the token lives from 09:02:00 to 09:03:00 UTC.

The same key, photos-2026-09, signed it, and its typ is still at+jwt. To the storage service it is an ordinary access token from the photo service, validated in the ordinary way.

Using the new token

GET /objects/photo-1187 HTTP/1.1
Host: storage.photos.example
Authorization: Bearer eyJhbGciOiJFUzI1NiIsImtpZCI6InBob3Rvcy0yMDI2LTA5IiwidHlwIjoiYXQrand0In0.eyJpc3Mi...

The storage service validates the token, checks that storage.read covers the request, confirms that photo-1187 belongs to user-2048, and sees that the actor is the photo API, a caller it expects. It returns the file.

The printer calls the photo API with its access token, addressed to the photo API. The photo API validates the token and checks that album 42 is yours. Authenticated as client photo-api, it sends a token exchange request with the printer's token as the subject token, the storage service as the resource, and storage.read as the scope. The authorization server checks the client, the subject token and its exchange policy, then returns a storage token addressed to the storage service, naming the photo API as actor and valid for 60 seconds. The photo API presents only the storage token to the storage service, which validates it and checks scope, owner and actor before returning the photo file. The photo API then returns the album's photos to the printer. The printer calls the photo API with its access token, addressed to the photo API. The photo API validates the token and checks that album 42 is yours. Authenticated as client photo-api, it sends a token exchange request with the printer's token as the subject token, the storage service as the resource, and storage.read as the scope. The authorization server checks the client, the subject token and its exchange policy, then returns a storage token addressed to the storage service, naming the photo API as actor and valid for 60 seconds. The photo API presents only the storage token to the storage service, which validates it and checks scope, owner and actor before returning the photo file. The photo API then returns the album's photos to the printer.
The photo API exchanges the printer's token for one addressed to the storage service and uses only the new token there. The authorization server and the storage service each make their own checks.

As Service-to-service access noted, the photo API can keep the storage token for its short life and reuse it for the album's other files. The cache needs one rule above all: an exchanged token is about one person, so it must be keyed by subject as well as audience, and never used for a request on behalf of someone else.

The exchange also leaves the printer's token as it was. Exchanging a token does not use it up, unless its type is defined to be single-use, so the printer's token stays valid at the photo API until 09:10. Nor are the two tokens linked afterwards. If the printer's token were revoked at 09:02:30, the storage token would not stop working on its own unless the deployment deliberately carries revocation across. Its one-minute lifetime keeps that gap short.

Try it in the Lab

PUT IT INTO PRACTICE

Check your understanding

Try these questions before moving on. If an answer isn't right, use the feedback and try again.

0 of 2 answered correctly

Enable JavaScript to answer these questions and save progress in this browser.

QUESTION 1 OF 2An exchange response has issued_token_type urn:ietf:params:oauth:token-type:access_token and token_type Bearer. What does each field tell the photo API?

QUESTION 2 OF 2The photo API asked for scope=storage.read, and the response has no scope field. What should it conclude?

We value your privacy

We use cookies and similar technologies to enhance your browsing experience, and analytics to understand our traffic. By clicking "Allow All", you consent to optional analytics. Cookie Policy

Learn identity