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

OPENID CONNECT · LAB

Deliver a code to the wrong attempt and see PKCE, then the nonce, catch it

Hand a real code from Ava's attempt to a second attempt of your own, watch the token endpoint refuse it, then remove PKCE briefly and let the nonce check refuse the token.

ReadyUses your lab tenant

The lesson

Builds on: The authentication request.

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. The token endpoint refuses Ava's code redeemed for another attempt

    Recorded as oauth.token rejected (pkce_failed) for lab-collage about [email protected].

  2. Make PKCE optional on lab-collage for one step

    Recorded as tenant.oauth.clients.update succeeded.

  3. Redeem the misdelivered code without PKCE

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

  4. Require PKCE on lab-collage again

    Recorded as tenant.oauth.clients.update succeeded.

Setup

  1. You need lab-collage, Ava, Ben and the signin_url and exchange helpers from the authentication request lab. lab-collage requires PKCE.

  2. Keep btl-lab callback running in a second terminal. It exits after each callback, so start it again before each attempt.

  3. Press Start.

Note: both attempts in this lab are yours. A code issued for one of your attempts is delivered to another of your attempts, the situation the lesson describes, so you can see which check catches it. No token is altered.

Walkthrough

  1. Attempt A, Ava's. In a normal window run signin_url 'openid' and keep its values. Sign in as Ava and note the code from the listener, but do not redeem it.

A_STATE=$STATE; A_NONCE=$NONCE; A_VERIFIER=$VERIFIER
A_CODE='<code from the listener>'
  1. Attempt B, a different browser session. In a private window run signin_url 'openid' again and keep its values. Sign in as Ben. Ignore this callback.

B_STATE=$STATE; B_NONCE=$NONCE; B_VERIFIER=$VERIFIER
  1. The misdelivered code. Ava's code arrives at the callback together with attempt B's state. Your state check passes, because attempt B is a real pending attempt in that browser. Redeem the code with attempt B's verifier, as your backend would:

curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/token" -d grant_type=authorization_code --data-urlencode "code=$A_CODE" --data-urlencode "redirect_uri=http://127.0.0.1:8765/callback" --data-urlencode "code_verifier=$B_VERIFIER" | jq .

The tenant answers 400 with invalid_grant.

Why it matters: PKCE caught it at the token endpoint. Attempt B's verifier does not match the challenge in Ava's request, so the provider issues nothing.

  1. Take PKCE away for one step. In OAuth > Clients, set lab-collage PKCE to optional. Changing this setting ends the client's existing tokens and codes.

  1. Repeat steps 1 and 2 with fresh attempts that send no challenge. Build each request without the PKCE parameters:

eval "$(btl-lab state)"
echo "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback&scope=openid&state=$STATE&nonce=$NONCE"

Run it once as Ava in the normal window (keep A_NONCE=$NONCE and the code as A_CODE), then once as Ben in the private window (keep B_NONCE=$NONCE). Redeem Ava's code for attempt B, now with no verifier at all:

RESP=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/token" -d grant_type=authorization_code --data-urlencode "code=$A_CODE" --data-urlencode "redirect_uri=http://127.0.0.1:8765/callback")
ID_TOKEN=$(jq -r .id_token <<<"$RESP"); btl-lab decode "$ID_TOKEN"

Tokens are issued. The ID token names Ava's sub and carries attempt A's nonce.

  1. Now the relying party does what the lesson asks. Validate the token against the attempt it arrived for:

btl-lab verify "$ID_TOKEN" --issuer "$ISSUER" --audience "$CLIENT_ID" --type id --nonce "$B_NONCE"
btl-lab verify "$ID_TOKEN" --issuer "$ISSUER" --audience "$CLIENT_ID" --type id --nonce "$A_NONCE"

The first run passes the signature, issuer and audience checks and stops at the nonce check. The second shows that the token itself is genuine: it simply belongs to attempt A.

Why it matters: the nonce, chosen for one attempt and copied into a signed token, exposes the misdelivered code when PKCE is absent. Attempt B signs no one in, and the access token from this response is not used either.

  1. Restore PKCE now.

Restore: in OAuth > Clients, set lab-collage PKCE back to required. Confirm that a request without code_challenge is now refused at the authorization endpoint with error=pkce_required.

Why it matters: the two checks happen in different places, by different parties. With both in place, a mistake in one still leaves the sign-in protected.

Break it

  1. A request without a nonce. Run signin_url 'openid', delete &nonce=... from the printed URL, sign in and redeem. The tenant accepts the request, because the nonce is optional in the code flow, and the ID token has no nonce claim. Your rule: if your request sent a nonce and the token has none, that is a failure. Run btl-lab verify with your --nonce "$NONCE" and see it stop at the nonce check.

  1. One use only. Validate a fresh, accepted token, then mark its attempt claimed as in the complete sign-in lab. A second token presented for the same attempt finds nothing pending, so its nonce has nothing to match.

Check your work

Press Check my progress. The checks look for, in order: oauth.token rejected with pkce_failed for lab-collage, a tenant.oauth.clients.update, oauth.token succeeded for the code redeemed without PKCE, and a second tenant.oauth.clients.update.

Your nonce refusals in step 6 and Break it appear nowhere at the tenant: the provider cannot see a relying party's validation. Check in OAuth > Clients that lab-collage requires PKCE.

Cleanup

  1. Confirm lab-collage PKCE is required.

  2. Run unset A_STATE A_NONCE A_VERIFIER A_CODE B_STATE B_NONCE B_VERIFIER RESP ID_TOKEN.

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