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

Working through an example

The printer is assembling your photo book and asks for the photos in album 42. Most of them sit in the storage service. One, photo-0311, was taken in 2019 and you have not opened it in years, so its original has moved to the archive service at https://archive.photos.example.

Fetching it involves four services and two exchanges. Every token along the way appears below, decoded, so you can see what changes at each hop and what stays the same.

Starting at the photo API

All domains, credentials, and tokens in this example are fictional. The three tokens share one header, {"alg": "ES256", "kid": "photos-2026-09", "typ": "at+jwt"}, so only their claims are shown. At 09:02:00 UTC, the printer sends its request:

GET /albums/42/photos HTTP/1.1
Host: api.photos.example
Authorization: Bearer eyJhbGciOiJFUzI1NiIsImtpZCI6InBob3Rvcy0yMDI2LTA5IiwidHlwIjoiYXQrand0In0.eyJpc3Mi...

The token, shortened above, is the printer's access token. Call it token A. Its claims:

{
  "iss": "https://auth.photos.example",
  "sub": "user-2048",
  "aud": "https://api.photos.example",
  "client_id": "photo-printer",
  "scope": "photos.read",
  "iat": 1790845200,
  "exp": 1790845800,
  "jti": "demo-token-id-7"
}

The photo API validates token A. The audience is the photo API, photos.read covers listing photos, and album 42 belongs to user-2048. There is no act claim. The printer is an ordinary OAuth client acting with your authorization, and client_id records it.

The first exchange

The photo API exchanges token A just as it did in Following a token exchange. Authenticated as photo-api, it sends this body to the token endpoint:

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
&resource=https%3A%2F%2Fstorage.photos.example
&scope=storage.read

Token B comes back with an expires_in of 60:

{
  "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"
  }
}

Token B is addressed to the storage service, carries the storage service's scope, and names photo-api as the current actor. The photo API sends it with each storage request. For photo-0311, the storage service validates token B, confirms that the photo belongs to user-2048 and that the actor is the photo API, and finds only a note saying the original is in the archive.

The second exchange

Now the storage service stands where the photo API stood. It holds a token issued for it, token B, and needs a token for another service. It is registered as client photo-storage, and the photo service's policy lets that client exchange a token carrying storage.read for archive.read at the archive. At 09:02:01 it sends:

POST /token HTTP/1.1
Host: auth.photos.example
Authorization: Basic cGhvdG8tc3RvcmFnZTpkZW1vLW9ubHktbm90LWEtcmVhbC1zZWNyZXQ=
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
&resource=https%3A%2F%2Farchive.photos.example
&scope=archive.read

The subject token is token B, shortened, and the Basic header represents photo-storage:demo-only-not-a-real-secret. The authorization server checks that photo-storage may exchange tokens, that token B was issued for the storage service and is still valid, that archive.read follows from storage.read, and that the archive is a target this client may reach. It returns token C with an expires_in of 59:

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

The act claim now has two levels. The outer one names photo-storage, the current actor. Inside it, photo-api appears as the earlier actor. Read from the outside in, it is a history: the storage service is acting for you now, and before it, the photo API was.

The expiry repays a closer look. A full minute from 09:02:01 would end at 09:03:01, but token B expires at 09:03:00, and the photo service never lets a token outlive its subject token. So token C ends at 09:03:00 as well, and expires_in is 59 seconds. No token in the chain can outlast the one before it, though each may end sooner.

Notice who is missing. The printer appears in token A only as its client_id, and that claim names the client that requested each token, so it changes at every hop. The chain in act records the actors that exchanges created. To learn that the printer started all this, the archive's operators would follow the jti values back through the authorization server's exchange records. A deployment that wanted the archive itself to know would have to put that in the token deliberately.

What each service decided

The archive validates token C like any access token, then decides, considering only the top-level claims and the current actor. archive.read covers the request, photo-0311 belongs to user-2048, and the current actor is photo-storage, the only caller the archive expects. photo-api, nested inside, goes into the archive's log and plays no part in the decision. The archive returns the original, the storage service passes it to the photo API, and the photo API includes it in the album for the printer.

ServiceTokenAudience and scopeCurrent actor
Photo APIAhttps://api.photos.example, photos.readNone; the client is photo-printer
Storage serviceBhttps://storage.photos.example, storage.readphoto-api
Archive serviceChttps://archive.photos.example, archive.readphoto-storage

The subject never changed: every service made its decision about user-2048. Everything else shifted at each hop. The audience moved to the next service, the scope moved into that service's vocabulary, the lifetime shrank to a minute or less, and the actor became whoever was holding the token.

The printer calls the photo API with token A. The photo API exchanges token A at the token endpoint for token B, addressed to the storage service with photo-api as actor, and requests photo-0311 from the storage service with token B. The original is archived, so the storage service exchanges token B for token C, addressed to the archive, with photo-storage as the current actor and photo-api nested as the earlier actor. The storage service requests the original from the archive with token C. The archive decides using the subject and the current actor, and returns the original, which passes back through the storage service and the photo API to the printer. The printer calls the photo API with token A. The photo API exchanges token A at the token endpoint for token B, addressed to the storage service with photo-api as actor, and requests photo-0311 from the storage service with token B. The original is archived, so the storage service exchanges token B for token C, addressed to the archive, with photo-storage as the current actor and photo-api nested as the earlier actor. The storage service requests the original from the archive with token C. The archive decides using the subject and the current actor, and returns the original, which passes back through the storage service and the photo API to the printer.
Two exchanges carry your request from the photo API to the archive. Each service receives a token addressed only to it, and each exchange adds the new actor to the front of the chain.

Small changes to the example bring the earlier lessons back into play. If the storage service had asked for archive.delete, the server would have answered invalid_scope. If the printer had tried to exchange token A itself, it would have received unauthorized_client. If the storage service had waited until 09:03:05, token B would have expired and the second exchange would have failed with invalid_request. The storage service would report that failure to the photo API, which still holds a valid token A and could exchange it once more and retry.

Every service in this example knew the photo service's token endpoint and signing keys from its own configuration. The next group, Authorization server metadata, looks at how clients and services can discover that configuration, and how to decide whether to trust what they find.

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 2Token C has an act claim naming photo-storage, with photo-api nested inside. Which actor should the archive service use in its access decision?

QUESTION 2 OF 2The archive's operators want to know which application started the request that read photo-0311. Where do they find it?

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