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

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.

  1. Read a redirect response that returns to the printer

    Recorded as oauth.authorize rejected (unauthorized_client) for lab-printer.

  2. The printer's code is refused for another client

    Recorded as oauth.token rejected (invalid_grant) for lab-printer-app.

  3. The printer redeems its own code

    Recorded as oauth.token succeeded for lab-printer.

  4. Shorten the authorization code lifetime

    Recorded as tenant.oauth.policy.update succeeded.

  5. An expired code is refused

    Recorded as oauth.token rejected (code_expired) for lab-printer.

Setup

  1. Set the lab-printer variables, plus APP_ID for lab-printer-app, and press Start on this page.

  2. Open OAuth > Flow policy and note the current Authorization code, seconds value (300 on a new tenant).

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

  1. Try return addresses. Open the output of try_redirect "<address>" for each row. lab-printer registers https://beyondthelogin.dev/lab/callback/ and http://127.0.0.1:8765/callback.

Requested redirect URIResult
https://beyondthelogin.dev/lab/callback (no trailing slash)tenant error page
https://beyondthelogin.dev/lab/Callback/tenant error page
https://beyondthelogin.dev/lab/callback/extratenant error page
http://localhost:8765/callbacktenant error page
http://127.0.0.1:9999/callbackaccepted: 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.

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

  1. Get a code: open try_redirect "$REDIRECT_URI", sign in as Ava, approve, and read -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.

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

  1. In OAuth > Flow policy, set Authorization code, seconds to 60 and 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 is invalid_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.

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

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

  1. Confirm Authorization code, seconds is back to its original value.

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