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.
- G6 Token exchange (RFC 8693)
- G55 Tenant resource (API) registry
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.
In Lab Photos, confirm OAuth > Flow policy allows
authorization_codeandclient_credentials, and thatlab-printerandlab-photo-apiexist as in Make your photo API refuse tokens meant for another API.In OAuth > Access token managers, create
lab-tmp-at-storage(JWT, ES256) with the standardaudclaim overridden tohttps://storage.lab.example.On
lab-photo-api, allow theclient_credentialsgrant, assign the scopephotos.read, and assign the managerlab-tmp-at-storage. This gives the photo API a token of its own for storage, the shortcut the lesson rejects.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
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
OAuth > Flow policy (planned): allow
urn:ietf:params:oauth:grant-type:token-exchangefor the tenant.OAuth > Resources (planned): the photo API at
$ISSUER/resourceowningphotos.readandphotos.delete; the storage service athttps://storage.lab.exampleowningstorage.readandstorage.delete; the archive athttps://archive.lab.exampleowningarchive.read.OAuth > Clients > lab-photo-api > Exchange policy (planned): accept access tokens from this tenant whose
audis$ISSUER/resource; targethttps://storage.lab.example; mapphotos.readtostorage.readandphotos.deletetostorage.delete; record this client inact(delegation); lifetime 60 seconds, never past the subject token'sexp; keepauth_timeandacrunchanged. Anything not listed is refused.Planned events:
oauth.tokensucceeded with reasontoken_exchanged, recording client, subject, the subject token'sjti, the issuedjti, target and scope, never a token. Refusals recordunauthorized_client,invalid_target,invalid_scope,invalid_subject_tokenorinvalid_actor_token. Policy changes recordtenant.oauth.clients.update.
Steps
Get Ava's printer token as in the audience lab (a
lab-printercode flow forphotos.read) and keep it asAT. 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.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
Forward Ava's token. Get it with an ordinary
lab-printercode flow forphotos.read(startbtl-lab callback, open the authorization URL, approve as Ava, checkstateandiss, exchange the code) and keep it asAT. 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.
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.
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.
Confirm in metadata:
GET$ISSUER/.well-known/oauth-authorization-server
Open in console
GET $ISSUER/.well-known/oauth-authorization-servergrant_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 withact, and thetoken_exchangedrecord.G55 Tenant resource (API) registry: named targets for
resourceand the scopes each target owns. Until it exists, an exchange target could only be a fixed manager audience.