OAUTH 2.0 · LAB
Go from an API's address to an access token for that API
Read the resource document member by member, follow authorization_servers to validated authorization server metadata, request a token naming the API, and call it. Today, run every step except the last for real.
PlannedIncludes a simulationUses your lab tenant
The lesson
Builds on: Discovering a protected resource.
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.
- G3 Sample protected resource API; no RFC 9728 protected resource metadata
- G16 Resource indicators
Setup
These steps are real today. You need fixture.mjs and find-api.mjs from Find an API's metadata starting from nothing but its address, and collage.sh with the hardened add_server from the metadata labs, all in ~/btl-issuer.
Store
lab-printer's credentials (confidential web, redirect URIhttp://127.0.0.1:8765/callback, scopephotos.read):
export CLIENT_ID='<lab-printer client id>'; read -rs CLIENT_SECRET; export CLIENT_SECRET
. ./collage.sh
Planned walkthrough
This walkthrough runs once Lab Photos hosts the sample photo API with RFC 9728 metadata (G3) and accepts the resource parameter (G16).
Discover from the address:
TRUSTED_ISSUERS="$ISSUER" node find-api.mjs "$ISSUER/resource"
It prints your issuer, the scope photos.read and "via": "challenge".
Why it matters: the API names its authorization server; the client decides whether to use it.
Read the rest of the document:
bearer_methods_supportedis["header"], so the token goes only in the Authorization header; there are no DPoP members, so plain bearer tokens are expected;resource_nameandresource_documentationare for people.Follow to the authorization server and validate it:
add_server photos "$ISSUER" "$(srv photos client_id)"refreshes and checks its metadata. Itsprotected_resourcesmember lists<your ISSUER>/resource, agreeing with the API'sauthorization_servers.
Why it matters: discovery runs API first, then authorization server, and each document is checked before anything in it is used.
Request an audience-restricted token. 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&scope=photos.read&resource=$(jq -rn --arg v "$ISSUER/resource" '$v|@uri')&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"
Approve as Ava, check state and iss, and redeem the code with the same resource:
read -rs CODE
TOKEN=$(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" | jq -r .access_token); unset CODE
The token's aud is <your ISSUER>/resource.
Why it matters: a client that takes directions from APIs should get tokens only the named API accepts. Otherwise an API could steer it into requesting a token some other API would also accept.
Call the API:
curl -s -H "Authorization: Bearer $TOKEN" "$ISSUER/resource/photos"returns sample photos.
Do today
Read a resource document member by member, from the fixture:
Simulation. the document comes from your local fixture.mjs, because your tenant publishes none yet (G3).
curl -s http://127.0.0.1:8790/.well-known/oauth-protected-resource/api | jq .
resource is the only required member and the one the client checks first. authorization_servers lists your real issuer. bearer_methods_supported says header. With no DPoP members, the fixture expects plain bearer tokens.
Run the whole client side. Discover from the fixture, then validate the named authorization server for real:
FIXTURE_ORIGIN=http://127.0.0.1:8790 TRUSTED_ISSUERS="$ISSUER" node find-api.mjs http://127.0.0.1:8790/api
add_server photos "$ISSUER" "$(srv photos client_id)"
The first prints your issuer; the second prints saved photos after checking the issuer exactly.
Request a token from the server you found. Run step 4 of the planned walkthrough as written, approve and redeem the code with
resource. Decode the token withbtl-lab decode:audis whateverlab-printer's access token manager sets (the tenant default<your ISSUER>/resource), not the fixture's address, because your tenant ignoresresourcetoday. Today's server cannot restrict a token to a discovered API, which is why the planned flow depends on G16.
Look for the reverse list:
curl -s "$ISSUER/.well-known/oauth-authorization-server" | jq .protected_resourcesprintsnull, so the cross-check in planned step 3 has nothing to compare yet.
Break it
Planned: send the token as a query parameter,
$ISSUER/resource/photos?access_token=.... The API refuses it, because its document lists onlyheader.
Check your work
There are no automated checks while this lab is planned. For Do today, look in Lab Photos' Audit for oauth.authorize with code_issued and oauth.token succeeded for lab-printer, and in your terminal for the discovered resource and the validated issuer.
Cleanup
Run unset TOKEN. Keep the fixture and find-api.mjs for the next lab.
Missing infrastructure
G3 Sample protected resource API: the document, the API, its challenges, and
protected_resourcesin authorization server metadata. With G3 alone the flow already works, because the sample API's identifier equals today's default audience.G16 Resource indicators:
resourcein the request, which keeps tokens restricted once a client uses more than one API.