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

Push a signed request object with a client assertion

Push lab-printer's signed request object to the PAR endpoint, authenticated by a client assertion from the same key, and see the tenant keep the two JWTs apart.

PlannedUses your lab tenant

The lesson

Builds on: Validating a request object.

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.

Setup

  1. Keep printer-2026-10.pem, the b64url helper and the shell variables from the earlier JAR labs.

  2. Planned (G11, G12): in OAuth > Flow policy, allow pushed authorization requests and allow request objects through PAR.

  3. Planned (G8, G56): lab-printer uses private_key_jwt with the registered key printer-2026-10.

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 and sign the request object (OBJ) as in Build a request object and send it by value or by reference, and a fresh client assertion (ASSERTION) with aud set to $ISSUER.

  2. Push only the object and the client authentication.

curl -s "$ISSUER/oauth/par" -d client_id=$CLIENT_ID \
  -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer -d client_assertion=$ASSERTION \
  -d request=$OBJ | jq .

Expect 201 with request_uri and expires_in.

Why it matters: "Pushing a signed request". Apart from request, the body carries only what client authentication needs. Every authorization parameter is inside the signed object.

  1. Open $ISSUER/oauth/authorize?client_id=$CLIENT_ID&request_uri=<url-encoded request_uri>, approve as Ava, and exchange the code with a new assertion.

Why it matters: from here it is the ordinary PAR flow, with the browser carrying only a reference.

  1. Decode both JWTs with btl-lab decode and fill in the lesson's comparison table: typ, iss and sub, aud, lifetime.

Why it matters: "What the server checks". One key signs both, so only the claims and type keep each from passing for the other.

  1. Open the oauth.par event in Audit. It records the authentication method private_key_jwt and the object's jti, and that the authenticated client matched the client_id claim inside the object.

Why it matters: the tenant now holds signed evidence of the exact request, the property PAR alone cannot give.

Do today

  1. Build both claim sets, without signing, and compare them.

NOW=$(date +%s)
jq -nc --arg c "$CLIENT_ID" --arg a "$ISSUER" --argjson n $NOW --arg j "$(openssl rand 16 | b64url)" \
  '{iss:$c, sub:$c, aud:$a, iat:$n, exp:($n+60), jti:$j}' | jq . > assertion-claims.json
echo "$CLAIMS" | jq . > request-claims.json
diff assertion-claims.json request-claims.json

Confirm that the request object claims have no sub, and write down the typ each would carry: JWT (or client-authentication+jwt) for the assertion, oauth-authz-req+jwt for the object.

  1. Push today with both parameters.

curl -si "$ISSUER/oauth/par" -d client_id=$CLIENT_ID \
  -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer -d client_assertion=demo-assertion \
  -d request=demo-request-object

The answer is 501 temporarily_unavailable, and Logs show oauth.par rejected not_implemented.

  1. Send the same client assertion parameters to the token endpoint.

curl -s "$ISSUER/oauth/token" -d grant_type=client_credentials -d client_id=$CLIENT_ID \
  -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer -d client_assertion=demo-assertion | jq .

The answer is 401 invalid_client, and Audit shows oauth.token rejected unsupported_auth_method.

  1. Fill in the lesson's "What each part contributes" table for this tenant today: write which column (PAR alone, JAR without PAR, JAR with PAR) the tenant can offer, and which two missing capabilities, PAR and private_key_jwt, block the last column.

Break it

These run once the gaps close.

  1. Swap your own two JWTs: put the request object in client_assertion and the assertion in request. The tenant refuses both, 401 invalid_client because the object has no sub, and 400 invalid_request_object because the assertion's typ is wrong.

  2. Add -d scope=openid beside request in the push. The tenant refuses it with 400 invalid_request: authorization parameters belong only inside the object.

Check your work

Today, Logs show oauth.par rejected not_implemented, and Audit shows oauth.token rejected unsupported_auth_method for lab-printer. Once the gaps close, Audit shows oauth.par succeeded with private_key_jwt and a request object jti, and the two Break it refusals.

Cleanup

  1. Delete assertion-claims.json and request-claims.json.

  2. Once the gaps close, remove printer-2026-10 from lab-printer's keys, switch it back to client_secret_basic, and rotate its secret.

  3. Delete printer-2026-10.pem and its public file when you finish the JAR labs.

Missing infrastructure

  • G11: the PAR endpoint, which must accept a request parameter and check that the authenticated client matches the object's client_id claim.

  • G12: request object processing for pushed objects.

  • G8: private_key_jwt at the PAR and token endpoints.

  • G56: per-client public keys.

  • Once these 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