OAUTH 2.0 · LAB
Write negative tests for your tenant, then plan the conformance suite
Run your own test plan of requests an honest client never sends, record PASS or FAIL for each module from real tenant answers and Audit reasons, and plan the official FAPI 2.0 test plan.
Partly readyUses your lab tenant
The lesson
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.
- G11 PAR
- G14 DPoP
- G8 Client authentication beyond `client_secret_basic` and `none`
- G3 Sample protected resource API; no RFC 9728 protected resource metadata
- G58 FAPI profile mode (incl. PS256/EdDSA keys)
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.
Complete a normal code flow with PKCE
Recorded as
oauth.tokensucceeded forlab-printer.Redeem the same code a second time
Recorded as
oauth.tokenrejected (code_replayed) forlab-printer.Redeem a code with another attempt's verifier
Recorded as
oauth.tokenrejected (pkce_failed) forlab-printer.Send an authorization request without PKCE
Recorded as
oauth.authorizerejected (pkce_required) forlab-printer.Redeem a code with a different redirect URI
Recorded as
oauth.tokenrejected (redirect_uri_mismatch) forlab-printer.Redeem a code after it expired
Recorded as
oauth.tokenrejected (code_expired) forlab-printer.
Setup
Use
lab-printerand the shell variables from the earlier labs, and startbtl-lab callbackbefore each sign-in. Make surelab-printerrequires PKCE and allowsopenidandphotos.read.Write down the current authorization code lifetime in OAuth > Flow policy, then set it to 60 seconds, the FAPI maximum.
Create a results file with one row per module: module, request, expected, observed, PASS or FAIL.
printf '| Module | Request | Expected | Observed | Result |\n|---|---|---|---|---|\n' > conformance.md
Walkthrough
Complete flow succeeds. Run a normal code flow for
lab-printerwithscope=openid photos.readand PKCE, and redeem the code.
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/token" -d grant_type=authorization_code \
-d code=$CODE --data-urlencode redirect_uri=$REDIRECT -d code_verifier=$VERIFIER | jq -r .access_token
Expect a token, and save it as TOKEN. Record PASS.
Why it matters: "Conformance testing". A positive baseline first, so each negative module has one cause.
Second use of the same code is rejected. Run the same command again with the same
CODE. Expect400 invalid_grant, and Auditoauth.tokenrejectedcode_replayed. Then call UserInfo withTOKEN:401, because a replayed code also revokes the tokens issued from it.
Why it matters: "Why a specification is not enough". This failure would be silent: no honest client ever sends a code twice.
Wrong verifier is rejected. Start two attempts in two shells, approve the second, and redeem its code with the first attempt's verifier. Expect
400 invalid_grant, reasonpkce_failed.Missing PKCE is rejected. Send an authorization request without
code_challenge.
curl -s -o /dev/null -w '%{redirect_url}\n' "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$(urlenc $REDIRECT)&scope=openid&state=conf-4"
The redirect carries error=invalid_request; Audit shows oauth.authorize rejected pkce_required.
Redirect URI mismatch is rejected. Get a new code and redeem it with
redirect_uri=http://127.0.0.1:8765/other. Expect400 invalid_grant, reasonredirect_uri_mismatch.Expired code is rejected. Get a new code, wait 70 seconds, then redeem it. Expect
400 invalid_grant, reasoncode_expired.Unregistered redirect shows an error page. Open an authorization request with
redirect_uri=https://printer.example/pay/callback. The tenant shows its own error page and sends nothing to the listener. Save a screenshot as evidence, the way the suite's REVIEW modules ask a person to.**
issis present.** Check that every callback in steps 1 to 5 carriedissequal to$ISSUER.Request objects and request URIs are refused, not ignored. Send
request_uri=urn:ietf:params:oauth:request_uri:demowith ordinary parameters, as in Redeem a pushed request URI in the browser, Do today step 2. Expecterror=request_uri_not_supported.Record which FAPI modules you could not run, and why: PAR, sender-constrained tokens and
private_key_jwt.
Why it matters: "What testing proves". Your results cover one configuration of one tenant at one moment, and this list says exactly what they do not cover.
Planned walkthrough
Once the tenant has PAR (G11), private_key_jwt (G8) or mutual TLS, DPoP (G14) or certificate binding, a protected resource the suite can call (G3), and FAPI profile mode (G58), run the OpenID Foundation FAPI 2.0 authorization server test plan, hosted or locally:
Register two temporary clients with different keys,
lab-tmp-fapi-1andlab-tmp-fapi-2, so the suite can try mixing them.Enter their client IDs, keys and the resource address into the suite's configuration, choosing
private_key_jwtand DPoP.Run the plan. Attach your step 7 style screenshots to the REVIEW modules.
Compare the suite's results with your own
conformance.md, and note any module the suite runs that your plan missed.
Break it
Weaken the configuration: set
lab-printer's PKCE policy to optional and run step 4 again. The request now succeeds, so that module FAILs. Record it.
Restore: set lab-printer's PKCE policy back to required and run step 4 once more. It PASSes again.
This is the lesson's point that a configuration change needs a new test run, ideally an automatic one.
Check your work
Press Check my progress. The checks look for, in order:
oauth.tokensucceeded forlab-printer(step 1)oauth.tokenrejectedcode_replayed(step 2)oauth.tokenrejectedpkce_failed(step 3)oauth.authorizerejectedpkce_required(step 4)oauth.tokenrejectedredirect_uri_mismatch(step 5)oauth.tokenrejectedcode_expired(step 6)
conformance.md should list each module with its observed result and Audit reason.
Cleanup
Set the authorization code lifetime back to the value you wrote down.
Revoke any remaining tokens with
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/revoke" -d token=<access_token>.Keep
conformance.mdso you can run the plan again after future tenant changes.
Missing infrastructure
G11, G8, G14, G3 and G58: the OpenID Foundation FAPI 2.0 test plan needs PAR,
private_key_jwtor mutual TLS, DPoP or certificate binding, a protected resource URL the suite can call, and a tenant that enforces the profile. Once they exist, the Planned walkthrough runs as written.