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

Keep a pending transaction per session and check who answered

Treat each terminal as one printer session, accept a callback only in the session that started it and only once, and stop when the response names an issuer you did not expect.

Partly readyIncludes a simulationUses both lab tenants

The lesson

Builds on: Proof Key for Code Exchange (PKCE).

New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.

Partly ready. Most of this lab runs today. Steps that wait on platform features are marked, and Missing infrastructure says what they need.

Needs a second tenant. This lab also uses Lab Mail, a second tenant. Additional tenants currently need a paid subscription or a BTL grant, so you may not be able to do the Lab Mail steps yet (gap G66).

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. Ava's attempt completes in her own session

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

  2. Ben's attempt completes in his own session

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

  3. A denial is correlated before it is reported

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

  4. A code from another session fails its PKCE check

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

Setup

  1. In your Lab Mail tenant, create a temporary client from Web application named lab-tmp-mail-printer, with the same redirect URI you use for lab-printer in Lab Photos. It plays the second photo service the printer imports from, the lesson's Pixel Vault.

  2. Press Start on this page (with Lab Photos as the lab tenant).

  3. You will use three terminals, A, B and C. Each one is a separate printer browser session. In each, set the lab-printer variables, ISSUER2 (Lab Mail's issuer) and MAIL_CLIENT_ID (the lab-tmp-mail-printer client ID), then define the printer's starter and callback handler.

start_attempt() {   # usage: start_attempt [issuer] [client ID]
  eval "$(btl-lab pkce)"; eval "$(btl-lab state)"; EXPECTED_ISSUER="${1:-$ISSUER}"; PENDING="$STATE"
  echo "$EXPECTED_ISSUER/oauth/authorize?response_type=code&client_id=${2:-$CLIENT_ID}&redirect_uri=$(enc "$REDIRECT_URI")&scope=photos.read&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"; }
handle_callback() {   # usage: handle_callback <state> <iss> <code, or error=...>
  [ -n "$PENDING" ] && [ "$1" = "$PENDING" ] || { echo "stop: no pending attempt with this state in this session"; return 1; }
  [ "$2" = "$EXPECTED_ISSUER" ] || { echo "stop: answered by '$2', expected $EXPECTED_ISSUER"; return 1; }
  PENDING=""   # claim the attempt once
  case "$3" in error=*) echo "not connected: ${3#error=}"; return 1;; esac
  CODE="$3"; echo "accepted"; }
redeem() { curl -s -u "$CLIENT_ID:$CLIENT_SECRET" -d grant_type=authorization_code -d "code=$CODE" \
  --data-urlencode "redirect_uri=$REDIRECT_URI" -d "code_verifier=$VERIFIER" "$ISSUER/oauth/token" | jq -c '{error, scope}'; }

Walkthrough

  1. Terminal A is your session. Run start_attempt, open the address in your normal window, sign in as Ava and approve. Keep the callback values, but do not hand them to the printer yet.

  1. Terminal B is a second session. Run start_attempt, open the address in a private window, sign in as Ben and approve.

  1. Hand Ben's callback to terminal A by mistake: handle_callback "<Ben's state>" "<iss>" "<Ben's code>". It prints "stop: no pending attempt with this state in this session".

Why it matters: state must locate a pending attempt bound to the session receiving the response. This binding is what stops the lesson's CSRF case, where someone else's code is pushed into your session.

  1. Hand each callback to its own terminal: both print "accepted". Run redeem in terminal A, then in terminal B. Both return a scope.

  1. Hand Ava's callback to terminal A a second time. It prints "stop", because the attempt was already claimed.

Why it matters: two callbacks must never both complete one pending attempt.

  1. Check who answered. In terminal C, start an attempt that expects Lab Mail, then send the browser to Lab Photos with the same state and challenge.

start_attempt "$ISSUER2" "$MAIL_CLIENT_ID" > /dev/null
echo "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$(enc "$REDIRECT_URI")&scope=photos.read&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"

Simulation. in the lesson, a malicious Pixel Vault forwards your browser to the genuine photo service. No lab server plays a malicious authorization server, so you open the forwarded address yourself. The approval, the response and its iss are real.

Open the printed address, approve as Ava, and hand the callback to terminal C. handle_callback prints "stop: answered by 'https://tenant-...' (Lab Photos), expected" Lab Mail's issuer. Try it once more with an empty second argument, as a response missing iss would arrive: it stops too.

Why it matters: the printer compares the returned iss exactly with the issuer saved for the attempt, stops before sending the code anywhere, and never uses a returned value to choose a token endpoint. That is the defense against authorization server mix-up, which PKCE does not provide.

  1. Error responses are correlated too. In terminal A, run start_attempt, choose Deny at consent, and pass the callback's state, iss and error=access_denied to handle_callback. It checks state and iss first, then prints "not connected: access_denied".

Break it

  1. In terminal A, run start_attempt and leave the address unopened. In terminal B, run start_attempt, approve as Ben, and keep his code.

  2. In terminal A, skip handle_callback: set CODE to Ben's code and run redeem. The token endpoint refuses it with invalid_grant, "code_verifier does not match the code_challenge sent in the authorization request."

PKCE gives a second, server-enforced layer for the same mistake. That is why current guidance lets a client rely on it in place of state once the server enforces PKCE. It does nothing for step 6.

Check your work

Press Check my progress. The five handle_callback outcomes from the walkthrough are your own evidence: a session mismatch, two acceptances, a second claim, an unexpected issuer, and a correlated error.

Cleanup

  1. Delete lab-tmp-mail-printer in your Lab Mail tenant.

Missing infrastructure

  • G66 Second lab tenant: this lab uses Lab Mail, a second tenant. Additional tenants currently need a paid subscription or a BTL grant, so an ordinary learner can do only the Lab Photos steps until every learner can have a second lab tenant.

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