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

Compare a server's record with a signature anyone can verify

Contrast what the tenant can prove about a request from its own records with what a signature over the request proves to anyone holding the client's public key.

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. You need openssl 3.x.

  2. Generate the printer's signing key and its public half. This is real today and stays on your machine.

openssl ecparam -name prime256v1 -genkey -noout -out printer-2026-10.pem
openssl ec -in printer-2026-10.pem -pubout -out printer-2026-10.pub.pem
  1. Planned (G56): in OAuth > Clients > lab-printer > Keys, register printer-2026-10.pub.pem with key ID printer-2026-10.

  2. Planned (G12): in OAuth > Flow policy, set Request objects to Allowed by value, with jti tracking on.

Note: the lab toolkit has no command yet that signs a JWT with your own key. The planned steps say exactly what to sign; they need that command before they can run.

Planned walkthrough

  1. Build a request object for lab-printer: the claims from Build a request object by value and by reference, signed as ES256 with printer-2026-10.pem and the header {"alg":"ES256","kid":"printer-2026-10","typ":"oauth-authz-req+jwt"}. Save it as OBJ, then send it by value.

echo "$ISSUER/oauth/authorize?client_id=$CLIENT_ID&request=$OBJ"

Open the address and approve as Ava.

Why it matters: "A request that carries its own proof". The parameters now travel through the browser inside the client's signature.

  1. Open the Audit event for that authorization. It records the request object's jti, its SHA-256 hash and the kid that verified it, never the object's contents.

Why it matters: the tenant now keeps evidence that someone else can check, not just its own note that the client authenticated.

  1. Give the object and printer-2026-10.pub.pem to someone with no private key, or a second shell that has never seen it, and verify the signature there.

Why it matters: "Evidence that lasts". Verification needs nothing secret, so an auditor can confirm what the printer asked for months later.

  1. Present the same object again within its lifetime. With jti tracking on, the tenant refuses it with invalid_request_object.

Why it matters: "What a signature does not settle". A signature does not stop a copy from being presented again; expiry and jti tracking do.

  1. Turn on Require signed request objects for lab-printer, then open an ordinary unsigned authorization URL for it. The listener receives error=invalid_request.

Why it matters: a signature protects nothing while the server keeps accepting unsigned requests for the same client.

Do today

  1. What the tenant's record proves. Run an ordinary code flow for lab-printer and open the oauth.token succeeded event in Audit. It is the tenant's own statement that the client authenticated. Nothing in it lets an outsider check which parameters the client asked for.

  2. Non-repudiation, for real and offline. Write the lesson's Pay by bank request as a small document, sign it with the private key, and verify it with only the public key.

printf '{"iss":"%s","aud":"%s","client_id":"%s","amount":"42.50 EUR"}' "$CLIENT_ID" "$ISSUER" "$CLIENT_ID" > request-42.json
openssl dgst -sha256 -sign printer-2026-10.pem -out request-42.sig request-42.json
openssl dgst -sha256 -verify printer-2026-10.pub.pem -signature request-42.sig request-42.json

The last command prints Verified OK. Change the amount in request-42.json to 45.00 EUR and run the verify command again: it prints Verification failure. The signature covers exactly one set of values.

  1. A shared secret proves nothing to an outsider. Compute a keyed hash of the same document with the client secret.

openssl dgst -sha256 -hmac "$CLIENT_SECRET" request-42.json

The tenant knows this secret too, so it could have produced the same value. Only the asymmetric signature in step 2 points to the holder of one private key.

  1. The tenant refuses rather than ignores. Send an authorization request that carries a request parameter. The value here is a stand-in, because the tenant refuses on the parameter's presence, before reading it.

curl -s -o /dev/null -w '%{redirect_url}\n' "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$(urlenc $REDIRECT)&state=jar-1&request=demo-request-object"

The redirect carries error=request_not_supported, state=jar-1 and iss. Audit shows oauth.authorize rejected request_not_supported.

  1. Confirm the same in metadata.

GET$ISSUER/.well-known/openid-configuration Open in console
GET $ISSUER/.well-known/openid-configuration

request_parameter_supported is false. A server that silently ignored request and used the URL parameters would make signing pointless.

Check your work

Today, Audit shows oauth.token succeeded for lab-printer and oauth.authorize rejected request_not_supported, and your terminal shows Verified OK followed by Verification failure. Once G12 and G56 exist, Audit also shows oauth.authorize code_issued with the request object's jti and kid, a refusal of the replayed object, and a refusal of the unsigned request once signing is required.

Cleanup

  1. Keep printer-2026-10.pem for the next JAR labs. Delete request-42.json and request-42.sig.

  2. Once G12 exists, turn Require signed request objects off on lab-printer.

Missing infrastructure

  • G12: request object processing at the authorization endpoint, with a tenant policy (by value, jti tracking, maximum lifetime) and a per-client Require signed request objects setting. Audit should record each object's jti, hash and verifying kid.

  • G56: per-client public keys (jwks or jwks_uri), with key IDs and rotation overlap.

  • Once both exist and the toolkit can sign with the learner's own key, 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