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

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.

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.

  1. Ask for photos.share before it is assigned

    Recorded as oauth.authorize rejected (invalid_scope) for lab-printer.

  2. Assign photos.share to lab-printer

    Recorded as tenant.oauth.clients.update succeeded.

  3. Get a sharing token for Ava

    Recorded as oauth.token succeeded for lab-printer about [email protected].

  4. Be refused a token that spans both APIs

    Recorded as oauth.token rejected (policy_denied) for lab-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

  1. Choose Lab Photos as the lab tenant and press Start.

  2. In OAuth Management, Scopes, confirm photos.read is Common and photos.share is Exclusive. If they are missing, reset the lab tenant with the Lab Photos preset or create them.

  3. Use the variables and the authorize and exchange helpers from Present an access token correctly, for lab-printer.

  4. In Access Token Management, create a manager lab-tmp-audiences (Signed JWT, the current ES256 key, Maximum lifetime 600). Under Standard claims, set aud to a JavaScript expression:

context.scopes.includes('photos.share') ? 'https://share.lab.test' : 'https://photos.lab.test'
  1. 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.read alone, photos.share alone 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: {} };
  1. 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.test

    • the sharing API: btl-lab resource --mode jwt --audience https://share.lab.test --port 8767

Walkthrough

  1. Ask for the exclusive scope before the registration allows it: authorize "photos.share", open the URL and sign in as Ava. The callback carries error=invalid_scope and 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.

  1. Read what discovery publishes.

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

scopes_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.

  1. Keep an earlier lab-printer token as BEFORE. In Clients, lab-printer, add photos.share to Assigned scopes and save. Read the warning: a change to access settings revokes the client's tokens. Then introspect BEFORE as lab-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.

  1. In Access Token Management, Assign a client: lab-printer to lab-tmp-audiences. Get a photos.read token. btl-lab decode "$TOKEN" shows aud https://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.

  1. Get a photos.share token. Its aud is https://share.lab.test. POST /shares on port 8767 returns 200; GET /photos on port 8766 returns 401 with wrong_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.

  1. Ask for both at once: authorize "photos.read photos.share", sign in and approve. The code is issued, and the exchange fails with invalid_grant. Audit records oauth.token rejected with policy_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.

  1. Try to name the API explicitly: add &resource=https://share.lab.test to a photos.read authorization URL, then exchange. The token's aud is 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.

  1. Weigh granularity. Look at the five lab scopes and decide, for each pair, whether Ava would decide differently about them. photos.read and photos.delete clearly 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

  1. Assign Default access tokens back to lab-printer, then delete lab-tmp-audiences.

  2. Leave photos.share assigned to lab-printer; later labs use it.

  3. Stop both btl-lab resource terminals.

Missing infrastructure

  • G16 Resource indicators. The tenant ignores the resource parameter, so the audience can only be derived from scope with a manager expression. Once resource indicators exist, step 7 names https://share.lab.test at the authorization request and the token request, the token's aud follows 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 --audience options and let the tenant validate the audience values against a resource registry.

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