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.
- G66 Second lab tenant for every learner: additional tenants need a paid subscription or a BTL grant, so labs that use Lab Mail cannot be completed by an ordinary learner yet
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.
Sign in to start this lab and check your progress. Log in or create an account.
Ava's attempt completes in her own session
Recorded as
oauth.tokensucceeded forlab-printerabout[email protected].Ben's attempt completes in his own session
Recorded as
oauth.tokensucceeded forlab-printerabout[email protected].A denial is correlated before it is reported
Recorded as
oauth.authorizerejected (access_denied) forlab-printer.A code from another session fails its PKCE check
Recorded as
oauth.tokenrejected (pkce_failed) forlab-printer.
Setup
In your Lab Mail tenant, create a temporary client from Web application named
lab-tmp-mail-printer, with the same redirect URI you use forlab-printerin Lab Photos. It plays the second photo service the printer imports from, the lesson's Pixel Vault.Press Start on this page (with Lab Photos as the lab tenant).
You will use three terminals, A, B and C. Each one is a separate printer browser session. In each, set the
lab-printervariables,ISSUER2(Lab Mail's issuer) andMAIL_CLIENT_ID(thelab-tmp-mail-printerclient 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
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.
Terminal B is a second session. Run
start_attempt, open the address in a private window, sign in as Ben and approve.
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.
Hand each callback to its own terminal: both print "accepted". Run
redeemin terminal A, then in terminal B. Both return ascope.
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.
Check who answered. In terminal C, start an attempt that expects Lab Mail, then send the browser to Lab Photos with the same
stateand 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.
Error responses are correlated too. In terminal A, run
start_attempt, choose Deny at consent, and pass the callback'sstate,issanderror=access_deniedtohandle_callback. It checksstateandissfirst, then prints "not connected: access_denied".
Break it
In terminal A, run
start_attemptand leave the address unopened. In terminal B, runstart_attempt, approve as Ben, and keep his code.In terminal A, skip
handle_callback: setCODEto Ben's code and runredeem. The token endpoint refuses it withinvalid_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
Delete
lab-tmp-mail-printerin 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.