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

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.

  1. Redeem a code with its own verifier

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

  2. A code swapped into another attempt is refused

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

  3. The refused attempt did not consume the code

    Recorded as oauth.token succeeded for lab-printer.

  4. Make PKCE optional for the printer

    Recorded as tenant.oauth.clients.update succeeded.

  5. A verifier for a code issued without PKCE is refused

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

  6. Require PKCE for the printer again

    Recorded as tenant.oauth.clients.update succeeded.

Setup

  1. Set the lab-printer variables and press Start on this page.

  2. 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

  1. 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.

  1. 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.

  1. Send only the challenge. Open the output of connect, sign in as Ava, approve, and read -r CODE. The verifier never left your shell.

  1. Prove possession: redeem "$CODE" "$VERIFIER" returns a scope and no error.

  1. 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.

    1. Run eval "$(btl-lab pkce)"; eval "$(btl-lab state)", keep A_VERIFIER=$VERIFIER, open connect in your normal window, approve as Ava, and keep her code as A_CODE.

    2. Run eval "$(btl-lab pkce)"; eval "$(btl-lab state)" again. This is attempt B, and $VERIFIER is now its verifier.

    3. Swap Ava's code into attempt B: redeem "$A_CODE" "$VERIFIER". The answer is invalid_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.

  1. 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.

  1. Leave the verifier out entirely on a fresh code: run step 3 again, then redeem "$CODE". The answer is invalid_grant for the same reason. A code issued with a challenge cannot be redeemed without the proof.

Break it

  1. Open OAuth > Clients > lab-printer, set PKCE for confidential clients to Optional, and save. Saving revokes the printer's tokens and codes.

  2. Start an authorization without any challenge: connect | sed 's/&code_challenge=.*//'. Approve as Ava and read -r CODE.

  3. Exchange it while still sending a verifier, as a client library that always adds one might: redeem "$CODE" "$VERIFIER". The answer is invalid_grant with 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

  1. Confirm lab-printer has PKCE for confidential clients set to Required.

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