OAUTH 2.0 · LAB
Run one printer connection and label every message
Run the whole connection once as the printer's backend, keeping a pending transaction, then label each Audit event as a browser message or a direct one.
ReadyUses your lab tenant
The lesson
Builds on: Why use the authorization code flow?, Trust boundaries.
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.
Ben approves and the browser returns a code
Recorded as
oauth.authorizesucceeded (code_issued) forlab-printerabout[email protected].The printer's backend exchanges the code
Recorded as
oauth.tokensucceeded forlab-printerabout[email protected].The photo API checks the printer's token
Recorded as
oauth.introspectsucceeded (other_client_token_found) forlab-photo-apiabout[email protected].A second attempt overwrote the first one's verifier
Recorded as
oauth.tokenrejected (pkce_failed) forlab-printer.
Setup
Set the shell variables for
lab-printerand forlab-photo-api, which plays the photo API throughbtl-lab introspect. Then press Start on this page.
export ISSUER="https://tenant-<id>.beyondthelogin.dev"
export CLIENT_ID="<lab-printer client ID>"; read -rs CLIENT_SECRET
export API_ID="<lab-photo-api client ID>"; read -rs API_SECRET && export API_SECRET
export REDIRECT_URI="https://beyondthelogin.dev/lab/callback/"
enc() { jq -rn --arg v "$1" '$v|@uri'; }
Ben has not used the printer yet, so he will see a consent page.
Walkthrough
Prepare the connection. The printer reads its endpoints from trusted configuration once, not from anything that arrives with a browser request. Your shell session is the printer's pending transaction: it holds
stateand the PKCE verifier before the browser leaves.
TOKEN_ENDPOINT=$(curl -s "$ISSUER/.well-known/openid-configuration" | jq -r .token_endpoint)
eval "$(btl-lab pkce)"; eval "$(btl-lab state)"; EXPECTED_ISSUER="$ISSUER"
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&prompt=login"
Why it matters: the lesson's printer creates its pending transaction when you select Connect. Everything it needs to finish this one attempt exists before the photo service sees the request.
Visit the photo service: open the address in a private window.
prompt=loginforces the sign-in page. Sign in as Ben and approve on the consent page.
Return to the printer. Copy the three values from the callback, then check that the response belongs to this attempt.
read -r GOT_STATE; read -r GOT_ISS; read -r CODE
[ "$GOT_STATE" = "$STATE" ] && [ "$GOT_ISS" = "$EXPECTED_ISSUER" ] && echo "belongs to this attempt"
Finish the exchange at the token endpoint from step 1, with the printer's secret and this attempt's verifier.
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" "$TOKEN_ENDPOINT" | jq
Copy the access token: read -r TOKEN.
The photo API decides.
btl-lab introspect "$TOKEN"shows Ben'ssub, the printer'sclient_idandscope: "photos.read".
Open Audit and label each event from this run.
| Event | Carried by |
|---|---|
oauth.authorize request_started | the browser, arriving at the authorization endpoint |
oauth.authorize user_signed_in, actor Ben | the browser |
oauth.authorize code_issued | the browser, carrying the code back to the printer |
oauth.token succeeded, actor lab-printer | a direct request from the printer's backend to the token endpoint |
oauth.introspect other_client_token_found, actor lab-photo-api | a direct request from the API to the authorization server |
In Logs, oauth.authorize succeeded consent_required counted the consent page Ben saw.
Why it matters: following who sends each message is why OAuth uses separate endpoints. The browser carries the request and the code; the backend carries the secret, the verifier and the token.
Break it
Run step 1 and open the address in a tab, but do not approve yet. This is attempt A.
Select "Connect" again before finishing: run step 1 once more in the same terminal. Attempt B overwrites
STATEandVERIFIER.Go back to attempt A's tab, approve, copy its code into
CODE, and run step 4. The token endpoint answersinvalid_grantwith "code_verifier does not match the code_challenge sent in the authorization request."
One record per attempt keeps one tab from overwriting another's verifier. The Correlating requests and responses lab builds that.
Check your work
Press Check my progress. The Audit chain in step 6, all with subject Ben, is the evidence for the connection.
Cleanup
None. Errors and denied access repeats this exchange as the section's final run, with every check in place.