OAUTH 2.0 · LAB
Read a plain authorization response, then plan a signed one
Capture lab-printer's authorization response in query and form_post modes, list what the client can and cannot conclude from it, and see where a signed JARM response would add assurance.
PlannedUses your lab tenant
The lesson
Builds on: The problem PAR solves.
New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.
Planned. The core of this lab waits on platform features that are not built yet. The planned walkthrough shows exactly how it will run; Do today is a real exercise you can do now.
- G13 JARM
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
Use
lab-printerand the shell variables from the PAR labs.In OAuth > Flow policy, make sure the response modes include
queryandform_post. Onlab-printer, allow both modes too.For
queryresponses, startbtl-lab callbackin a second terminal. Forform_postresponses you need to see the raw POST body, so runnc -l 8765instead (on some Linux systems,nc -l -p 8765).Planned (G13): add the response modes
query.jwt,form_post.jwtandjwtto the Flow policy and tolab-printer, and set the client's authorization response signing algorithm toES256with encryption off.
Planned walkthrough
Run an ordinary code flow with no
response_mode. The listener printscode,stateandissas separate plain parameters.
Why it matters: "An answer anyone can write". Nothing in these values shows who produced them.
Run the flow again with
&response_mode=jwtadded. The callback carries one parameter,response. Decode it.
btl-lab decode "$RESPONSE"
The claims are iss (your issuer), aud ($CLIENT_ID), exp about five minutes ahead, code and state.
Why it matters: "Signing the response". The same values now sit inside a signature, with an audience and an expiry.
Validate it against the tenant's keys rather than just decoding it.
btl-lab verify "$RESPONSE" --issuer "$ISSUER" --audience "$CLIENT_ID" --type jarm
Why it matters: the four assurances in the lesson, unaltered, from the tenant, for this client, issuer named inside the signature, come from this check, never from decoding.
Complete the flow: compare
statewith your pending attempt, then exchange the code with the verifier.
Why it matters: "What a signature cannot tell". JARM adds checks and replaces none. State and PKCE still tie the response to this browser session.
Do today
Run a code flow for
lab-printerand record the callback from the listener:code,stateandiss.Run another with
&response_mode=form_postandnc -l 8765listening. The tenant answers the browser with a page that posts itself, andncprints the raw POST. Findcode=...&state=...&iss=...in the body. Stopncwith Ctrl+C; the browser will show an error, which is expected.For each value, write what your client can check on its own:
state: equals the value in your shell, which ties the response to your pending attempt.iss: equals$ISSUER, which names the server that is supposed to have answered.code: can only be checked by redeeming it with your verifier.
None of these proves that the tenant produced the response as a whole, or that it was meant for your client. Even iss is text the response says about itself.
Ask for a signed response with
&response_mode=jwt. The listener receiveserror=invalid_requestwith the description "response_mode must be query, fragment or form_post." Audit showsoauth.authorizerejectedunsupported_response_mode.Confirm what the tenant offers.
GET$ISSUER/.well-known/oauth-authorization-server
Open in console
GET $ISSUER/.well-known/oauth-authorization-serverresponse_modes_supported lists only plain modes, and there is no authorization_signing_alg_values_supported.
Show that a genuine response can still be the wrong one. Start an attempt in shell A (
btl-lab pkce,btl-lab state), then start and approve a separate attempt from shell B. The response B produces is genuine, from your tenant, for your client, and unexpired, yet itsstateis not A's, and redeeming its code with A's verifier fails withinvalid_grant(Auditoauth.tokenrejectedpkce_failed). A signature would not change that outcome.
Break it
Once G13 exists, repeat Do today step 6 with response_mode=jwt. The second response verifies, is addressed to your client and has not expired, and your client must still refuse it because its state is not the pending one. The signature cannot tell which session a response belongs to.
Check your work
Today, Audit shows oauth.authorize rejected unsupported_response_mode and oauth.token rejected pkce_failed for lab-printer. Once G13 exists, Audit also shows oauth.authorize code_issued with a JWT response mode.
Cleanup
Stop
ncand the listener.Revoke any access token you obtained with
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/revoke" -d token=<access_token>.Once G13 exists, remove the
*.jwtmodes fromlab-printerif later labs should use plain responses.
Missing infrastructure
G13: JARM response modes (
query.jwt,fragment.jwt,form_post.jwt,jwt), a per-clientauthorization_signed_response_alg, optional response encryption with a key the client registers (which also needs G56), andauthorization_signing_alg_values_supportedin discovery. Once these exist, the Planned walkthrough runs as written.