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.
- G6 Token exchange (RFC 8693)
- G7 JWT bearer and SAML assertion grants
- G8 Client authentication beyond `client_secret_basic` and `none`
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.
Sign in to start this lab and check your progress. Log in or create an account.
Let lab-photo-api use client credentials
Recorded as
tenant.oauth.clients.updatesucceeded.Get the photo API's own storage token
Recorded as
oauth.tokensucceeded forlab-photo-api.Get Ava's photo token through the printer
Recorded as
oauth.tokensucceeded forlab-printerabout[email protected].Be refused a client assertion the tenant does not support
Recorded as
oauth.tokenrejected (unsupported_auth_method) forlab-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
Choose Lab Photos as the lab tenant and press Start.
Flow policy must allow client credentials. In Clients, open
lab-photo-apiand allow the client credentials grant. Save.In Access Token Management, create
lab-tmp-storage: Signed JWT, Maximum lifetime300, and under Standard claims setaudto the texthttps://storage.lab.test. Assign a client:lab-photo-api.Load
API_IDandAPI_SECRET(withread -rs) forlab-photo-api, andCLIENT_IDandCLIENT_SECRETforlab-printerwith the helpers from Present an access token correctly.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
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.
Forward Ava's token unchanged.
authorize "photos.read", sign in as Ava,exchange, then present$TOKENto storage:401, logged aswrong_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.
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.
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.testIn 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.
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
Assign Default access tokens back to
lab-photo-api, then deletelab-tmp-storage.In Clients, remove the client credentials grant from
lab-photo-api, so it is again an introspection-only client.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
audhttps://storage.lab.test,subAva, narrowed scope, anactclaim 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. Withprivate_key_jwtor mutual TLS, step 5 succeeds, and with certificate-bound tokens a copied storage token is useless without the photo API's private key.