OAUTH 2.0 · LAB
Send a token exchange request field by field and follow the new token
Send the lesson's exchange request, read issued_token_type and the missing scope, use the new token at storage, and see the subject token is neither used up nor linked. Today, trace the refusal by request ID.
PlannedUses your lab tenant
The lesson
Builds on: The problem token exchange solves.
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.
- G6 Token exchange (RFC 8693)
- G55 Tenant resource (API) registry
Setup
These steps are real today. You need lab-printer, lab-photo-api, their credentials in the shell, and a fresh printer token AT for Ava, as in Try three ways to call the storage service.
In its own terminal, start the photo API, which accepts Ava's printer token:
btl-lab resource --port 8766 --mode introspect --audience "$ISSUER/resource"
Planned walkthrough
This walkthrough runs once your tenant supports token exchange (G6) with the planned resources and lab-photo-api exchange policy from the first token exchange lab (G55).
In another terminal, start storage with
btl-lab resource --port 8767 --mode introspect --audience https://storage.lab.example --read-scope storage.read. Send the full request, including the optionalrequested_token_type:
X=$(curl -s -D h.txt -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 requested_token_type=urn:ietf:params:oauth:token-type:access_token \
--data-urlencode resource=https://storage.lab.example -d scope=storage.read)
jq 'del(.access_token)' <<<"$X"; grep -i cache-control h.txt
The response has issued_token_type urn:ietf:params:oauth:token-type:access_token, token_type Bearer and expires_in 60, with Cache-Control: no-store. It has no scope, because the server issued exactly what was asked for, and no refresh token.
Why it matters: issued_token_type says what was issued, token_type says how to use it at an API. The new token arrives in access_token even though an exchange can issue other kinds.
Look inside:
ST=$(jq -r .access_token <<<"$X"); unset X; btl-lab decode "$ST". The header hastypat+jwtand your tenant's usualkid. The claims keepissand Ava'ssub, and changeaudto storage,client_idto the photo API,scopetostorage.read, andexpto 60 seconds afteriat, withactnaming the photo API.
Why it matters: to storage it is an ordinary access token from the same issuer, validated in the ordinary way.
Use it:
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $ST" http://127.0.0.1:8767/objects/photo-1187returns200, and storage logs Ava as subject and the photo API as actor. The photo API may reuseSTfor the album's other files during its minute, keyed by subject as well as audience.The subject token is not used up. Call the photo API with
ATagain: still200.The two tokens are not linked. As
lab-printer, revokeATat$ISSUER/oauth/revoke, then call storage withSTwithin its minute: still allowed, andbtl-lab introspect "$ST"still reports it active.
Why it matters: revocation does not carry across an exchange unless the deployment builds that in. The one-minute lifetime keeps the gap short. A planned tenant setting, "Revoke exchanged tokens when the subject token is revoked", would be a meaningful choice to expose, with this step as its test.
Do today
Send exactly the step 1 request today, with a real
AT:
curl -s -D h.txt -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 requested_token_type=urn:ietf:params:oauth:token-type:access_token \
--data-urlencode resource=https://storage.lab.example -d scope=storage.read | jq .
grep -i -E 'x-request-id|cache-control' h.txt
The result is {"error":"unsupported_grant_type",...}, with Cache-Control: no-store and an X-Request-ID.
Find it. In Lab Photos, open Logs, filter outcome rejected, and look for the token endpoint summary that includes your request ID. Audit has no entry, because the request was refused before the client authenticated.
Why it matters: a request ID joins records even when no token was issued, which is exactly what an operator needs when an exchange fails several services deep.
A token is not consumed by being used. Call the photo API twice with
AT:
for i in 1 2; do curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $AT" http://127.0.0.1:8766/photos; done
Both return 200. An exchange reads its subject token the same way, unless a token type is defined to be single-use.
Break it
Planned: ask for
requested_token_type=urn:ietf:params:oauth:token-type:jwtwhen the policy allows only access tokens. The result isinvalid_request.
Check your work
There are no automated checks while this lab is planned. For Do today, look in Lab Photos' Logs for the refused exchange by its request ID, and in Audit for the photo API's oauth.introspect events with other_client_token_found.
Cleanup
Stop the local services and delete h.txt. Run unset ST.
Missing infrastructure
G6 Token exchange: the grant,
issued_token_type,requested_token_type, the 60-second capped lifetime, delegationact, and a tenant-visible exchange record.G55 Tenant resource (API) registry for the storage target and its scopes.