OAUTH 2.0 · LAB
Validate reference tokens by introspection, with a cache that fails closed
Switch the printer to opaque tokens, introspect them as the photo API, compare what other callers learn, then measure the cache window and an introspection outage.
ReadyUses your lab tenant
The lesson
Builds on: Using access tokens, Trust boundaries.
New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.
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.
Introspect Ava's token as the photo API
Recorded as
oauth.introspectsucceeded (other_client_token_found) forlab-photo-api.Introspect the same token as lab-print-orders
Recorded as
oauth.introspectsucceeded (token_not_found) forlab-print-orders.Try to introspect as a public client
Recorded as
oauth.introspectrejected (client_authentication_required) forlab-printer-app.Revoke Ava's token as lab-printer
Recorded as
oauth.revokesucceeded (access_token_found) forlab-printer.Move the introspection endpoint to rehearse an outage
Recorded as
tenant.oauth.metadata.updatesucceeded.
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.
Confirm these clients exist:
lab-printer,lab-printer-app(public),lab-print-orders, andlab-photo-api(Confidential, Resource server introspection on). Iflab-photo-apiis missing, complete Trust boundaries first.Load the credentials you need. Secrets are read without echo.
export API_ID="<lab-photo-api client ID>"; read -rs API_SECRET; export API_SECRET
export ORDERS_ID="<lab-print-orders client ID>"; read -rs ORDERS_SECRET
export APP_ID="<lab-printer-app client ID>"
Use the variables and the
authorizeandexchangehelpers from Present an access token correctly, forlab-printer.In Access Token Management, create a manager
lab-tmp-opaque(Token format: Opaque reference token, Maximum lifetime600) and assign it tolab-printer.
Walkthrough
Get a reference token.
authorize "openid photos.read offline_access", sign in as Ava,exchange, and keepREFRESH=$(jq -r .refresh_token <<<"$RESP").btl-lab decode "$TOKEN"has nothing to show.
Why it matters: the token means nothing outside the authorization server's records, so nothing about Ava travels inside it, and an API has to ask what it stands for.
Ask as the photo API. Open this in the console with
lab-photo-api's credentials, or runbtl-lab introspect "$TOKEN", which makes the same call aslab-photo-api.
POST$ISSUER/oauth/introspect
Open in console
POST $ISSUER/oauth/introspect HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Accept: application/json
token=$TOKEN&token_type_hint=access_tokenReturns {"active": true, "iss": ..., "sub": ..., "aud": "$ISSUER/resource", "client_id": ..., "scope": "openid photos.read", "token_type": "Bearer", "exp": ...}.
Why it matters: the same facts a JWT would carry, delivered only to an authenticated caller over a protected connection. The token travels in the body, never in the URL.
Ask as a confidential client that is not a resource server:
curl -s -u "$ORDERS_ID:$ORDERS_SECRET" "$ISSUER/oauth/introspect" --data-urlencode "token=$TOKEN" | jq .
Returns {"active": false} with no reason. Audit records token_not_found for this call, and other_client_token_found for the photo API's call in step 2.
Why it matters: the endpoint answers per caller. A caller not allowed to ask learns nothing, which is what stops token scanning.
Ask as the public client:
curl -s "$ISSUER/oauth/introspect" -d "client_id=$APP_ID" --data-urlencode "token=$TOKEN". Returns400 unauthorized_client.
Why it matters: a public client cannot authenticate, so it cannot introspect. The endpoint has to know who is asking.
Ask about the refresh token. As
lab-photo-apiit is{"active": false}; aslab-printer(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/introspect" --data-urlencode "token=$REFRESH" -d token_type_hint=refresh_token) it is active.
Why it matters: the photo API needs to know about access tokens presented to it, and nothing about the printer's other credentials. Sending less is the simplest privacy measure.
Run the photo API in introspection mode in its own terminal:
btl-lab resource --mode introspect. It authenticates aslab-photo-apiusingAPI_IDandAPI_SECRETfrom its environment, caches answers for up to 60 seconds keyed by a hash of the token and never pastexp, and checksauditself. Call it:
curl -si http://127.0.0.1:8766/photos -H "Authorization: Bearer $TOKEN" # 200
curl -si -X POST http://127.0.0.1:8766/photos -H "Authorization: Bearer $TOKEN" # 403 insufficient_scope
curl -si http://127.0.0.1:8766/photos -H "Authorization: Bearer not-a-real-token" # 401 invalid_token
Why it matters: active: true replaces the signature and claim checks, not the scope and object decision. The tenant's resource server flag lets this client ask about any client's access token, so the API checks aud on every answer itself.
Measure the cache window. Revoke Ava's access token as the printer, then call the API at once and again after a minute:
curl -si -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/revoke" --data-urlencode "token=$TOKEN" -d token_type_hint=access_token
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8766/photos -H "Authorization: Bearer $TOKEN" # 200, from the cache
sleep 65; curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8766/photos -H "Authorization: Bearer $TOKEN" # 401
Why it matters: the cache lifetime is exactly the window in which a revoked token still works at this API. A deployment might accept a minute for reads and introspect afresh before every deletion.
Rehearse an outage. Get a new token for Ava. In Metadata Management, change the Introspection endpoint path (for example to
/oauth/introspect-moved) and save. The running API still uses the address it read at start, which now answers404. Call it with the new token:503, logged asintrospection_unavailable, not401.
Why it matters: the API fails closed, and a 503 does not send the printer off to replace a token that was never the problem. Cached answers are not stretched to ride out the outage.
Restore: set the Introspection endpoint path back to /oauth/introspect and save. Restart btl-lab resource so it reads the metadata again.
Break it
Step 8 is the failure case. Also try a wrong API_SECRET when starting btl-lab resource: every call becomes 503, and Audit shows oauth.introspect rejected with invalid_client against lab-photo-api. A misconfigured API looks like an outage to its callers, which is why its own log must say which one it is.
Check your work
Press Check my progress. The checks look for, in order: other_client_token_found for lab-photo-api, token_not_found for lab-print-orders, the public client's refusal, oauth.revoke with access_token_found for lab-printer, and the tenant.oauth.metadata.update that moved the endpoint.
The API log should show allowed, insufficient_scope, inactive and introspection_unavailable, never a token.
Cleanup
Confirm the Introspection endpoint path is
/oauth/introspect.Assign Default access tokens back to
lab-printer, then deletelab-tmp-opaque.Stop
btl-lab resource. Rununset TOKEN REFRESH RESP API_SECRET ORDERS_SECRET.