OAUTH 2.0 · LAB
Compute, send and prove a PKCE verifier
Reproduce the lesson's challenge calculation, generate a real verifier, and see the token endpoint refuse every verifier that does not match the challenge bound to the code.
ReadyUses your lab tenant
The lesson
Builds on: Redirects and authorization codes.
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.
Redeem a code with its own verifier
Recorded as
oauth.tokensucceeded forlab-printerabout[email protected].A code swapped into another attempt is refused
Recorded as
oauth.tokenrejected (pkce_failed) forlab-printer.The refused attempt did not consume the code
Recorded as
oauth.tokensucceeded forlab-printer.Make PKCE optional for the printer
Recorded as
tenant.oauth.clients.updatesucceeded.A verifier for a code issued without PKCE is refused
Recorded as
oauth.tokenrejected (pkce_failed) forlab-printer.Require PKCE for the printer again
Recorded as
tenant.oauth.clients.updatesucceeded.
Setup
Set the
lab-printervariables and press Start on this page.Define the printer's request with the current challenge.
connect() { 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"; }
redeem() { curl -s -u "$CLIENT_ID:$CLIENT_SECRET" -d grant_type=authorization_code -d "code=$1" \
--data-urlencode "redirect_uri=$REDIRECT_URI" ${2:+-d "code_verifier=$2"} "$ISSUER/oauth/token" | jq -c '{error, error_description, scope}'; }
Walkthrough
Repeat the lesson's teaching calculation with its fixed verifier.
printf '%s' btl_training_verifier_for_one_attempt_only_2026 | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '='
It prints qd-75t-gnweZAhIl6REjxxgFnPXGwvqYcxq3vlsaJYQ, the lesson's challenge.
Why it matters: S256 is SHA-256 followed by Base64url without padding. It is a hash, so nothing can turn the challenge back into the verifier.
Generate a real pair and check the toolkit's challenge with the same calculation.
eval "$(btl-lab pkce)"; eval "$(btl-lab state)"
echo "${#VERIFIER} ${#CHALLENGE}"
printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=' ; echo; echo "$CHALLENGE"
The verifier comes from secure randomness and is between 43 and 128 characters. The challenge is always 43 characters, and the two printed challenges are identical.
Why it matters: the teaching value meets the length rule but is predictable. A real verifier must be unguessable, which only randomness provides.
Send only the challenge. Open the output of
connect, sign in as Ava, approve, andread -r CODE. The verifier never left your shell.
Prove possession:
redeem "$CODE" "$VERIFIER"returns ascopeand no error.
The lesson's code injection, with your own two attempts. Attempt A is Ava's in your normal window; attempt B is a second session in a private window.
Run
eval "$(btl-lab pkce)"; eval "$(btl-lab state)", keepA_VERIFIER=$VERIFIER, openconnectin your normal window, approve as Ava, and keep her code asA_CODE.Run
eval "$(btl-lab pkce)"; eval "$(btl-lab state)"again. This is attempt B, and$VERIFIERis now its verifier.Swap Ava's code into attempt B:
redeem "$A_CODE" "$VERIFIER". The answer isinvalid_grant, "code_verifier does not match the code_challenge sent in the authorization request."
Why it matters: an attacker who swaps a stolen code into their own session makes the printer redeem it with the attacker's attempt's verifier. The printer authenticates genuinely, yet the exchange fails, because the verifier does not match the challenge bound to that code.
Redeem Ava's code with its own verifier:
redeem "$A_CODE" "$A_VERIFIER"succeeds. The mismatched attempt in step 5 did not consume the code.
Leave the verifier out entirely on a fresh code: run step 3 again, then
redeem "$CODE". The answer isinvalid_grantfor the same reason. A code issued with a challenge cannot be redeemed without the proof.
Break it
Open OAuth > Clients > lab-printer, set PKCE for confidential clients to Optional, and save. Saving revokes the printer's tokens and codes.
Start an authorization without any challenge:
connect | sed 's/&code_challenge=.*//'. Approve as Ava andread -r CODE.Exchange it while still sending a verifier, as a client library that always adds one might:
redeem "$CODE" "$VERIFIER". The answer isinvalid_grantwith the same mismatch message. The server refuses a verifier for a code issued without a challenge, so PKCE cannot be half used.
Restore: in OAuth > Clients > lab-printer, set PKCE for confidential clients back to Required and save.
Check your work
Press Check my progress. All three refusals appear in Audit as oauth.token rejected pkce_failed for lab-printer.
Cleanup
Confirm
lab-printerhas PKCE for confidential clients set to Required.