OAUTH 2.0 · LAB
Register installations from a trusted software statement
Trust a software statement issuer and register installations whose name, redirect URI and scope come from the statement. Today, classify the claims and test a self-asserted name.
PlannedUses your lab tenant
The lesson
Builds on: Registering a client dynamically.
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.
- G9 Dynamic client registration and registration management
- G56 Client JWKS (`jwks` / `jwks_uri` per client)
Setup
Keep the shell variables and, once G9 exists, the
IATinitial access token from Register a review-site client through the registration API.Create the printing company's statement key, used for nothing else. This runs today.
openssl ecparam -name prime256v1 -genkey -noout -out printer-statements-2026.pem
openssl ec -in printer-statements-2026.pem -pubout -out printer-statements-2026.pub.pem
Planned (G9, G56): in OAuth > Registration > Trusted statement issuers, add the issuer
https://printer.examplewith the public keyprinter-statements-2026.pub.pem(key IDprinter-statements-2026) and the allowedsoftware_id7e3c1f52-9a4d-4b8e-a6f0-2d91c5e8b371.
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 the statement claims (this part runs today), then sign them as ES256 with
printer-statements-2026.pemand the header{"alg":"ES256","kid":"printer-statements-2026","typ":"JWT"}. Save the result asSS.
STATEMENT=$(jq -nc --argjson iat $(date +%s) '{iss:"https://printer.example", iat:$iat, software_id:"7e3c1f52-9a4d-4b8e-a6f0-2d91c5e8b371",
software_version:"4.2.0", client_name:"Photo Printer", client_uri:"https://printer.example",
redirect_uris:["http://127.0.0.1:8765/callback"], grant_types:["authorization_code","refresh_token"],
response_types:["code"], token_endpoint_auth_method:"none", scope:"photos.read"}')
echo "$STATEMENT" | jq .
Why it matters: "Software statements". A key the tenant already trusts vouches for these values, and that key signs statements and nothing else.
Register an installation with the statement plus a conflicting plain value.
curl -s "$ISSUER/oauth/register" -H "Authorization: Bearer $IAT" -H 'Content-Type: application/json' \
-d "{\"software_statement\":\"$SS\",\"client_name\":\"Something Else\"}" | jq '{client_id, client_name, software_id, redirect_uris}'
client_name is "Photo Printer".
Why it matters: where a trusted statement and the plain request disagree, the statement wins.
Register without a statement, with
client_urihttps://printer.exampleand a redirect URI on another host. The tenant flags the mismatch, or refuses it when Require matching hosts is on.
Why it matters: "Deciding what to accept". The tenant weighs the request as a whole, not field by field.
Register without
token_endpoint_auth_method. The response showsclient_secret_basicand includes a secret.
Why it matters: "What a request can say". Defaults can hand a client a credential it was never designed to hold, so a careful client states every field.
Do today
Run Planned walkthrough step 1's
jqcommand. For each claim, write one label: "decides where codes go", "decides how the client gets tokens", "decides how it authenticates", "shown to people" or "identifies the software". Mark which labels the lesson says are dangerous when self-asserted.Check the request as a whole, as the lesson suggests: compare the scheme and host of
client_uriwith each redirect URI.
echo "$STATEMENT" | jq -r '.client_uri, .redirect_uris[]' | sed -E 's#^([a-z]+://[^/]+).*#\1#'
The loopback redirect URI does not share the website's host. In a real registration, that would deserve a closer look.
See a self-asserted name on a real consent screen. In OAuth > Clients, create
lab-tmp-statementwith the Web application preset, redirect URIhttp://127.0.0.1:8765/callbackand scopeopenid. Run a code flow for it and look at the consent screen: it shows exactly the name typed into the form. Nothing checked it, which is why a server must treat a registration'sclient_nameas a claim unless something it trusts vouches for it.Confirm that the tenant assigns the client ID: the form has no field for choosing one.
Break it
These run once G9 and G56 exist.
Sign the statement with a key that is not registered for
https://printer.example. Expect400 invalid_software_statement.Sign a correct statement whose
issis an issuer the tenant has not added. Expect400 unapproved_software_statement.
Check your work
Today, Audit shows tenant.oauth.clients.create for lab-tmp-statement, its oauth.authorize events, and tenant.oauth.clients.delete after Cleanup. Once the gaps close, Audit shows oauth.register succeeded with the software_id and statement issuer recorded, and the two Break it refusals.
Cleanup
Delete
lab-tmp-statementin OAuth > Clients.Once the gaps close, remove the trusted statement issuer and the clients you registered.
Delete
printer-statements-2026.pemand its public file.
Missing infrastructure
G9: the registration endpoint, trusted statement issuers per tenant, per-field acceptance rules (accept, replace or reject), and a request-as-a-whole check such as Require matching hosts.
G56: public keys for statement issuers and for installations that register a key by value.
Once these exist and the toolkit can sign with the learner's own key, the Planned walkthrough runs as written.