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

Probe exact redirect URI matching with near-miss addresses

Register one exact HTTPS callback, send authorization requests with the lesson's near-miss variants, confirm none redirects, and see the token endpoint check the address again.

ReadyUses your lab tenant

The lesson

Builds on: Public and confidential clients.

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. Register the hosted callback on lab-printer

    Recorded as tenant.oauth.clients.update succeeded.

  2. Get a real code at the loopback callback

    Recorded as oauth.authorize succeeded (code_issued) for lab-printer.

  3. Redeem it with a different redirect URI

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

  4. Redeem it with the registered redirect URI

    Recorded as oauth.token succeeded for lab-printer about [email protected].

Setup

  1. Choose Lab Photos as the lab tenant and press Start.

  2. In Clients, lab-printer, add the redirect URI https://beyondthelogin.dev/lab/callback/ beside http://127.0.0.1:8765/callback and save. This is the hosted lab callback page: it only displays what it receives and never exchanges or stores it.

  3. Load ISSUER, CLIENT_ID and CLIENT_SECRET for lab-printer, then run eval "$(btl-lab pkce)" so CHALLENGE is set.

  4. Define a probe that sends an authorization request with a given redirect URI and shows the status and any redirect target without following it:

probe() {
  curl -s -o /dev/null -w "%{http_code} %{redirect_url}  <- $1\n" "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&scope=photos.read&state=probe&code_challenge=$CHALLENGE&code_challenge_method=S256&redirect_uri=$(jq -rn --arg v "$1" '$v|@uri')"
}

Walkthrough

  1. Probe the registered address: probe https://beyondthelogin.dev/lab/callback/. Returns 200, the tenant's sign-in page, because curl has no session. No redirect target.

Why it matters: this is the baseline. Only after the client and the exact address are verified does the tenant show anything.

  1. Probe each near miss from the lesson's table, rewritten for this host:

for uri in \
  "https://beyondthelogin.dev.attacker.example/lab/callback/" \
  "https://beyondthelogin.dev/lab/callback/../../go" \
  "https://beyondthelogin.dev/lab/callback/%2e%2e/%2e%2e/go" \
  "https://beyondthelogin.dev/lab/callback/preview" \
  "https://beyondthelogin.dev/lab/callback/?next=https://attacker.example/" \
  'https://attacker.example\.beyondthelogin.dev/lab/callback/' \
  "https://beyondthelogin.dev/LAB/CALLBACK/" \
  "http://beyondthelogin.dev:8080/lab/callback/"; do probe "$uri"; done

Every line shows 400 and an empty redirect target. Open any one of them in a browser: the tenant's own page says the return address is not registered.

Why it matters: each shortcut in the lesson accepted one of these and sent a code to an address the client never registered. Exact comparison never has to decide what two parsers meant, so even a harmless letter-case variant is refused.

  1. Probe the loopback rules. The registered value is http://127.0.0.1:8765/callback:

probe http://127.0.0.1:53682/callback     # 200: only the loopback port may vary
probe http://127.0.0.1:8765/callback/     # 400: the path is still exact
probe http://localhost:8765/callback      # 400: localhost is not the literal loopback address

Why it matters: the one flexibility current guidance allows is a native app's loopback port. It does not extend to any other part of the address or any other host.

  1. Try to register addresses the guidance forbids. In Clients, lab-printer, add each in turn and save: https://*.beyondthelogin.dev/lab/callback/, http://beyondthelogin.dev/lab/callback/. Each is refused at save time, and nothing is stored.

Why it matters: a wildcard would send codes to any subdomain, including one whose DNS record points at a service the owner stopped using, and plain HTTP outside loopback sends codes unencrypted.

  1. Show that the token endpoint checks again. Run btl-lab callback in a second terminal, build a real request with authorize "photos.read" (from Present an access token correctly), sign in as Ava and copy the code into CODE. Redeem it with a trailing slash:

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/" -d "code_verifier=$VERIFIER" | jq .

Returns invalid_grant, Audit reason redirect_uri_mismatch. Repeat with redirect_uri=http://127.0.0.1:8765/callback: it succeeds.

Why it matters: the server stores the redirect URI with the code and checks the token request against it, so a code delivered somewhere unexpected cannot be redeemed as though it arrived at the registered address.

  1. Inspect the client's half of the work on a well-behaved callback page:

curl -sI "$ISSUER/token-decoder" | grep -i -E 'referrer-policy|cache-control|content-security-policy'

Note Referrer-Policy: no-referrer and Cache-Control: no-store.

Why it matters: the authorization server decides where a code can go; the client decides whether it stays there. A callback loads nothing from other sites, sends no referrer, and moves to a clean address straight away.

Break it

Steps 2, 3 and 5 are the failure cases. Nothing in the tenant is weakened: the probes send ordinary authorization requests with wrong addresses, and the tenant refuses each one on its own page.

Check your work

Press Check my progress. The checks look for, in order: the tenant.oauth.clients.update that registered the hosted callback, oauth.authorize with code_issued for lab-printer, the redirect_uri_mismatch refusal, and the successful exchange with the registered address.

The near-miss probes appear in Logs, as oauth.authorize rejected with invalid_redirect_uri, counted once per probe. They are not in Audit, because the request never reached a verified client and address.

Cleanup

Keep https://beyondthelogin.dev/lab/callback/ on lab-printer if you want to use the hosted callback in later labs; otherwise remove it. Run unset CODE.

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