OAUTH 2.0 · LAB
Read what an access token says about how and when Ava signed in
Put auth_time into lab-printer-app's access tokens, see it survive a refresh and reach the API through introspection, and see that nothing in the token says whether Ava used a password or a passkey.
Partly readyUses your lab tenant
The lesson
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.
- G20 `acr` / `acr_values` and acr-driven step-up
- G48 Authentication context (`amr`, request context) in the token policy script and in access tokens and introspection
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.
Sign Ava in with her password at lab-printer-app
Recorded as
oauth.authorizesucceeded (user_signed_in) forlab-printer-app.Exchange the code as a public client
Recorded as
oauth.tokensucceeded forlab-printer-app.Let the photo API read auth_time by introspection
Recorded as
oauth.introspectsucceeded (other_client_token_found) forlab-photo-api.Enroll a passkey for Ava
Recorded as
account.securitysucceeded (method_enrolled) about[email protected].
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
In these labs lab-printer-app, the printer's public browser and mobile twin, plays the lesson's photo editor: it can delete photos, and deleting needs a recent sign-in.
In Lab Photos, confirm OAuth > Flow policy allows
authorization_codeandrefresh_token, and that the exclusive scopephotos.deleteexists (the Lab Photos preset creates it).In OAuth > Access token managers, create
lab-tmp-at-signin(JWT, ES256) and add the claim mappingauth_timefrom the attribute "Sign-in time". The step-up labs share this manager; the last one deletes it.In OAuth > Clients, configure
lab-printer-app(create it with the Native or SPA preset if needed): public, redirect URIhttp://127.0.0.1:8765/callback, PKCE required, grantsauthorization_codeandrefresh_token, scopesopenid,photos.read,photos.deleteandoffline_access, consent mode Always, access token managerlab-tmp-at-signin.In Authentication, set passkey to optional and allow passwordless passkey sign-in. Make sure Ava has a password.
Confirm
lab-photo-apihas Resource server on, and store what the toolkit needs:
export APP_ID='<lab-printer-app client id>'
export API_ID='<lab-photo-api client id>'; read -rs API_SECRET; export API_SECRET
Press Start on this page with Lab Photos selected.
Walkthrough
A password sign-in. Start
btl-lab callback, then request a fresh sign-in withprompt=login:
eval "$(btl-lab pkce)"; eval "$(btl-lab state)"
echo "$ISSUER/oauth/authorize?response_type=code&client_id=$APP_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback&scope=openid%20photos.read%20photos.delete%20offline_access&prompt=login&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"
Sign in as Ava with her password and approve. Check state and iss, then exchange the code. The app is a public client, so it sends its client ID and no secret:
read -rs CODE
RESP=$(curl -s "$ISSUER/oauth/token" -d grant_type=authorization_code --data-urlencode "client_id=$APP_ID" --data-urlencode "code=$CODE" \
--data-urlencode redirect_uri=http://127.0.0.1:8765/callback --data-urlencode "code_verifier=$VERIFIER")
TOKEN=$(jq -r .access_token <<<"$RESP"); REFRESH=$(jq -r .refresh_token <<<"$RESP"); ID_TOKEN=$(jq -r .id_token <<<"$RESP"); unset CODE RESP
btl-lab decode "$TOKEN"
btl-lab decode "$ID_TOKEN"
The access token carries auth_time, equal to or just before iat, and no acr or amr. The ID token, which is for the app, shows amr with the password value.
Why it matters: auth_time records when Ava last actively authenticated, not when the token was issued. It is a claim for the API; the app treats the access token as opaque.
Wait a minute, then refresh, and save the rotated refresh token at once:
RESP=$(curl -s "$ISSUER/oauth/token" -d grant_type=refresh_token --data-urlencode "client_id=$APP_ID" --data-urlencode "refresh_token=$REFRESH")
TOKEN=$(jq -r .access_token <<<"$RESP"); REFRESH=$(jq -r .refresh_token <<<"$RESP"); unset RESP
btl-lab decode "$TOKEN"
iat moved; auth_time did not.
Why it matters: a token obtained by refreshing describes the original sign-in, because no new authentication happened. Yesterday's sign-in can sit behind this morning's token.
The API sees the same value. Introspect as
lab-photo-api:
btl-lab introspect "$TOKEN"
The response includes active: true and auth_time, and has no acr or amr.
Why it matters: an API that relies on introspection can apply a freshness rule today, but not a strength rule.
A passkey sign-in. Open
$ISSUER/account/security, sign in as Ava and add a passkey. Then repeat step 1, choosing the passkey on the sign-in page. The ID token'samrchanges to the passkey method; the access token still carries onlyauth_time.
Why it matters: the API, which decides whether photos may be deleted, cannot tell a password from a passkey. Carrying that difference in the access token is exactly what acr is for.
Look for the server's list of authentication context values:
GET$ISSUER/.well-known/openid-configuration
Open in console
GET $ISSUER/.well-known/openid-configurationacr_values_supported is absent. Your tenant also ignores an acr_values parameter on an authorization request rather than rejecting it, which the specifications permit.
Why it matters: a client checks this list before sending a user off to satisfy a requirement, and an API can only ask for values the server defines.
Planned walkthrough
These steps need tenant-defined acr values (G20) and authentication context in access tokens (G48).
In Authentication (planned), the administrator defines two values:
urn:btl:acr:basicfor a password sign-in andurn:btl:acr:phishing-resistantfor a passkey or security key.acr_values_supportedin discovery then lists both.Repeat steps 1 and 4. The access token itself now carries
"acr": "urn:btl:acr:basic"after the password and"acr": "urn:btl:acr:phishing-resistant"after the passkey, set by the authentication engine.acris already a reserved claim name, so no claim mapping can write it.Repeat step 3: introspection returns
acrandamralongsideauth_time.
Break it
A missing claim never meets a requirement. In
lab-tmp-at-signin, remove theauth_timemapping and save. Repeat step 1. In its own terminal, start a local photo API whose deletion policy needs a sign-in within the last 120 seconds, then call it from your main terminal:
btl-lab resource --port 8766 --mode introspect --audience "$ISSUER/resource" --delete-max-age 120
curl -s -i -X DELETE -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8766/photos/9137 | grep -i -E '^HTTP|www-authenticate'
The API answers 401 with WWW-Authenticate: Bearer error="insufficient_user_authentication", max_age="120", although Ava signed in seconds ago.
Restore: add the auth_time mapping back to lab-tmp-at-signin and save. Stop the local API.
Why it matters: without the claim the API cannot know when Ava signed in, so it must refuse rather than assume.
Check your work
Press Check my progress. The checks look in Lab Photos for Ava's password sign-in and code exchange at lab-printer-app, the photo API's introspection and Ava's passkey enrollment. Audit also shows tenant.oauth.managers.create and the manager updates from Break it.
Cleanup
Keep lab-tmp-at-signin, lab-printer-app, Ava's passkey and the REFRESH value for the next lab, and confirm the auth_time mapping is restored. If you stop here, assign lab-printer-app back to the tenant default manager, delete lab-tmp-at-signin, and run unset TOKEN REFRESH ID_TOKEN.
Missing infrastructure
G20
acrandacr_values: tenant-definedacrvalues mapped to sign-in methods in the authentication policy,acr_valueshonored as a requirement at the authorization endpoint,acr_values_supportedin discovery, andacrin ID tokens.G48 Authentication context in access tokens:
acrandamrset by the protocol in access tokens and introspection responses. Today onlyauth_timecan be mapped, so step 4 cannot show the difference to the API.