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

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.

  1. Introspect Ava's token as the photo API

    Recorded as oauth.introspect succeeded (other_client_token_found) for lab-photo-api.

  2. Introspect the same token as lab-print-orders

    Recorded as oauth.introspect succeeded (token_not_found) for lab-print-orders.

  3. Try to introspect as a public client

    Recorded as oauth.introspect rejected (client_authentication_required) for lab-printer-app.

  4. Revoke Ava's token as lab-printer

    Recorded as oauth.revoke succeeded (access_token_found) for lab-printer.

  5. Move the introspection endpoint to rehearse an outage

    Recorded as tenant.oauth.metadata.update succeeded.

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. Confirm these clients exist: lab-printer, lab-printer-app (public), lab-print-orders, and lab-photo-api (Confidential, Resource server introspection on). If lab-photo-api is missing, complete Trust boundaries first.

  3. 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>"
  1. Use the variables and the authorize and exchange helpers from Present an access token correctly, for lab-printer.

  2. In Access Token Management, create a manager lab-tmp-opaque (Token format: Opaque reference token, Maximum lifetime 600) and assign it to lab-printer.

Walkthrough

  1. Get a reference token. authorize "openid photos.read offline_access", sign in as Ava, exchange, and keep REFRESH=$(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.

  1. Ask as the photo API. Open this in the console with lab-photo-api's credentials, or run btl-lab introspect "$TOKEN", which makes the same call as lab-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_token

Returns {"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.

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

  1. Ask as the public client: curl -s "$ISSUER/oauth/introspect" -d "client_id=$APP_ID" --data-urlencode "token=$TOKEN". Returns 400 unauthorized_client.

Why it matters: a public client cannot authenticate, so it cannot introspect. The endpoint has to know who is asking.

  1. Ask about the refresh token. As lab-photo-api it is {"active": false}; as lab-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.

  1. Run the photo API in introspection mode in its own terminal: btl-lab resource --mode introspect. It authenticates as lab-photo-api using API_ID and API_SECRET from its environment, caches answers for up to 60 seconds keyed by a hash of the token and never past exp, and checks aud itself. 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.

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

  1. 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 answers 404. Call it with the new token: 503, logged as introspection_unavailable, not 401.

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

  1. Confirm the Introspection endpoint path is /oauth/introspect.

  2. Assign Default access tokens back to lab-printer, then delete lab-tmp-opaque.

  3. Stop btl-lab resource. Run unset TOKEN REFRESH RESP API_SECRET ORDERS_SECRET.

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