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

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.

Setup

  1. Use lab-printer, lab-printer-app and the shell variables from Push an authorization request from the back end.

  2. Start btl-lab callback in a second terminal before each attempt.

  3. Planned (G11): in OAuth > Flow policy, keep pushed authorization requests Allowed with a request URI lifetime of 60 seconds.

Planned walkthrough

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

  1. Open the address and sign in as Ava. The consent screen lists photos.read, built from the stored request. Approve. The listener prints code, your state and iss.

Why it matters: the response leg is unchanged by PAR. It still comes back through the browser.

  1. Check state and iss yourself, then exchange the code. Repeat redirect_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.

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

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

  1. Push as lab-printer-app (no credential, -d client_id=$APP_ID), then open the address with client_id=$CLIENT_ID instead 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.

  1. Push for lab-printer, then in OAuth > Clients remove photos.read from lab-printer and save. Now open the address. The tenant re-checks the stored request against the current registration and sends error=invalid_scope to 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

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

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

  1. See why every pushed request still needs state and PKCE, with two ordinary requests. In shell A, run btl-lab pkce and btl-lab state and 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.

  2. Back in shell A, compare the callback's state with A's STATE. 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

  1. Revoke any access token you obtained.

curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/revoke" -d token=<access_token>
  1. Confirm that photos.read is allowed on lab-printer again.

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_id matches the pushing client, and re-validate the stored parameters against the client's current settings. Once it does, the Planned walkthrough runs as written.

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