OAUTH 2.0 · LAB
Audit your tenant and your clients against RFC 9700, row by row
Fill in the lesson's client, authorization server and resource server tables for your own tenant, with a real request or an earlier lab's Audit event as evidence for every row.
ReadyUses your lab tenant
The lesson
Builds on: Code interception and injection.
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.
Show a public client's request without PKCE is refused
Recorded as
oauth.authorizerejected (pkce_required) forlab-printer-app.Redeem a fresh code for the printer
Recorded as
oauth.tokensucceeded forlab-printerabout[email protected].Show a reused code is refused
Recorded as
oauth.tokenrejected (code_replayed) forlab-printer.
Request console
Requests in this lab can be sent from this page to your tenant: open one and choose Send. Fill in the values below first. They stay in this page's memory and are gone when you leave; secrets are never stored or sent anywhere except the request you send.
Setup
Choose Lab Photos as the lab tenant and press Start.
Copy the lesson's three tables into a document of your own and add an Evidence column. Evidence is a response you saw, an Audit or Logs entry with its request ID, or a reference to the lab that produced it.
Load
ISSUER,CLIENT_IDandCLIENT_SECRETforlab-printer,APP_IDforlab-printer-app, and the helpers from Present an access token correctly.
Walkthrough
Start from the metadata a client should configure itself from:
GET$ISSUER/.well-known/oauth-authorization-server
Open in console
GET $ISSUER/.well-known/oauth-authorization-server HTTP/1.1Record code_challenge_methods_supported (S256 only), authorization_response_iss_parameter_supported (true), grant_types_supported, response_types_supported and token_endpoint_auth_methods_supported. Compare grant_types_supported with what Flow policy allows today.
Why it matters: a client that reads endpoints and capabilities from metadata cannot be configured with a mistyped or attacker-supplied endpoint, and can confirm PKCE is enforced before relying on it.
Redirect rows: run one near miss from Redirect URI validation, for example the registered loopback address with a trailing slash. Record the
400, the absence of a redirect, and theinvalid_redirect_urisummary in Logs.
PKCE rows: send a request for the public client without a challenge.
eval "$(btl-lab state)"
echo "$ISSUER/oauth/authorize?response_type=code&client_id=$APP_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback&scope=photos.read&state=$STATE"
Open it with btl-lab callback running: the callback carries error=invalid_request, and Audit shows pkce_required. Cite the pkce_failed downgrade refusal from Request correlation and CSRF for the verifier-without-challenge row.
Code rows: get a code for
lab-printer(authorize "photos.read", sign in as Ava), redeem it withexchange, then send the same exchange request again. The second returnsinvalid_grant, Audit reasoncode_replayed. Record the code lifetime from Flow policy (300seconds).
Issuer row: in the
btl-lab callbackoutput from step 4, the response carriedissequal to your issuer.
Clickjacking row: read the headers on the authorization endpoint's own page.
curl -s -D - -o /dev/null "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback&scope=photos.read&state=x&code_challenge=$CHALLENGE&code_challenge_method=S256" \
| grep -i -E 'content-security-policy|x-frame-options'
Record both frame-ancestors 'none' in the Content Security Policy and X-Frame-Options: DENY, the combination the guidance asks for.
Redirect status row: in developer tools, Network, sign in through any authorization request and select the response to the sign-in form submission. Record
303 See Other, never307, so the password is not posted onward.
Grant rows: confirm Flow policy does not allow the implicit grant, then try the password grant:
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/token" -d grant_type=password -d [email protected] -d password=not-her-password | jq .
Returns 400 unsupported_grant_type. It appears in Logs only, because it is refused before the client is authenticated.
Refresh token rows: record each access token manager's Refresh token lifetime, Sign-in limit and reuse grace. Cite
refresh_replayedfrom Refresh token theft as evidence that public clients' refresh tokens rotate with reuse detection, and confirm every manager's reuse grace is0or a deliberate few seconds.
Client rows for your own code: cite the labs where you built each check, such as session-bound state in the Token Decoder, the issuer check in Authorization server mix-up, and the origin check before sending a token in Present an access token correctly.
Resource server rows: cite the
btl-lab resourcedecisions from the token labs: issuer, audience, type and algorithm checks, scope and object checks,client_only_token, and logs that never contain a token.
Rows this tenant cannot meet yet: sender-constrained access and refresh tokens (DPoP and mutual TLS) and pushed authorization requests. Record "not available" with the metadata evidence from step 1, where
dpop_signing_alg_values_supportedandpushed_authorization_request_endpointsupport are absent or marked not implemented.
Why it matters: the best practice is a checklist of defenses that earlier lessons explained one attack at a time. Writing evidence beside each row turns "we follow RFC 9700" into something a reviewer can check, and the empty rows say exactly which OAuth 2.1 direction remains.
Break it
None. This lab is an evidence review; every refusal it produces comes from an ordinary wrong request.
Check your work
Press Check my progress. The checks look for, in order: pkce_required for lab-printer-app, the printer's code exchange for Ava, and code_replayed for the same code.
Your table should have an evidence item in every row, with request IDs for the ones taken from Audit or Logs.
Cleanup
None. Keep your table; the Advanced OAuth labs fill in its empty rows.