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

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.

  1. Show a public client's request without PKCE is refused

    Recorded as oauth.authorize rejected (pkce_required) for lab-printer-app.

  2. Redeem a fresh code for the printer

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

  3. Show a reused code is refused

    Recorded as oauth.token rejected (code_replayed) for lab-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

  1. Choose Lab Photos as the lab tenant and press Start.

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

  3. Load ISSUER, CLIENT_ID and CLIENT_SECRET for lab-printer, APP_ID for lab-printer-app, and the helpers from Present an access token correctly.

Walkthrough

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

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

  1. 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 the invalid_redirect_uri summary in Logs.

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

  1. Code rows: get a code for lab-printer (authorize "photos.read", sign in as Ava), redeem it with exchange, then send the same exchange request again. The second returns invalid_grant, Audit reason code_replayed. Record the code lifetime from Flow policy (300 seconds).

  1. Issuer row: in the btl-lab callback output from step 4, the response carried iss equal to your issuer.

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

  1. 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, never 307, so the password is not posted onward.

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

  1. Refresh token rows: record each access token manager's Refresh token lifetime, Sign-in limit and reuse grace. Cite refresh_replayed from Refresh token theft as evidence that public clients' refresh tokens rotate with reuse detection, and confirm every manager's reuse grace is 0 or a deliberate few seconds.

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

  1. Resource server rows: cite the btl-lab resource decisions from the token labs: issuer, audience, type and algorithm checks, scope and object checks, client_only_token, and logs that never contain a token.

  1. 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_supported and pushed_authorization_request_endpoint support 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.

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