OAUTH 2.0 · LAB
Decide, as the photo API, whether to trust a token
Create the client that plays the photo API, check a token's issuer, signature, audience, lifetime and revocation as an API must, and see a correctly signed token for another API refused.
Partly readyUses your lab tenant
The lesson
Builds on: The problem OAuth solves.
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.
- G3 Sample protected resource API; no RFC 9728 protected resource metadata
- G16 Resource indicators
- G22 End-user authorization engine (PDP, RBAC/ABAC/ReBAC for application data)
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.
Create the lab-photo-api client
Recorded as
tenant.oauth.clients.createsucceeded.The photo API asks the authorization server about a token
Recorded as
oauth.introspectsucceeded (other_client_token_found) forlab-photo-api.The photo API cannot revoke the printer's grant
Recorded as
oauth.revokerejected (other_client_token) forlab-photo-api.The client revokes its own token
Recorded as
oauth.revokesucceeded (access_token_found).
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
Press Start on this page.
In the tenant portal, open OAuth > Clients > Create client and choose Blank client. Name it
lab-photo-api, type Confidential, tick Resource server introspection, choose no grants and no redirect URIs, tick Enabled, and save. Copy its client ID.Open Access Token Management > Client authentication secret, choose
lab-photo-apiand Rotate client secret. The secret is shown once.In Access Token Management, create a temporary manager named
lab-tmp-other-audience: format Signed JWT, an active signing key, lifetime 600 seconds, and under Standard claims setaudtohttps://api.printlab.example. Save it, but do not assign it yet.Set the shell variables.
btl-lab introspectandbtl-lab resource --mode introspectcall the tenant aslab-photo-apiwith them.
export ISSUER="https://tenant-<id>.beyondthelogin.dev"
export DECODER_ID="<Token Decoder client ID>"
export API_ID="<lab-photo-api client ID>"
read -rs API_SECRET && export API_SECRET # paste the secret, then press Enter
Walkthrough
Get a fresh Token Decoder token for Ava with Scopes
openid photos.read, and copy the access token:read -r TOKEN.
Does the API recognize the authority that issued it? Compare the token's key ID with the keys your tenant publishes, then run the full validation.
btl-lab decode "$TOKEN" # header: alg ES256, typ at+jwt, and a kid
btl-lab discover "$ISSUER" # the JWKS lists the same kid
btl-lab verify "$TOKEN" --issuer "$ISSUER" --audience "$ISSUER/resource" --type at+jwt
verify prints each check: signature against the published key, alg, typ, iss, aud, and exp with clock skew. All pass.
Why it matters: the lesson's first API question is whether it recognizes the issuer. Signature verification answers that, and the remaining lines answer the separate questions of intended API and lifetime.
Play the photo API. In a second terminal, start the toolkit's local resource server, which validates JWTs itself, enforces
audand scopes, and answers with RFC 6750 challenges.
btl-lab resource --mode jwt
Back in the first terminal, call it with the token: curl -s -i -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8766/photos returns 200, and the resource server's terminal prints the checks it made.
Ask the authorization server instead, as an API with opaque tokens must.
btl-lab introspect "$TOKEN"
The answer is active: true, the Token Decoder's client_id, Ava's sub and scope: "openid photos.read". Because lab-photo-api is marked as a resource server, the tenant lets it introspect tokens issued to other clients.
Each participant checks only what is its responsibility. Try to end the decoder's grant as the photo API.
curl -s -u "$API_ID:$API_SECRET" -d "token=$TOKEN" "$ISSUER/oauth/revoke"
The answer is 400 with {"error":"unauthorized_client"}: a client can revoke only its own tokens. The token stays active.
Why it matters: the API decides whether to accept a request; it does not get to withdraw the access Ava gave another application.
A correctly signed token meant for a different API. In OAuth > Clients > Token Decoder, set Access token manager to
lab-tmp-other-audienceand save. This revokes the decoder's existing tokens. Get a new decoder token and run the same checks.
read -r TOKEN # the new access token
btl-lab verify "$TOKEN" --issuer "$ISSUER" --audience "$ISSUER/resource" --type at+jwt
curl -s -i -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8766/photos
The signature check passes and the aud check fails, because aud is https://api.printlab.example. The resource server answers 401 with WWW-Authenticate: Bearer error="invalid_token".
Why it matters: a correctly signed token intended for another API is not permission at this one. Audience is a separate check from signature.
Restore: in OAuth > Clients > Token Decoder, set Access token manager back to Default access tokens and save.
Optional, with your Lab Mail tenant: get a Token Decoder token there and run
btl-lab introspecton it against Lab Photos. The answer is{"active": false}, andbtl-lab verifyagainst$ISSUERfails because itskidis not in this tenant's JWKS.
Look back at the
200from step 3. The resource server already returned its answer. Nothing you do to the token now can take that response back.
Why it matters: this is the boundary after access is granted. Revocation and short lifetimes stop future requests; they cannot make a copy the printer already downloaded disappear.
Planned walkthrough
These steps need the hosted lab API (G3), resource indicators (G16) and per-resource authorization (G22).
Call the hosted photo API with Ava's token for one of Ben's albums. It answers
403, because a valid token is not permission to retrieve whichever album identifier appears in the URL.
curl -s -i -H "Authorization: Bearer $TOKEN" "$ISSUER/lab-api/photos/albums/<one of Ben's album IDs>"
Ask for a token for
https://api.printlab.exampleper request with theresourceparameter instead of changing the client's token manager, then see the hosted photo API refuse it for the wrong audience.
Break it
Get a fresh decoder token for Ava (
read -r TOKEN). Confirm that the local resource server in jwt mode accepts it, then revoke it as the decoder, which owns it.
POST$ISSUER/oauth/revoke
Open in console
POST $ISSUER/oauth/revoke HTTP/1.1
Content-Type: application/x-www-form-urlencoded
client_id=$DECODER_ID&token=$TOKENbtl-lab introspect "$TOKEN"now returns{"active": false}. Callhttp://127.0.0.1:8766/photosagain: the jwt-mode resource server still answers200, because the signature andexpare still fine.Stop the resource server and restart it with
btl-lab resource --mode introspect. The same request now gets401 invalid_token.
An API that validates JWTs only locally keeps honoring a revoked token until it expires. Short lifetimes and introspection are the two answers this tenant offers.
Check your work
Press Check my progress. In tenant Audit you can also find tenant.oauth.clients.create and tenant.oauth.credentials.rotate for lab-photo-api, tenant.oauth.managers.create for lab-tmp-other-audience, oauth.introspect succeeded other_client_token_found with actor lab-photo-api, oauth.revoke rejected other_client_token, and oauth.revoke succeeded access_token_found.
Cleanup
Confirm the Token Decoder uses Default access tokens.
Delete the
lab-tmp-other-audiencemanager.Keep
lab-photo-apiand its secret. Later labs use it to play the photo API.
Missing infrastructure
G3, sample protected resource API. The toolkit's local resource server stands in for the photo API today. A hosted per-tenant API at
$ISSUER/lab-api/photos/would validate tokens, answer withWWW-Authenticatechallenges and write its own Audit events, so the API's decisions in steps 3 and 6 appear in tenant history too.G16, resource indicators. The client would ask for a token for
https://api.printlab.exampleper request instead of the tenant changing a token manager.G22, per-resource authorization. The full lab has Ava's token request Ben's album and receive
403, showing that a valid token is not permission to any identifier in a URL.