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

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.

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. Use lab-printer and the shell variables from the PAR labs.

  2. In OAuth > Flow policy, make sure the response modes include query and form_post. On lab-printer, allow both modes too.

  3. For query responses, start btl-lab callback in a second terminal. For form_post responses you need to see the raw POST body, so run nc -l 8765 instead (on some Linux systems, nc -l -p 8765).

  4. Planned (G13): add the response modes query.jwt, form_post.jwt and jwt to the Flow policy and to lab-printer, and set the client's authorization response signing algorithm to ES256 with encryption off.

Planned walkthrough

  1. Run an ordinary code flow with no response_mode. The listener prints code, state and iss as separate plain parameters.

Why it matters: "An answer anyone can write". Nothing in these values shows who produced them.

  1. Run the flow again with &response_mode=jwt added. 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.

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

  1. Complete the flow: compare state with 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

  1. Run a code flow for lab-printer and record the callback from the listener: code, state and iss.

  2. Run another with &response_mode=form_post and nc -l 8765 listening. The tenant answers the browser with a page that posts itself, and nc prints the raw POST. Find code=...&state=...&iss=... in the body. Stop nc with Ctrl+C; the browser will show an error, which is expected.

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

  1. Ask for a signed response with &response_mode=jwt. The listener receives error=invalid_request with the description "response_mode must be query, fragment or form_post." Audit shows oauth.authorize rejected unsupported_response_mode.

  2. Confirm what the tenant offers.

GET$ISSUER/.well-known/oauth-authorization-server Open in console
GET $ISSUER/.well-known/oauth-authorization-server

response_modes_supported lists only plain modes, and there is no authorization_signing_alg_values_supported.

  1. 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 its state is not A's, and redeeming its code with A's verifier fails with invalid_grant (Audit oauth.token rejected pkce_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

  1. Stop nc and the listener.

  2. Revoke any access token you obtained with curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/revoke" -d token=<access_token>.

  3. Once G13 exists, remove the *.jwt modes from lab-printer if later labs should use plain responses.

Missing infrastructure

  • G13: JARM response modes (query.jwt, fragment.jwt, form_post.jwt, jwt), a per-client authorization_signed_response_alg, optional response encryption with a key the client registers (which also needs G56), and authorization_signing_alg_values_supported in discovery. Once these exist, the Planned walkthrough runs as written.

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