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

Call a storage service from the photo API and see what each shortcut loses

Give the photo API its own storage-audience token, show that forwarding Ava's token or naming her in a header fails or proves nothing, and meet the missing token exchange.

Partly readyUses your lab tenant

The lesson

Builds on: Scopes and audiences, Trust boundaries.

New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.

Partly ready. Most of this lab runs today. Steps that wait on platform features are marked, and Missing infrastructure says what they need.

Your progress

Press Start before you begin. Only events your tenant records after that count, in the order below. Checking reads your tenant's Audit, so you need Audit read access in it.

  1. Let lab-photo-api use client credentials

    Recorded as tenant.oauth.clients.update succeeded.

  2. Get the photo API's own storage token

    Recorded as oauth.token succeeded for lab-photo-api.

  3. Get Ava's photo token through the printer

    Recorded as oauth.token succeeded for lab-printer about [email protected].

  4. Be refused a client assertion the tenant does not support

    Recorded as oauth.token rejected (unsupported_auth_method) for lab-photo-api.

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

  1. Choose Lab Photos as the lab tenant and press Start.

  2. Flow policy must allow client credentials. In Clients, open lab-photo-api and allow the client credentials grant. Save.

  3. In Access Token Management, create lab-tmp-storage: Signed JWT, Maximum lifetime 300, and under Standard claims set aud to the text https://storage.lab.test. Assign a client: lab-photo-api.

  4. Load API_ID and API_SECRET (with read -rs) for lab-photo-api, and CLIENT_ID and CLIENT_SECRET for lab-printer with the helpers from Present an access token correctly.

  5. Run a storage service in its own terminal. Its read routes stand in for storage's file routes, and it accepts only storage-audience tokens:

btl-lab resource --mode jwt --audience https://storage.lab.test --port 8768 --album "42=<Ava's sub>"

Find Ava's sub by introspecting one of her tokens as lab-printer, as in Enforcing access at an API.

Walkthrough

  1. The photo API calls as itself. It gets a token with client credentials and calls storage:

API_TOKEN=$(curl -s -u "$API_ID:$API_SECRET" "$ISSUER/oauth/token" -d grant_type=client_credentials -d scope=photos.read | jq -r .access_token)
btl-lab decode "$API_TOKEN"
curl -si http://127.0.0.1:8768/photos -H "Authorization: Bearer $API_TOKEN"            # 200
curl -si http://127.0.0.1:8768/albums/42/photos -H "Authorization: Bearer $API_TOKEN"  # 403, client_only_token

The token's aud is https://storage.lab.test and its sub is the photo API's client ID.

Why it matters: storage knows which service is calling, but not for whom. To serve album 42 it would have to trust the photo API's object checks completely, and the photo API's token would need to read every user's files.

  1. Forward Ava's token unchanged. authorize "photos.read", sign in as Ava, exchange, then present $TOKEN to storage: 401, logged as wrong_audience.

Why it matters: a token issued for the photo API must not work at storage. It names the wrong audience, and its scope describes what the printer may do at the photo API, not what the photo API needs from storage.

  1. Name the user in a header. Call storage with the photo API's own token plus -H "X-User-Id: <Ava's sub>". The decision line shows the photo API's client ID as the subject, as before: storage has no way to verify the header, and a correct service ignores it.

Why it matters: a downstream service should not accept a user identity its caller merely asserts. Any caller could name anyone.

  1. Ask for a token for the next hop with token exchange, presenting Ava's token:

POST$ISSUER/oauth/token Open in console
POST $ISSUER/oauth/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange&subject_token=$TOKEN&subject_token_type=urn:ietf:params:oauth:token-type:access_token&audience=https://storage.lab.test

In the console, enter lab-photo-api's credentials. Returns 400 unsupported_grant_type: this tenant does not offer token exchange yet (G6).

Why it matters: the exchanged token would keep Ava as subject, name storage as audience, carry only what this call needs, record that the photo API acts for her, and expire within minutes. Storage could then refuse album 43 on its own.

  1. Look at how the photo API proves who it is. It sends a long-lived client secret with HTTP Basic. Try authenticating with a signed client assertion instead, using a placeholder value:

curl -s "$ISSUER/oauth/token" -d grant_type=client_credentials -d "client_id=$API_ID" \
  -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer -d client_assertion=placeholder | jq .

Returns 401 invalid_client, Audit reason unsupported_auth_method. The tenant accepts only client_secret_basic for confidential clients today (G8), and has no workload identity federation (G7).

Why it matters: a workload identity credential from the hosting platform, or mutual TLS between services, would leave no secret to copy or rotate by hand.

Break it

Try the tempting fix for step 2. Restart storage with the photo API's audience instead: btl-lab resource --mode jwt --audience "$ISSUER/resource" --port 8768. Ava's printer token now works directly at storage, and so would any token issued for the photo API, from any client that holds one.

Restore: restart storage with --audience https://storage.lab.test as in Setup.

Check your work

Press Check my progress. The checks look for, in order: the tenant.oauth.clients.update that allowed client credentials on lab-photo-api, its own token, Ava's token through lab-printer, and the refused client assertion.

The storage log shows allowed, client_only_token and wrong_audience. The token exchange refusal appears only in Logs, because it is refused before the client is identified.

Cleanup

  1. Assign Default access tokens back to lab-photo-api, then delete lab-tmp-storage.

  2. In Clients, remove the client credentials grant from lab-photo-api, so it is again an introspection-only client.

  3. Stop the storage terminal. Run unset API_TOKEN TOKEN API_SECRET.

Missing infrastructure

  • G6 Token exchange. With RFC 8693, step 4 returns a token with aud https://storage.lab.test, sub Ava, narrowed scope, an act claim naming the photo API and a lifetime of minutes. Storage then serves album 42 for Ava and refuses album 43 by its own check.

  • G7 Workload identity assertions. The photo API would authenticate with a short-lived credential issued by its hosting platform, trusted in advance by the tenant, instead of a copied secret.

  • G8 Client authentication beyond client_secret_basic. With private_key_jwt or mutual TLS, step 5 succeeds, and with certificate-bound tokens a copied storage token is useless without the photo API's private key.

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