OAUTH 2.0 · LAB
Ask for one album instead of the whole photo library
Request read access to album 42 with authorization_details and compare the consent screen and token with photos.read. Today, see the parameter ignored and feel the scope-string workaround.
PlannedUses your lab tenant
The lesson
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.
- G15 RAR (`authorization_details`)
- G3 Sample protected resource API; no RFC 9728 protected resource metadata
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 Lab Photos, confirm
lab-printer(confidential web, redirect URIhttp://127.0.0.1:8765/callback, PKCE required, scopephotos.read) and that OAuth > Flow policy allowsauthorization_code.Store its credentials and the album request you will send:
export CLIENT_ID='<lab-printer client id>'; read -rs CLIENT_SECRET; export CLIENT_SECRET
AD='[{"type":"photo_album","actions":["read"],"identifier":"42"}]'
enc() { jq -rn --arg v "$1" '$v|@uri'; }
Planned walkthrough
This walkthrough runs once your tenant supports Rich Authorization Requests with tenant-defined types (G15) and hosts a sample photo API (G3).
Planned setup
Open OAuth > Authorization details (planned) and create the type
photo_album. Fields:actions(list, allowed valueread),identifier(string, required),datatypes(list, allowedimagesandcaptions),locations(list, allowed$ISSUER/resource). Consent text: "Read album {identifier}". Allowed clients:lab-printer. Audit recordstenant.oauth.authorization_details.create(planned event name).
Steps
The server lists the types it understands:
GET$ISSUER/.well-known/oauth-authorization-server
Open in console
GET $ISSUER/.well-known/oauth-authorization-serverauthorization_details_types_supported is ["photo_album"].
Why it matters: a client confirms the server knows a type before building an object of it.
The scope baseline. Start
btl-lab callback, then requestphotos.read:
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=photos.read&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"
The consent screen says the printer wants to view your photos, and the token's scope is photos.read.
Why it matters: the scope cannot say which album, so the token can read every photo in Ava's library.
One album. Run the listener again, and send the details instead of a scope:
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&authorization_details=$(enc "$AD")&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"
The consent screen reads "Read album 42". Approve as Ava, check state and iss, then exchange the code:
read -rs CODE
RESP=$(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")
jq .authorization_details <<<"$RESP"; TOKEN=$(jq -r .access_token <<<"$RESP"); unset CODE RESP
btl-lab decode "$TOKEN"
The token response repeats the approved object, and the token carries an authorization_details claim and no scope.
Why it matters: access described as data travels from client to consent screen to token, with the part that changes every time, the album, as a value rather than a private scope format.
Scopes and details together. Repeat step 3 with
scope=openidadded. One consent screen shows both the sign-in and the album.
Why it matters: the server must process both forms together and show the combined request, never two separate approvals.
Enforcement at the sample photo API:
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOKEN" "$ISSUER/resource/albums/42/photos"
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOKEN" "$ISSUER/resource/albums/57/photos"
The first is 200, the second 403.
Why it matters: details narrow what a token allows, and the API enforces them on each request while keeping its own checks, such as whether album 42 is Ava's.
Do today
Send the album request to today's tenant. Run step 3 with
&scope=photos.readadded to the URL (your tenant needs a scope to issue a token), approve and exchange the code:
jq 'has("authorization_details")' <<<"$RESP"
Run this before unset RESP. It prints false, and the consent screen showed only photos.read. Your tenant ignores authorization_details like any parameter it does not define. The client must notice that no details were granted and must not behave as if the token were limited to album 42: it can read the whole library.
Feel the scope-string workaround. In OAuth > Scopes, create the common scope
lab-tmp-album-42.readand assign it tolab-printer. Request it instead ofphotos.read. It works, but every album needs another scope, and the consent screen can only show that scope's fixed description. Imagine a second album, a different action, or the lesson's 42.50 EUR payment squeezed into a string such aspay:42.50:EUR, where a parsing mistake costs someone money.
A server must not advertise what it lacks. In OAuth > Metadata, add the custom member
{"authorization_details_types_supported": ["photo_album"]}and save. The change is refused, and Audit recordstenant.oauth.metadata.updaterejected.
Nothing else may fake it either. In OAuth > Access token managers, try to add a claim mapping named
authorization_detailsto any manager. The name is reserved, so the change is refused: only the authorization server, once it supports RAR, may set that claim from what Ava approved.
Break it
Planned: remove
lab-printerfrom thephoto_albumtype's allowed clients and repeat step 3. The listener receiveserror=invalid_authorization_detailswithstateandiss, and Audit recordsoauth.authorizerejectedinvalid_authorization_details(planned reason). Add the client back.
Why it matters: supporting a type is not the same as allowing every client to use it.
Check your work
There are no automated checks while this lab is planned. For Do today, look in Lab Photos' Audit for oauth.authorize with code_issued and oauth.token succeeded for lab-printer, tenant.oauth.scopes.create, and the rejected tenant.oauth.metadata.update and manager update.
Cleanup
Delete the scope lab-tmp-album-42.read and run unset TOKEN.
Missing infrastructure
G15 Rich Authorization Requests: parse and validate
authorization_detailsat the authorization and token endpoints against tenant-defined types (fields, kinds, allowed values, required fields, allowed clients), render the consent text, store the approval with the grant, and return it in the token response, the JWT claim, introspection andauthorization_details_types_supported.G3 Sample protected resource API: a hosted photo API with per-user albums that reads the claim. Until then,
btl-lab resourceon loopback can show the album check once tokens carry details.