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.
- G12 JAR
- G56 Client JWKS (`jwks` / `jwks_uri` per client)
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. You needopenssl3.x.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
Planned (G56): in OAuth > Clients > lab-printer > Keys, register
printer-2026-10.pub.pemwith key IDprinter-2026-10.Planned (G12): in OAuth > Flow policy, set Request objects to Allowed by value, with
jtitracking 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
Build a request object for
lab-printer: the claims from Build a request object by value and by reference, signed as ES256 withprinter-2026-10.pemand the header{"alg":"ES256","kid":"printer-2026-10","typ":"oauth-authz-req+jwt"}. Save it asOBJ, 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.
Open the Audit event for that authorization. It records the request object's
jti, its SHA-256 hash and thekidthat 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.
Give the object and
printer-2026-10.pub.pemto 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.
Present the same object again within its lifetime. With
jtitracking on, the tenant refuses it withinvalid_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.
Turn on Require signed request objects for
lab-printer, then open an ordinary unsigned authorization URL for it. The listener receiveserror=invalid_request.
Why it matters: a signature protects nothing while the server keeps accepting unsigned requests for the same client.
Do today
What the tenant's record proves. Run an ordinary code flow for
lab-printerand open theoauth.tokensucceeded 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.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.
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.
The tenant refuses rather than ignores. Send an authorization request that carries a
requestparameter. 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.
Confirm the same in metadata.
GET$ISSUER/.well-known/openid-configuration
Open in console
GET $ISSUER/.well-known/openid-configurationrequest_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
Keep
printer-2026-10.pemfor the next JAR labs. Deleterequest-42.jsonandrequest-42.sig.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,
jtitracking, maximum lifetime) and a per-client Require signed request objects setting. Audit should record each object'sjti, hash and verifyingkid.G56: per-client public keys (
jwksorjwks_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.