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.
- G16 Resource indicators
- G55 Tenant resource (API) registry
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.
In Lab Photos, confirm the people and scopes from the Lab Photos preset, and that OAuth > Flow policy allows
authorization_codeandclient_credentials.Confirm
lab-printer(confidential web, redirect URIhttp://127.0.0.1:8765/callback, PKCE required, scopephotos.read) andlab-print-orders(confidential,client_credentials, scopeprints.create) exist. Create them as the OAuth core track describes if you skipped it.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
Open OAuth > Resources (planned) and register three APIs: "Photo API" at
$ISSUER/resourceowningphotos.read,photos.writeandphotos.delete; "Sharing API" athttps://share.lab.exampleowningphotos.share; "Print API" athttps://prints.lab.exampleowningprints.create. Each resource names the access token manager used for tokens addressed to it, andaudis set to the resource identifier. Audit recordstenant.oauth.resources.create(planned event name).In the tenant's resource policy (planned), leave "Resource parameter" set to **Optional, default audience
$ISSUER/resource**.
Steps
Name the API in the authorization request. Start
btl-lab callbackin 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.
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.
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.exampleThe token's aud is https://prints.lab.example.
Leave the parameter out. Repeat step 2 without
resource:audis the tenant default$ISSUER/resource. Switch the resource policy to Required and repeat:{"error":"invalid_target"}, with Auditoauth.tokenrejectedinvalid_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.
Send
resourcetoday 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.
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.
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 standardaudclaim overridden tohttps://prints.lab.example, and assign it tolab-print-orders. Request a token again as in step 1, withoutresource:audis nowhttps://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.
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
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 returnsinvalid_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
resourceat 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, returninvalid_target, and setaudfrom 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.