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

Name the API you want a token for with the resource parameter

Name the photo API with resource in authorization and token requests and get a token whose aud is that value. Today, see the parameter ignored and the audience set per client instead.

PlannedUses your lab tenant

The lesson

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 and prepare both the planned walkthrough and the Do today exercise.

  1. In Lab Photos, confirm the people and scopes from the Lab Photos preset, and that OAuth > Flow policy allows authorization_code and client_credentials.

  2. Confirm lab-printer (confidential web, redirect URI http://127.0.0.1:8765/callback, PKCE required, scope photos.read) and lab-print-orders (confidential, client_credentials, scope prints.create) exist. Create them as the OAuth core track describes if you skipped it.

  3. Store their credentials in the shell:

export CLIENT_ID='<lab-printer client id>'; read -rs CLIENT_SECRET; export CLIENT_SECRET
export ORDERS_ID='<lab-print-orders client id>'; read -rs ORDERS_SECRET; export ORDERS_SECRET

In this track the photo API's identifier is $ISSUER/resource, which is also your tenant's default access token audience. The print API's identifier is https://prints.lab.example and the sharing API's is https://share.lab.example. The .lab.example values are names only; no API is hosted there.

Planned walkthrough

This walkthrough runs once your tenant accepts resource (G16) and keeps a list of the APIs it issues tokens for (G55).

Planned setup

  1. Open OAuth > Resources (planned) and register three APIs: "Photo API" at $ISSUER/resource owning photos.read, photos.write and photos.delete; "Sharing API" at https://share.lab.example owning photos.share; "Print API" at https://prints.lab.example owning prints.create. Each resource names the access token manager used for tokens addressed to it, and aud is set to the resource identifier. Audit records tenant.oauth.resources.create (planned event name).

  2. In the tenant's resource policy (planned), leave "Resource parameter" set to **Optional, default audience $ISSUER/resource**.

Steps

  1. Name the API in the authorization request. Start btl-lab callback in a second terminal, 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&scope=photos.read&resource=$(jq -rn --arg v "$ISSUER/resource" '$v|@uri')&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"

The consent screen names "Photo API". Approve as Ava, check that the listener's state and iss match, then read -rs CODE.

Why it matters: scope says what kind of access, resource says where it will be used. In the code flow, a resource named here applies to the whole authorization, so the server can tell Ava which service is involved and remember which resources later token requests may name.

  1. Name the same API in the code exchange:

RESP=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/token" -d grant_type=authorization_code --data-urlencode "code=$CODE" \
  --data-urlencode redirect_uri=http://127.0.0.1:8765/callback --data-urlencode "code_verifier=$VERIFIER" --data-urlencode "resource=$ISSUER/resource")
TOKEN=$(jq -r .access_token <<<"$RESP"); unset CODE RESP; btl-lab decode "$TOKEN"

The payload has "aud": "<your ISSUER>/resource", because the client asked for it rather than leaving the server to infer it.

Why it matters: for JWT access tokens, the audience should be the requested resource value.

  1. Another grant works the same way. The print orders job names its API:

POST$ISSUER/oauth/token Open in console
POST $ISSUER/oauth/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64($ORDERS_ID:$ORDERS_SECRET)

grant_type=client_credentials&scope=prints.create&resource=https%3A%2F%2Fprints.lab.example

The token's aud is https://prints.lab.example.

  1. Leave the parameter out. Repeat step 2 without resource: aud is the tenant default $ISSUER/resource. Switch the resource policy to Required and repeat: {"error":"invalid_target"}, with Audit oauth.token rejected invalid_target (planned reason). Switch it back to Optional.

Why it matters: defaulting or requiring a resource is the server's documented policy, and here the tenant administrator chooses it.

Do today

Your tenant ignores a single resource parameter, refuses a repeated one, and sets each access token's audience from the client's assigned access token manager.

  1. Send resource today and see it ignored:

TOKEN=$(curl -s -u "$ORDERS_ID:$ORDERS_SECRET" "$ISSUER/oauth/token" -d grant_type=client_credentials -d scope=prints.create \
  --data-urlencode resource=https://prints.lab.example | jq -r .access_token)
btl-lab decode "$TOKEN"

The request succeeds, but aud is <your ISSUER>/resource. A client cannot assume a server honored resource; the server's documentation and the API's own audience check decide.

  1. Repeat the parameter, as the multiple-resources lesson will:

curl -s -u "$ORDERS_ID:$ORDERS_SECRET" "$ISSUER/oauth/token" -d grant_type=client_credentials \
  --data-urlencode resource=https://prints.lab.example --data-urlencode resource=https://share.lab.example | jq .

The result is {"error":"invalid_request",...}. Your tenant refuses every repeated parameter, so it rejects this before client authentication. Look for it in Logs (filter outcome rejected), not Audit.

  1. See today's alternative, where the server decides the audience per client. In OAuth > Access token managers, create lab-tmp-at-prints (JWT, ES256) with the standard aud claim overridden to https://prints.lab.example, and assign it to lab-print-orders. Request a token again as in step 1, without resource: aud is now https://prints.lab.example. This is inference: the client could not choose, and a client that needs tokens for two APIs cannot get both this way.

  1. Check resource values before sending them, as a careful client does. Save as resource-check.mjs:

const value = process.argv[2];
let url; try { url = new URL(value); } catch { console.log('refuse: not an absolute URI'); process.exit(1); }
if (value.includes('#')) { console.log('refuse: fragment'); process.exit(1); }
console.log(url.search ? 'warning: query, only if the API is identified that way' : 'ok, send exactly this string');

Run it for each value in the lesson's table: node resource-check.mjs photos.lab.example, node resource-check.mjs 'https://prints.lab.example#orders', node resource-check.mjs 'https://prints.lab.example?version=2' and node resource-check.mjs https://prints.lab.example. A URL for one album passes the syntax check but names the wrong thing; only the API's documented identifier is right.

Break it

  1. Planned: send each of these at the token endpoint, one request each: prints.lab.example (no scheme), https://prints.lab.example#orders (fragment), https://prints.lab.example/ (trailing slash, an unregistered string) and $ISSUER/resource/albums/42 (one album, not the API). Each returns invalid_target.

Why it matters: the client sends the exact identifier from its configuration and the server compares exactly.

Check your work

There are no automated checks while this lab is planned. For Do today, look in Lab Photos' Audit for oauth.token succeeded for lab-print-orders (steps 1 and 3) and tenant.oauth.managers.create and tenant.oauth.managers.assign, and in Logs for the rejected repeated-parameter request.

Cleanup

Assign lab-print-orders back to the access token manager it used before (the tenant default unless you changed it), then delete lab-tmp-at-prints. Run unset TOKEN.

Missing infrastructure

  • G16 Resource indicators: accept resource at the authorization and token endpoints (an absolute URI without a fragment), allow it to repeat as the only repeatable parameter, store approved resources with the grant, return invalid_target, and set aud from the requested resource. Steps 1 to 4 and Break it then run as written.

  • G55 Tenant resource (API) registry: identifiers, display names for consent, owned scopes, an access token manager per resource, and the optional or required resource policy that step 4 switches.

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