Following a step-up challenge
At 09:02 UTC you open the editor's tidy-up view and choose Delete 38 photos. The editor holds the access token it obtained this morning by refreshing, which still reflects yesterday's password sign-in, and it starts with the first photo on the list.
Receiving the challenge
All domains, codes, and tokens in these examples are fictional. The editor sends the deletion with its current token:
DELETE /photos/9137 HTTP/1.1
Host: api.photos.example
Authorization: Bearer demo-access-token-31
The photo API validates the token as usual. Its signature, issuer, audience, and expiry are fine, and it carries photos.delete. Then the API applies its policy for deletions: acr must be urn:example:photos:acr:phishing-resistant, and auth_time must be no more than 300 seconds ago. The token says basic, and its sign-in happened 86,520 seconds ago. The API refuses:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="insufficient_user_authentication",
error_description="A recent phishing-resistant sign-in is required",
acr_values="urn:example:photos:acr:phishing-resistant",
max_age="300"
The header is split across lines for display. error names the problem: the authentication behind this token does not meet the API's requirements. acr_values lists the acceptable authentication context classes, separated by spaces and in order of preference, and the API will accept any one of them. max_age is the number of seconds allowed since your last active authentication: the last time you responded to a sign-in prompt at the authorization server. A session that quietly continues does not count. A challenge can carry either requirement or both, and if the token also lacked a scope, it could include a scope attribute as well.
The response uses status 401, as an invalid token would, because the remedy is a different token. The error_description is optional text meant for developers. As with other error descriptions, the editor does not insert it into its page as markup.
Asking for a new sign-in
The editor remembers what it was doing, the 38 photos waiting to be deleted, and tells you that deleting photos needs a fresh sign-in with your passkey. When you choose to continue, it builds a new authorization request, copying the two requirements from the challenge exactly as it received them:
https://auth.photos.example/authorize
?response_type=code
&client_id=photo-editor
&redirect_uri=https%3A%2F%2Feditor.example%2Foauth%2Fcallback
&scope=photos.read%20photos.delete
&acr_values=urn%3Aexample%3Aphotos%3Aacr%3Aphishing-resistant
&max_age=300
&state=demo-attempt-31
&code_challenge=gmBRnRQIy0EQKWg3CemaN64l-xOox_nHZAphyPcG6hI
&code_challenge_method=S256
Everything else is an ordinary authorization request, with a fresh state and PKCE challenge for this attempt. The requirements are the only new part.
At the photo service, you still have yesterday's session. On an ordinary visit, the server would recognize you and continue without a prompt. This time, max_age=300 tells it that a sign-in more than five minutes old is too old, so it must ask you to authenticate again. acr_values tells it which kind of sign-in to ask for, so it prompts for your passkey rather than your password. You use your passkey at 09:03:00.
Under the step-up specification, the authorization server should treat the requested acr as a requirement, not a preference. It puts that value in the new access token only when the sign-in actually met it. If the sign-in falls short, the request fails instead of producing a token the API would refuse.
Your browser returns to https://editor.example/oauth/callback with a code, the attempt's state, and the iss parameter. The editor checks them as it would for any callback, then exchanges the code with its PKCE verifier. It is a public client, so it sends its client ID and no client credential.
Retrying with the new token
The new access token, decoded, describes this morning's passkey sign-in:
{
"iss": "https://auth.photos.example",
"sub": "user-2048",
"aud": "https://api.photos.example",
"client_id": "photo-editor",
"scope": "photos.read photos.delete",
"iat": 1790845385,
"exp": 1790845985,
"jti": "demo-token-id-32",
"auth_time": 1790845380,
"acr": "urn:example:photos:acr:phishing-resistant"
}
auth_time is 09:03:00, and the token was issued five seconds later. The editor does not decode the token to confirm any of this. It repeats the request it was trying to make and lets the API decide:
DELETE /photos/9137 HTTP/1.1
Host: api.photos.example
Authorization: Bearer demo-access-token-32
HTTP/1.1 204 No Content
The API runs the same checks as before. At 09:03:10, the sign-in is 10 seconds old, well inside the 300 allowed, and its acr is one the API accepts. The photo is deleted, and the editor continues with the other 37.
The new sign-in will not stay fresh. At 09:08:00 it turns five minutes old, and from then on this token no longer meets the deletion policy, although it still works for browsing until it expires. If the tidy-up were still running then, the API would challenge again, and the editor would have to decide whether to ask you for another sign-in or stop.