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

Validate, approve and enforce authorization details end to end

Trigger each invalid_authorization_details rule, approve one album of two, narrow a token to part of the grant, and watch an API enforce exactly what Ava approved. Today, practice the same rules with scopes.

PlannedIncludes a simulationUses your lab tenant

The lesson

Builds on: Writing authorization details.

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.

Setup

These steps are real today.

  1. In Lab Photos, open OAuth > Flow policy and allow refresh_token. On lab-printer, allow the refresh_token grant and assign the scopes photos.read, photos.delete and offline_access.

  2. Keep CLIENT_ID, CLIENT_SECRET, AD, PAY and the enc helper from Write album and payment authorization details. For introspection, export API_ID and API_SECRET for lab-photo-api, which has Resource server on.

Planned walkthrough

This walkthrough runs once your tenant supports tenant-defined authorization details types (G15), with the photo_album and payment_initiation types from the previous lab, and hosts a sample photo API with per-user albums (G3).

  1. An unknown field. Send this through the browser as in the previous lab:

[{"type": "photo_album", "actions": ["read"], "identifer": "42"}]

The listener receives error=invalid_authorization_details with state and iss. Audit records oauth.authorize rejected invalid_authorization_details (planned reason).

Why it matters: ignoring a field the client thought limited access would grant something nobody approved, so the whole request is refused.

  1. The other four rules, one request each: an unknown type (account_information), a field of the wrong kind ("identifier": 42), a value the type does not allow ("actions": ["delete"]), and a missing required field (photo_album without identifier). Each gets the same error before Ava sees anything.

  2. Approve less than was asked. Send AD with its two albums, untick album 57 on the consent screen, and exchange the code. The token response's authorization_details holds only album 42. Compare it with the request and continue with what was granted.

Why it matters: the server stores what Ava approved with the grant, and later token requests and refreshes are judged against it, as with granted scopes.

  1. Narrow at the token endpoint. Redeem a two-album code with one album named:

curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/token" -d grant_type=authorization_code --data-urlencode "code=$CODE" \
  --data-urlencode redirect_uri=http://127.0.0.1:8765/callback --data-urlencode "code_verifier=$VERIFIER" \
  --data-urlencode 'authorization_details=[{"type":"photo_album","actions":["read"],"identifier":"42"}]' | jq .authorization_details

The token covers album 42 only. Asking for album 99 instead returns invalid_authorization_details.

Why it matters: a token can carry part of the grant, never more.

  1. The API's copy. btl-lab introspect "$TOKEN" shows the same approved authorization_details, filtered to what the photo API needs.

  2. Enforcement at the sample API: GET $ISSUER/resource/albums/42/photos returns 200; .../albums/57/photos, GET $ISSUER/resource/photos and any DELETE return 403.

Why it matters: the API looks for an object of a type it understands whose action and identifier match the request, compared exactly, then still checks that album 42 is Ava's.

  1. Escaping on the consent screen. Push a payment whose creditorName is <b>Photo Printer</b>. The consent screen shows the angle brackets as text.

Why it matters: every value on that screen came from the client, so it is data, never markup.

Do today

  1. The scope version of step 4 is real. Start btl-lab callback, then request photos.read photos.delete offline_access:

eval "$(btl-lab pkce)"; eval "$(btl-lab state)"
echo "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback&scope=$(enc 'photos.read photos.delete offline_access')&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"

Approve as Ava, check state and iss, read -rs CODE, exchange the code and keep the refresh token in REFRESH. Then ask for part of the grant, and then for more than it:

RESP=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/token" -d grant_type=refresh_token --data-urlencode "refresh_token=$REFRESH" -d scope=photos.read)
REFRESH=$(jq -r .refresh_token <<<"$RESP"); jq '{scope}' <<<"$RESP"; unset RESP
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/token" -d grant_type=refresh_token --data-urlencode "refresh_token=$REFRESH" \
  -d 'scope=photos.read photos.share' | jq .

The first returns "scope": "photos.read". The second returns invalid_scope, and Audit records oauth.token rejected scope_not_granted. The rotated refresh token still carries the full grant, just as a narrowed details token leaves the stored approval intact.

  1. Build the API's matching rule now.

Simulation. no tenant issues authorization_details yet, so these claims are the lesson's hand-written token, not one from your tenant.

Save as match.mjs:

const claims = {sub: 'user-2048', authorization_details: [{type: 'photo_album', actions: ['read'], identifier: '42'}]};
const [action, album] = process.argv.slice(2);
const ok = (claims.authorization_details ?? []).some(d => d.type === 'photo_album' && d.actions?.includes(action) && d.identifier === album);
console.log(ok ? 'allow, then check that the album is the subject\'s' : '403 Forbidden');

Run node match.mjs read 42 (allow), node match.mjs read 57 and node match.mjs delete 42 (both 403 Forbidden).

Break it

  1. Planned: steps 1, 2 and 4 are the failures. Each is a decision, and repeating the same request changes nothing.

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 with the refresh grant and oauth.token rejected scope_not_granted.

Cleanup

Delete match.mjs if you do not want it and run unset REFRESH CODE.

Missing infrastructure

  • G15 Rich Authorization Requests: the five validation rules, partial approval on the consent screen, enrichment where a type allows it, storage with the grant, narrowing at the token endpoint, and the token, JWT and introspection members.

  • G3 Sample protected resource API: a hosted photo API with per-user sample albums, so the refusal for an album that belongs to someone else is real. Your tenant itself has no albums to check.

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