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

Write album and payment authorization details and send each the right way

Write photo_album and payment_initiation objects with common and type-specific fields, send the album through the browser and push the payment. Today, build and check them and detect PAR first.

PlannedUses your lab tenant

The lesson

Builds on: When scopes need more detail.

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

These steps are real today. Keep CLIENT_ID, CLIENT_SECRET and the enc helper from Ask for one album instead of the whole photo library.

  1. Build the album request: two albums are two objects, because identifier holds one value.

AD=$(jq -cn --arg api "$ISSUER/resource" '[
  {type: "photo_album", locations: [$api], actions: ["read"], datatypes: ["images", "captions"], identifier: "42"},
  {type: "photo_album", actions: ["read"], identifier: "57"}]')
  1. Build the payment request with the amount as a string, exactly as the type will define it. Lab Photos plays the lesson's bank for this type:

PAY=$(jq -cn '[{type: "payment_initiation", actions: ["initiate", "status"],
  instructedAmount: {currency: "EUR", amount: "42.50"}, creditorName: "Photo Printer",
  creditorAccount: {accountId: "demo-merchant-account-17"}, remittanceInformationUnstructured: "Photo book order book-5561"}]')

Planned walkthrough

This walkthrough runs once your tenant supports tenant-defined authorization details types (G15) and pushed authorization requests (G11).

Planned setup

  1. In OAuth > Authorization details (planned), edit photo_album so locations may hold $ISSUER/resource.

  2. Create the type payment_initiation: actions (allowed initiate and status), instructedAmount (object with currency, three letters, and amount, a string matching two decimal places), creditorName (string, at most 70 characters), creditorAccount (object with accountId), remittanceInformationUnstructured (string, at most 140 characters). Turn on Require PAR and allow only lab-printer.

Steps

  1. Send the album request through the browser. Start btl-lab callback, then:

eval "$(btl-lab pkce)"; eval "$(btl-lab state)"
echo "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback&authorization_details=$(enc "$AD")&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"

The consent screen lists two albums, and for album 42 both images and captions. There is no scope parameter.

Why it matters: details appear wherever a scope would. Listed values ask for every combination (read images, read captions), and one combination without another needs a separate object.

  1. Push the payment request over a direct, authenticated connection:

eval "$(btl-lab pkce)"; eval "$(btl-lab state)"
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/par" -d response_type=code --data-urlencode "client_id=$CLIENT_ID" \
  --data-urlencode redirect_uri=http://127.0.0.1:8765/callback --data-urlencode "authorization_details=$PAY" \
  -d "state=$STATE" -d "code_challenge=$CHALLENGE" -d code_challenge_method=S256 | jq .

The response is {"request_uri": "urn:ietf:params:oauth:request_uri:...", "expires_in": 60}. The browser URL then carries only client_id and request_uri.

Why it matters: the amount and the account never pass through the address bar, history or Referer headers, and nobody can change them in the browser on the way to the server.

Do today

  1. Check the objects against the definitions you planned, before sending anything:

jq -e 'all(.[]; has("type"))' <<<"$AD"
jq -e 'all(.[]; .instructedAmount.amount | type == "string" and test("^[0-9]+\\.[0-9]{2}$"))' <<<"$PAY"
printf 'album: %s characters, payment: %s characters once encoded\n' "$(enc "$AD" | wc -c)" "$(enc "$PAY" | wc -c)"

Both checks print true. "42.5" would fail the second, because values are compared exactly as written. The payment is several hundred characters once encoded.

  1. Detect PAR before relying on it:

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

pushed_authorization_request_endpoint is absent and btl_endpoint_status marks /oauth/par as not_implemented. Confirm with a request:

curl -s -o /dev/null -w '%{http_code}\n' -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/par" -d response_type=code

The status is 501. A payment client that requires PAR stops here; it does not fall back to the browser.

  1. See the exposure PAR avoids. Start the listener and send the payment through the browser with &scope=photos.read added (your tenant ignores the details):

eval "$(btl-lab pkce)"; eval "$(btl-lab state)"
echo "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback&scope=photos.read&authorization_details=$(enc "$PAY")&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"

Approve or deny, then open your browser history: the full array, with the amount and the account, sits in the entry. Your tenant's Logs keep allowlisted fields only and do not record it, but the browser and anything that saw the address did. Delete that history entry afterwards.

Break it

  1. Planned: send the payment through the browser instead of PAR. The listener receives error=invalid_request with state and iss, because the type requires PAR.

  2. Planned: push the payment with "amount": 42.5, a number. The PAR endpoint answers {"error": "invalid_authorization_details"}, because the type defines the amount as a string.

Why it matters: the type definition, which the authorization server controls, decides what each object may contain and how it must travel.

Check your work

There are no automated checks while this lab is planned. For Do today, your terminal shows two true results and 501, and Lab Photos' Logs show the refused PAR request.

Cleanup

Delete the history entry from Do today step 3 and run unset AD PAY.

Missing infrastructure

  • G15 Rich Authorization Requests, as in the previous lab, plus type-specific fields with kinds and patterns, and a per-type Require PAR setting.

  • G11 Pushed authorization requests: /oauth/par answers 501 today. The lesson's client authenticates at the PAR endpoint with private_key_jwt, which is G8; the planned step uses client_secret_basic instead.

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