OAUTH 2.0 · LAB
Exchange for a narrower token and see which targets and scopes are refused
Name the storage target by resource and by audience, let the policy map photos.read to storage.read, and meet invalid_scope and invalid_target. Today, practice the same limits with client credentials.
PlannedUses your lab tenant
The lesson
Builds on: Following a token exchange.
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)
- G16 Resource indicators
- G55 Tenant resource (API) registry
Setup
These steps are real today. You need lab-photo-api with the client_credentials grant, the photos.read scope and the manager lab-tmp-at-storage, its credentials in the shell, and a fresh printer token AT for Ava, all from Try three ways to call the storage service. Confirm photos.delete exists in Lab Photos and is not assigned to lab-photo-api.
Planned walkthrough
This walkthrough runs once your tenant supports token exchange (G6) with resource indicators semantics in the exchange (G16) and a resource registry with logical names (G55), using the planned exchange policy from the first token exchange lab.
Planned setup
Create the confidential client
lab-tmp-photo-storage, the storage service's own registration. In the registry (planned), give the storage resource the logical namelab-tmp-photo-storage, soaudiencecan name it. Also register the sharing API athttps://share.lab.example, whichlab-photo-api's exchange policy does not list.
Steps
Each step sends the exchange from the first lab with a different target or scope. This helper keeps the requests short:
exchange() { 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 'del(.access_token)'; }
Name the target by location:
exchange --data-urlencode resource=https://storage.lab.example -d scope=storage.read. The token'saudishttps://storage.lab.example.Name it logically:
exchange -d audience=lab-tmp-photo-storage -d scope=storage.read. The sameaud.
Why it matters: resource names where the token will be used and audience names the service as both sides know it. Either way the authorization server decides aud, and the storage service's audience check gives an exchanged token no exception.
Ask for more than the subject token allows. Ava's printer token carries only
photos.read:exchange --data-urlencode resource=https://storage.lab.example -d scope=storage.deletereturnsinvalid_scope.
Why it matters: no policy row turns read access into delete access. An exchange cannot add what Ava never allowed the printer.
Ask for both:
exchange --data-urlencode resource=https://storage.lab.example -d 'scope=storage.read storage.delete'. Lab Photos issuesstorage.readalone, and the response includes"scope": "storage.read"because it differs from the request.A real target this client may not reach:
exchange --data-urlencode resource=https://share.lab.example -d scope=photos.sharereturnsinvalid_target.
Why it matters: a target that exists is not a target this client may reach through exchange.
Two targets at once:
exchange --data-urlencode resource=https://storage.lab.example --data-urlencode resource=https://archive.lab.example -d 'scope=storage.read archive.read'returnsinvalid_target.
Why it matters: one token valid at both services is exactly what audience restriction avoids, and an exchange costs only one round trip, so the photo API asks for one target when the call is about to happen.
Do today
Per-client scope limits are real. Ask for an exclusive scope
lab-photo-apiis not assigned, then for a mix:
curl -s -u "$API_ID:$API_SECRET" "$ISSUER/oauth/token" -d grant_type=client_credentials -d scope=photos.delete | jq .
curl -s -u "$API_ID:$API_SECRET" "$ISSUER/oauth/token" -d grant_type=client_credentials -d 'scope=photos.read photos.delete' | jq .
Both return invalid_scope, and Audit records oauth.token rejected invalid_scope for lab-photo-api. Your tenant refuses the whole request rather than issuing the allowed part, the stricter of the two behaviors the lesson describes for step 4.
Leave the scope out:
RESP=$(curl -s -u "$API_ID:$API_SECRET" "$ISSUER/oauth/token" -d grant_type=client_credentials)
jq 'del(.access_token)' <<<"$RESP"; CT=$(jq -r .access_token <<<"$RESP"); unset RESP
The response has no scope member, and the token carries none: your tenant's policy grants nothing that was not requested. A client that needs particular access asks for it and checks what came back.
The server decides the audience. Run
btl-lab decode "$CT":audishttps://storage.lab.example, set bylab-tmp-at-storage, whatever the request said. Sending tworesourcevalues today is refused outright withinvalid_request, before client authentication.
Break it
Planned: ask for
scope=photos.readwithresource=https://storage.lab.example. The scope has no meaning at storage, and Lab Photos answers with its documented choice,invalid_targetorinvalid_scope. Either way, sending the same request again changes nothing.
Check your work
There are no automated checks while this lab is planned. For Do today, look in Lab Photos' Audit for two oauth.token rejections with invalid_scope and one success for lab-photo-api.
Cleanup
Delete lab-tmp-photo-storage if you created it for the planned steps. Keep the storage setup for the next lab. Run unset CT.
Missing infrastructure
G6 Token exchange with a scope mapping per target in the exchange policy.
G16 Resource indicators semantics in the exchange:
resourceandinvalid_target, including too many targets.G55 Tenant resource (API) registry with logical names for
audience.