OAUTH 2.0 · LAB
Redeem a pushed request URI in the browser
Complete a code flow where the browser carries only client_id and request_uri, then show that the reference is single use, short lived and bound to the client that pushed it.
PlannedUses your lab tenant
The lesson
Builds on: Pushing an authorization request.
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.
- G11 PAR
Setup
Use
lab-printer,lab-printer-appand the shell variables from Push an authorization request from the back end.Start
btl-lab callbackin a second terminal before each attempt.Planned (G11): in OAuth > Flow policy, keep pushed authorization requests Allowed with a request URI lifetime of 60 seconds.
Planned walkthrough
Push a request as in the previous lab and save
REQUEST_URI. Then build the browser address.
echo "$ISSUER/oauth/authorize?client_id=$CLIENT_ID&request_uri=$(urlenc "$REQUEST_URI")"
Why it matters: this is "Sending the browser". There is no scope, return address, state or challenge in the URL, only the client and an encoded reference.
Open the address and sign in as Ava. The consent screen lists
photos.read, built from the stored request. Approve. The listener printscode, yourstateandiss.
Why it matters: the response leg is unchanged by PAR. It still comes back through the browser.
Check
stateandissyourself, then exchange the code. Repeatredirect_uri, because the pushed request contained it.
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/token" -d grant_type=authorization_code \
-d code=<code> --data-urlencode redirect_uri=$REDIRECT -d code_verifier=$VERIFIER | jq '{token_type, scope}'
Why it matters: nothing in the code exchange changes.
Open the step 1 address again. Expect the tenant's own error page with
invalid_request_uri, and nothing at the listener.
Why it matters: single use. A copy from history or a log is worth nothing once the reference has been used.
Push again, wait 70 seconds, then open the new address. Expect the same error page.
Why it matters: a short life. A copy found later has already expired.
Push as
lab-printer-app(no credential,-d client_id=$APP_ID), then open the address withclient_id=$CLIENT_IDinstead of$APP_ID. Expect the error page.
Why it matters: the binding check refuses a reference pushed by one client and presented under another client's name, the lesson's attacker-registered-client case.
Push for
lab-printer, then in OAuth > Clients removephotos.readfromlab-printerand save. Now open the address. The tenant re-checks the stored request against the current registration and sendserror=invalid_scopeto the listener.
Restore: add photos.read back to lab-printer and save.
Why it matters: "Something can change in 90 seconds". A stored request is processed against the client's settings at the moment the browser arrives.
Do today
Present only a reference, as a PAR client would.
curl -s -o /dev/null -w '%{http_code}\n' "$ISSUER/oauth/authorize?client_id=$CLIENT_ID&request_uri=$(urlenc urn:ietf:params:oauth:request_uri:demo)"
The tenant answers with its own error page (invalid_redirect_uri). With no stored request it has no validated return address, so it cannot redirect. This is the lesson's "shows its own error page" case.
Add the ordinary parameters, so the tenant can trust the return address.
curl -s -o /dev/null -w '%{redirect_url}\n' "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$(urlenc $REDIRECT)&state=demo-1&request_uri=$(urlenc urn:ietf:params:oauth:request_uri:demo)"
The redirect carries error=request_uri_not_supported, state=demo-1 and iss. Audit shows oauth.authorize rejected request_uri_not_supported. The tenant refuses the reference explicitly rather than ignoring it and carrying on with the URL parameters.
See why every pushed request still needs
stateand PKCE, with two ordinary requests. In shell A, runbtl-lab pkceandbtl-lab stateand keep the values as your pending attempt. In shell B, do the same, build the authorization URL from B's values, open it and approve as Ava.Back in shell A, compare the callback's
statewith A'sSTATE. They differ, so a correct client refuses the callback. Confirm that the tenant enforces the same rule by redeeming B's code with A's verifier.
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/token" -d grant_type=authorization_code \
-d code=<code from B> --data-urlencode redirect_uri=$REDIRECT -d code_verifier=$VERIFIER | jq .
The answer is 400 invalid_grant, and Audit shows oauth.token rejected pkce_failed. A swapped request URI would fail the same way, because the stored request carries the other attempt's state and challenge.
Break it
Once G11 exists, swap references between two pending pushes: push A and B with different state values, keep A as the pending attempt, then open B's reference and approve. Your client must refuse the callback because its state is not A's, and redeeming B's code with A's verifier fails with pkce_failed, as in Do today step 4.
Check your work
Today, Audit shows oauth.authorize rejected request_uri_not_supported and oauth.token rejected pkce_failed. Once G11 exists, Audit also shows oauth.authorize code_issued for a request that began with request_uri, and refusals of the reused, expired and wrongly bound references.
Cleanup
Revoke any access token you obtained.
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/revoke" -d token=<access_token>
Confirm that
photos.readis allowed onlab-printeragain.
Missing infrastructure
G11: the authorization endpoint must look up stored requests by reference, enforce expiry, mark a reference used when the authorization completes (so a link preview does not consume it), check that
client_idmatches the pushing client, and re-validate the stored parameters against the client's current settings. Once it does, the Planned walkthrough runs as written.