OAUTH 2.0 · LAB
Give the photo API and the sharing API separate audiences
Assign an exclusive scope, derive each token's audience from its scope, refuse a request that mixes two APIs, and watch two local APIs reject each other's tokens.
Partly readyUses your lab tenant
The lesson
Builds on: Token formats and validation.
New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.
Partly ready. Most of this lab runs today. Steps that wait on platform features are marked, and Missing infrastructure says what they need.
- G16 Resource indicators
- G3 Sample protected resource API; no RFC 9728 protected resource metadata
Your progress
Press Start before you begin. Only events your tenant records after that count, in the order below. Checking reads your tenant's Audit, so you need Audit read access in it.
Sign in to start this lab and check your progress. Log in or create an account.
Ask for photos.share before it is assigned
Recorded as
oauth.authorizerejected (invalid_scope) forlab-printer.Assign photos.share to lab-printer
Recorded as
tenant.oauth.clients.updatesucceeded.Get a sharing token for Ava
Recorded as
oauth.tokensucceeded forlab-printerabout[email protected].Be refused a token that spans both APIs
Recorded as
oauth.tokenrejected (policy_denied) forlab-printer.
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
Choose Lab Photos as the lab tenant and press Start.
In OAuth Management, Scopes, confirm
photos.readis Common andphotos.shareis Exclusive. If they are missing, reset the lab tenant with the Lab Photos preset or create them.Use the variables and the
authorizeandexchangehelpers from Present an access token correctly, forlab-printer.In Access Token Management, create a manager
lab-tmp-audiences(Signed JWT, the current ES256 key, Maximum lifetime600). Under Standard claims, setaudto a JavaScript expression:
context.scopes.includes('photos.share') ? 'https://share.lab.test' : 'https://photos.lab.test'
In the same manager, open Advanced issuance policy and refuse any request that mixes the two APIs' scopes. Use Test with sample context with
photos.readalone,photos.sharealone and both together before you save.
const share = context.scopes.includes('photos.share');
const photo = context.scopes.some(s => ['photos.read', 'photos.write', 'photos.delete'].includes(s));
return share && photo ? { allow: false, claims: {} } : { allow: true, claims: {} };
Do not assign the manager yet. Start two local APIs in separate terminals:
the photo API:
btl-lab resource --mode jwt --audience https://photos.lab.testthe sharing API:
btl-lab resource --mode jwt --audience https://share.lab.test --port 8767
Walkthrough
Ask for the exclusive scope before the registration allows it:
authorize "photos.share", open the URL and sign in as Ava. The callback carrieserror=invalid_scopeand no code.
Why it matters: the registration caps every token. However willing Ava is, she cannot approve a scope this client's registration does not allow.
Read what discovery publishes.
GET$ISSUER/.well-known/oauth-authorization-server
Open in console
GET $ISSUER/.well-known/oauth-authorization-server HTTP/1.1scopes_supported lists photos.read and the built-in scopes, not the exclusive photos.share.
Why it matters: discovery describes what any client may ask for. It grants nothing, and it does not advertise scopes reserved for particular clients.
Keep an earlier
lab-printertoken asBEFORE. In Clients,lab-printer, addphotos.shareto Assigned scopes and save. Read the warning: a change to access settings revokes the client's tokens. Then introspectBEFOREaslab-printer:
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/introspect" --data-urlencode "token=$BEFORE" | jq .
Returns {"active": false}.
Why it matters: the registration is the third limit in the lesson's table, and changing it ends what the client already holds rather than leaving old tokens with old rules.
In Access Token Management, Assign a client:
lab-printertolab-tmp-audiences. Get aphotos.readtoken.btl-lab decode "$TOKEN"showsaudhttps://photos.lab.test. Call both APIs:
curl -si http://127.0.0.1:8766/photos -H "Authorization: Bearer $TOKEN" # 200
curl -si -X POST http://127.0.0.1:8767/shares -H "Authorization: Bearer $TOKEN" # 401, wrong_audience
Why it matters: the audience is checked during validation, before scope. A token addressed to the photo API never reaches the sharing API's scope check.
Get a
photos.sharetoken. Itsaudishttps://share.lab.test.POST /shareson port 8767 returns200;GET /photoson port 8766 returns401withwrong_audience.
Why it matters: if a sharing token leaks from the sharing API's logs, it can create share links until it expires, but it cannot read a single photo.
Ask for both at once:
authorize "photos.read photos.share", sign in and approve. The code is issued, and the exchange fails withinvalid_grant. Audit recordsoauth.tokenrejected withpolicy_denied.
Why it matters: one token for two APIs would recreate the shared token the lesson warns about. The JWT profile's answer here is invalid_scope at the authorization endpoint; this tenant applies your issuance policy at the token endpoint instead.
Try to name the API explicitly: add
&resource=https://share.lab.testto aphotos.readauthorization URL, then exchange. The token'saudis unchanged.
Note: the tenant ignores resource today (G16). With resource indicators the printer would name the photo API at the code exchange and the sharing API at a later refresh, and receive two narrowly addressed tokens from one approval.
Weigh granularity. Look at the five lab scopes and decide, for each pair, whether Ava would decide differently about them.
photos.readandphotos.deleteclearly pass. Write down one scope you would not split, and why.
Why it matters: a scope that bundles reading and deleting forces a printer to ask for the power to empty a library in order to print from it.
Break it
Turn on Restrict scopes for lab-printer without assigning photos.read. A photos.read request now fails with invalid_scope, even though the scope is Common.
Restore: assign photos.read to lab-printer, or turn Restrict scopes off again, whichever matches how you had it.
Check your work
Press Check my progress. The checks look for, in order: oauth.authorize rejected with invalid_scope for lab-printer, the tenant.oauth.clients.update that assigned photos.share, a successful photos.share token for Ava, and oauth.token rejected with policy_denied.
Both local API logs should show wrong_audience refusals.
Cleanup
Assign Default access tokens back to
lab-printer, then deletelab-tmp-audiences.Leave
photos.shareassigned tolab-printer; later labs use it.Stop both
btl-lab resourceterminals.
Missing infrastructure
G16 Resource indicators. The tenant ignores the
resourceparameter, so the audience can only be derived from scope with a manager expression. Once resource indicators exist, step 7 nameshttps://share.lab.testat the authorization request and the token request, the token'saudfollows it, and a request for two resources is narrowed per token instead of refused by a policy script.G3 Hosted protected resource. Both APIs run locally from the toolkit. A hosted photo API and sharing API with their own registered identifiers would replace the
--audienceoptions and let the tenant validate the audience values against a resource registry.