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

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.

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.

  1. Store lab-printer's credentials (confidential web, redirect URI http://127.0.0.1:8765/callback, scope photos.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).

  1. 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.

  1. Read the rest of the document: bearer_methods_supported is ["header"], so the token goes only in the Authorization header; there are no DPoP members, so plain bearer tokens are expected; resource_name and resource_documentation are for people.

  2. Follow to the authorization server and validate it: add_server photos "$ISSUER" "$(srv photos client_id)" refreshes and checks its metadata. Its protected_resources member lists <your ISSUER>/resource, agreeing with the API's authorization_servers.

Why it matters: discovery runs API first, then authorization server, and each document is checked before anything in it is used.

  1. 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.

  1. Call the API: curl -s -H "Authorization: Bearer $TOKEN" "$ISSUER/resource/photos" returns sample photos.

Do today

  1. 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.

  1. 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.

  1. 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 with btl-lab decode: aud is whatever lab-printer's access token manager sets (the tenant default <your ISSUER>/resource), not the fixture's address, because your tenant ignores resource today. Today's server cannot restrict a token to a discovered API, which is why the planned flow depends on G16.

  1. Look for the reverse list: curl -s "$ISSUER/.well-known/oauth-authorization-server" | jq .protected_resources prints null, so the cross-check in planned step 3 has nothing to compare yet.

Break it

  1. Planned: send the token as a query parameter, $ISSUER/resource/photos?access_token=.... The API refuses it, because its document lists only header.

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_resources in authorization server metadata. With G3 alone the flow already works, because the sample API's identifier equals today's default audience.

  • G16 Resource indicators: resource in the request, which keeps tokens restricted once a client uses more than one API.

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