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

Build a request object and send it by value or by reference

Turn lab-printer's authorization request into request object claims, measure the signed URL, and plan the switch to private_key_jwt, by reference delivery and encryption.

PlannedUses your lab tenant

The lesson

Builds on: The problem JAR 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. Keep printer-2026-10.pem and printer-2026-10.pub.pem from Compare a server's record with a signature anyone can verify, and the shell variables from the PAR labs.

  2. Add a Base64url helper to your shell.

b64url() { base64 | tr '+/' '-_' | tr -d '=\n'; }
  1. Planned (G56, G8): on lab-printer, register printer-2026-10 under Keys, set the authentication method to private_key_jwt and the request object signing algorithm to ES256.

  2. Planned (G12): in OAuth > Flow policy, allow request objects by value and, optionally, by reference from registered HTTPS addresses.

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 the claims: every authorization parameter plus the claims that protect the object. This step runs today.

btl-lab pkce; btl-lab state     # export VERIFIER, CHALLENGE and STATE
NOW=$(date +%s)
CLAIMS=$(jq -nc --arg iss "$CLIENT_ID" --arg aud "$ISSUER" --arg ru "$REDIRECT" --arg st "$STATE" --arg cc "$CHALLENGE" \
  --argjson now $NOW --arg jti "$(openssl rand 16 | b64url)" \
  '{iss:$iss, aud:$aud, response_type:"code", client_id:$iss, redirect_uri:$ru, scope:"openid photos.read",
    state:$st, code_challenge:$cc, code_challenge_method:"S256", iat:$now, nbf:$now, exp:($now+300), jti:$jti}')
echo "$CLAIMS" | jq .

Why it matters: "From parameters to claims". The names are the same, the values are plain JSON, the return address is no longer URL-encoded, and there is no sub.

  1. Sign CLAIMS as ES256 with printer-2026-10.pem and the header {"alg":"ES256","kid":"printer-2026-10","typ":"oauth-authz-req+jwt"}. Save the result as OBJ and decode it with btl-lab decode "$OBJ".

Why it matters: "Claims that protect the object". iss, aud, exp and jti bind the object to one client, one server and five minutes, and typ says what kind of JWT it is.

  1. Send it by value, open the address and approve as Ava. The listener prints code, state and iss.

URL="$ISSUER/oauth/authorize?client_id=$CLIENT_ID&request=$OBJ"; printf %s "$URL" | wc -c

Why it matters: the signed URL is roughly three times longer than the plain one, which is why the other routes exist.

  1. Redeem the code with a client assertion instead of a secret. Sign the claims {"iss":"$CLIENT_ID","sub":"$CLIENT_ID","aud":"$ISSUER","iat":<now>,"exp":<now+60>,"jti":"<random>"} with the same key and save the result as ASSERTION.

curl -s "$ISSUER/oauth/token" -d grant_type=authorization_code -d code=<code> --data-urlencode redirect_uri=$REDIRECT \
  -d code_verifier=$VERIFIER -d client_id=$CLIENT_ID \
  -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer -d client_assertion=$ASSERTION | jq '{token_type, scope}'

Why it matters: one key pair now covers request signing and client authentication. The assertion's sub and the object's typ keep the two kinds of JWT apart.

  1. Optional, by reference: serve OBJ at an HTTPS address under the prefix you registered, with Content-Type: application/oauth-authz-req+jwt, and open $ISSUER/oauth/authorize?client_id=$CLIENT_ID&request_uri=<url-encoded address>.

Why it matters: the URL stays short, but the tenant now fetches from an address the client hosts, so the address needs a long random part and a short life.

  1. Optional, encrypted: encrypt the signed object to the tenant's use: enc key from its JWKS (alg RSA-OAEP-256, enc A256GCM, cty JWT), send it by value, and count five dot-separated parts.

Why it matters: sign first, then encrypt. Encryption alone proves nothing about who made the object.

Do today

  1. Run Planned walkthrough step 1. It is local and works now.

  2. Measure what signing would cost in URL length, without signing anything. The header and claims are Base64url-encoded, and an ES256 signature always adds 86 characters.

HEADER='{"alg":"ES256","kid":"printer-2026-10","typ":"oauth-authz-req+jwt"}'
INPUT="$(printf %s "$HEADER" | b64url).$(printf %s "$CLAIMS" | b64url)"
echo "plain:  $(printf %s "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$(urlenc $REDIRECT)&scope=$(urlenc 'openid photos.read')&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256" | wc -c)"
echo "signed: $(( $(printf %s "$ISSUER/oauth/authorize?client_id=$CLIENT_ID&request=" | wc -c) + ${#INPUT} + 87 ))"

Compare the two numbers with the lesson's 270 and 770.

  1. Check the object rules by hand: confirm that CLAIMS contains neither request nor request_uri, and has no sub.

  2. Send an authorization request with a request parameter. The tenant refuses on its presence, so a stand-in value is enough.

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-2&request=demo-request-object"

The redirect carries error=request_not_supported with state and iss; Audit shows oauth.authorize rejected request_not_supported.

  1. Try a client assertion at the token endpoint. Again the tenant refuses on the parameter's presence.

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 with a description naming client_secret_basic, and Audit shows oauth.token rejected unsupported_auth_method.

  1. Read the algorithms the tenant advertises.

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

token_endpoint_auth_methods_supported is ["client_secret_basic","none"], and there is no request_object_signing_alg_values_supported.

Break it

These run once G12 exists.

  1. Put request_uri inside the claims and send the signed object: error=invalid_request_object. An object cannot point to another object.

  2. Sign a correct object with a fresh key that is not registered for lab-printer: error=invalid_request_object.

Check your work

Today, Audit shows oauth.authorize rejected request_not_supported and oauth.token rejected unsupported_auth_method for lab-printer. Once the gaps close, Audit also shows oauth.authorize code_issued with the object's jti, and oauth.token succeeded with the authentication method private_key_jwt.

Cleanup

  1. Remove any request object file you served.

  2. Keep printer-2026-10.pem for the next lab.

Missing infrastructure

  • G12: request object parsing and policy, by value, by reference from registered HTTPS addresses with fetch limits, and decryption with a tenant enc key.

  • G8: private_key_jwt client authentication.

  • 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