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

OAUTH 2.0 · LAB

Try three ways to call the storage service on Ava's behalf

Forward Ava's token to a storage service, then call it with the photo API's own credentials, and see what each loses. Then send the token exchange that replaces both.

PlannedUses your lab tenant

The lesson

Builds on: Audience restrictions and validation.

New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.

Planned. The core of this lab waits on platform features that are not built yet. The planned walkthrough shows exactly how it will run; Do today is a real exercise you can do now.

Request console

Requests in this lab can be sent from this page to your tenant: open one and choose Send. Fill in the values below first. They stay in this page's memory and are gone when you leave; secrets are never stored or sent anywhere except the request you send.

Setup

These steps are real today. In the token exchange labs, lab-photo-api plays the lesson's photo API, and a local btl-lab resource plays the storage service at https://storage.lab.example, a name only.

  1. In Lab Photos, confirm OAuth > Flow policy allows authorization_code and client_credentials, and that lab-printer and lab-photo-api exist as in Make your photo API refuse tokens meant for another API.

  2. In OAuth > Access token managers, create lab-tmp-at-storage (JWT, ES256) with the standard aud claim overridden to https://storage.lab.example.

  3. On lab-photo-api, allow the client_credentials grant, assign the scope photos.read, and assign the manager lab-tmp-at-storage. This gives the photo API a token of its own for storage, the shortcut the lesson rejects.

  4. Store the credentials:

export CLIENT_ID='<lab-printer client id>'; read -rs CLIENT_SECRET; export CLIENT_SECRET
export API_ID='<lab-photo-api client id>'; read -rs API_SECRET; export API_SECRET
  1. In its own terminal, start the storage service. It validates tokens by introspection and accepts only its own audience:

btl-lab resource --port 8767 --mode introspect --audience https://storage.lab.example

Planned walkthrough

This walkthrough runs once your tenant supports the token exchange grant with a per-client exchange policy (G6) and a resource registry (G55). The token exchange labs share this planned setup.

Planned setup

  1. OAuth > Flow policy (planned): allow urn:ietf:params:oauth:grant-type:token-exchange for the tenant.

  2. OAuth > Resources (planned): the photo API at $ISSUER/resource owning photos.read and photos.delete; the storage service at https://storage.lab.example owning storage.read and storage.delete; the archive at https://archive.lab.example owning archive.read.

  3. OAuth > Clients > lab-photo-api > Exchange policy (planned): accept access tokens from this tenant whose aud is $ISSUER/resource; target https://storage.lab.example; map photos.read to storage.read and photos.delete to storage.delete; record this client in act (delegation); lifetime 60 seconds, never past the subject token's exp; keep auth_time and acr unchanged. Anything not listed is refused.

  4. Planned events: oauth.token succeeded with reason token_exchanged, recording client, subject, the subject token's jti, the issued jti, target and scope, never a token. Refusals record unauthorized_client, invalid_target, invalid_scope, invalid_subject_token or invalid_actor_token. Policy changes record tenant.oauth.clients.update.

Steps

  1. Get Ava's printer token as in the audience lab (a lab-printer code flow for photos.read) and keep it as AT. Restart the storage service so it reads the storage scope: btl-lab resource --port 8767 --mode introspect --audience https://storage.lab.example --read-scope storage.read.

  2. Exchange it as the photo API:

X=$(curl -s -u "$API_ID:$API_SECRET" "$ISSUER/oauth/token" \
  --data-urlencode grant_type=urn:ietf:params:oauth:grant-type:token-exchange --data-urlencode "subject_token=$AT" \
  --data-urlencode subject_token_type=urn:ietf:params:oauth:token-type:access_token \
  --data-urlencode resource=https://storage.lab.example -d scope=storage.read)
jq '{issued_token_type, token_type, expires_in, scope}' <<<"$X"; ST=$(jq -r .access_token <<<"$X"); unset X
btl-lab decode "$ST"
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $ST" http://127.0.0.1:8767/objects/photo-1187

The new token's sub is Ava's ID, aud is the storage service, client_id and act.sub are the photo API, and scope is storage.read. Storage allows the request and logs Ava as the subject and the photo API as the actor.

Why it matters: only the authorization server, which issued Ava's token and knows the clients, scopes and APIs, can produce a token for storage, about Ava, with no more access than she gave the printer and for less time.

Do today

  1. Forward Ava's token. Get it with an ordinary lab-printer code flow for photos.read (start btl-lab callback, open the authorization URL, approve as Ava, check state and iss, exchange the code) and keep it as AT. Then:

curl -s -i -H "Authorization: Bearer $AT" http://127.0.0.1:8767/objects/photo-1187 | head -n 1

The status is 401 and storage logs a wrong audience.

Why it matters: storage correctly refuses a token addressed to the photo API, and the photo API should not want a token that works in both places.

  1. Use the photo API's own credentials:

CT=$(curl -s -u "$API_ID:$API_SECRET" "$ISSUER/oauth/token" -d grant_type=client_credentials -d scope=photos.read | jq -r .access_token)
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $CT" http://127.0.0.1:8767/objects/photo-1187
btl-lab decode "$CT"

Storage allows it, and the token's sub is the photo API's own client ID.

Why it matters: this token is about the photo API, not Ava. Storage cannot tell whose files are being read, and would serve anyone's.

  1. Send the exchange to today's tenant:

curl -s -u "$API_ID:$API_SECRET" "$ISSUER/oauth/token" \
  --data-urlencode grant_type=urn:ietf:params:oauth:grant-type:token-exchange --data-urlencode "subject_token=$AT" \
  --data-urlencode subject_token_type=urn:ietf:params:oauth:token-type:access_token | jq .

The result is {"error":"unsupported_grant_type",...}. The tenant refuses it before client authentication, so it appears in Logs (filter outcome rejected), not in Audit. A client handling this fails closed; it does not fall back to step 2.

  1. Confirm in metadata:

GET$ISSUER/.well-known/oauth-authorization-server Open in console
GET $ISSUER/.well-known/oauth-authorization-server

grant_types_supported does not list urn:ietf:params:oauth:grant-type:token-exchange.

Check your work

There are no automated checks while this lab is planned. For Do today, look in Lab Photos' Audit for oauth.token succeeded for lab-printer and for lab-photo-api, and in Logs for the refused exchange. The storage service's log shows a wrong audience, then allowed with the photo API as subject.

Cleanup

Stop the storage service. If you continue with the token exchange labs, keep lab-tmp-at-storage and the client_credentials settings on lab-photo-api; the last lab removes them. Otherwise, on lab-photo-api remove the client_credentials grant and the photos.read scope, assign the tenant default manager, and delete lab-tmp-at-storage. Run unset AT CT.

Missing infrastructure

  • G6 Token exchange: the grant at the token endpoint, subject token validation, a per-client exchange policy, issued_token_type, delegation with act, and the token_exchanged record.

  • G55 Tenant resource (API) registry: named targets for resource and the scopes each target owns. Until it exists, an exchange target could only be a fixed manager audience.

Back to all labs

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

The Lab