OAUTH 2.0 · LAB
Test exact redirect matching and how the tenant handles codes
Find which return addresses the tenant accepts, read a real redirect response with its iss, and see that a code is opaque, bound to its client and short-lived.
ReadyUses your lab tenant
The lesson
Builds on: The authorization request.
New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.
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.
Sign in to start this lab and check your progress. Log in or create an account.
Read a redirect response that returns to the printer
Recorded as
oauth.authorizerejected (unauthorized_client) forlab-printer.The printer's code is refused for another client
Recorded as
oauth.tokenrejected (invalid_grant) forlab-printer-app.The printer redeems its own code
Recorded as
oauth.tokensucceeded forlab-printer.Shorten the authorization code lifetime
Recorded as
tenant.oauth.policy.updatesucceeded.An expired code is refused
Recorded as
oauth.tokenrejected (code_expired) forlab-printer.
Setup
Set the
lab-printervariables, plusAPP_IDforlab-printer-app, and press Start on this page.Open OAuth > Flow policy and note the current Authorization code, seconds value (300 on a new tenant).
Define a request that changes only the return address.
eval "$(btl-lab pkce)"; eval "$(btl-lab state)"
try_redirect() { echo "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$(enc "$1")&scope=photos.read&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"; }
Walkthrough
Try return addresses. Open the output of
try_redirect "<address>"for each row.lab-printerregistershttps://beyondthelogin.dev/lab/callback/andhttp://127.0.0.1:8765/callback.
| Requested redirect URI | Result |
|---|---|
https://beyondthelogin.dev/lab/callback (no trailing slash) | tenant error page |
https://beyondthelogin.dev/lab/Callback/ | tenant error page |
https://beyondthelogin.dev/lab/callback/extra | tenant error page |
http://localhost:8765/callback | tenant error page |
http://127.0.0.1:9999/callback | accepted: the sign-in page appears |
Why it matters: for a web client, matching is exact. A different path, a changed letter or a trailing slash makes a different address. The only variation allowed is the port of a loopback address, which installed apps need.
Read a redirect response without a browser, using a request that fails after the client and redirect URI checks pass.
curl -s -i "$ISSUER/oauth/authorize?response_type=token&client_id=$CLIENT_ID&redirect_uri=$(enc "$REDIRECT_URI")&state=demo-attempt-7"
The answer is 303 See Other with Cache-Control: no-store and a Location header holding your callback address with error=unauthorized_client, error_description, state=demo-attempt-7 and iss. For a successful run, open your browser's developer tools on the Network tab with Preserve log, complete a sign-in, and find the same 303 with code, state and iss.
Why it matters: iss names the authorization server that answered, on success and on error. The lesson shows a 302; any redirect status works the same way for the browser.
Get a code: open
try_redirect "$REDIRECT_URI", sign in as Ava, approve, andread -r CODE. Look at it.
echo "${#CODE}"
btl-lab decode "$CODE" # reports that this is not a JWT
Why it matters: the code is a random value with nothing to decode. The printer treats it as opaque and presents it, rather than reading an account or permissions from it.
Present the printer's code as the printer's public app, an easy mistake when two clients share a callback.
curl -s -d grant_type=authorization_code -d "code=$CODE" --data-urlencode "redirect_uri=$REDIRECT_URI" \
-d "code_verifier=$VERIFIER" -d "client_id=$APP_ID" "$ISSUER/oauth/token" | jq
The answer is invalid_grant, "The authorization code or refresh token is invalid or was issued to another client." Now exchange it as the printer, with -u "$CLIENT_ID:$CLIENT_SECRET" instead of -d "client_id=$APP_ID": 200.
Why it matters: the code is associated with the client it was issued to. Holding it is not enough for any other client.
In OAuth > Flow policy, set Authorization code, seconds to
60and save. Get a new code (eval "$(btl-lab pkce)"; eval "$(btl-lab state)", then step 3), wait 70 seconds, and exchange it as the printer. The answer isinvalid_grant, "The authorization code has expired. Start a new authorization request."
Why it matters: a short lifetime limits how long a leaked callback URL is worth anything, whether or not anyone tried to use it.
Restore: in Flow policy, set Authorization code, seconds back to the value you noted in Setup and save.
Where codes leak. If you used the loopback listener, its output holds the full callback query, code included. That is exactly what a real callback handler must keep out of application logs. The hosted callback page displays the values but never stores or sends them.
Break it
Present a code from step 5's expired attempt again, or any code a second time after a successful exchange. Neither can be redeemed. The Errors and denied access lab shows what a second use of a valid code does to the tokens already issued from it.
Check your work
Press Check my progress. Logs also has oauth.authorize rejected invalid_redirect_uri rows for the refused addresses in step 1. They stop on the tenant's error page, so they are not in Audit.
Cleanup
Confirm Authorization code, seconds is back to its original value.