When an API needs stronger authentication
The photo editor at editor.example, a browser application registered as the public client photo-editor, has a tidy-up tool. It finds blurry shots and duplicates and, when you agree, deletes them from your library. You connected the editor with photos.read and photos.delete. You last signed in to the photo service yesterday morning, with your password, and the editor has kept the connection working with its refresh token ever since.
Deleting photos cannot be undone. The photo service wants each deletion to come from someone who proved who they are recently, and with a method that a phishing page cannot capture. A copied token, or a laptop left open with yesterday's session, should not be enough to empty an album.
A request that needs more
The Authentication policy and SSO lesson described two ways a service responds to a sensitive action: reauthentication, which asks for a fresh check, and step-up authentication, which asks for stronger evidence than the person gave earlier. The photo service needs both here. The difference in OAuth is where the question comes up.
The deletion arrives at the photo API as an ordinary request with an access token. Only the API sees that this request deletes photos, and it may weigh more than the operation itself: how many photos, how quickly, from which kind of client. The authorization server authenticated you yesterday and has been issuing tokens since, without knowing what any of them would be used for.
The simple alternatives cost too much. Requiring a passkey at every sign-in, or making every token last a few minutes, would make each ordinary visit harder in order to protect a rare one. What the API needs is narrower: a way to learn from the token how and when you authenticated, and a way to tell the client that this request needs more.
What a token can say about sign-in
The Authentication policy and SSO lesson met the two claims that carry this information, auth_time and acr, in OpenID Connect, whose lessons explain them in full, starting with Session age and reauthentication and Authentication context and methods. The JWT access token profile lets an access token carry them too, describing the sign-in behind the authorization.
auth_time records when you last actively authenticated at the authorization server, by typing a password or using a passkey, for example. Like other JWT times, it counts seconds since 1 January 1970 UTC. It is not the time the token was issued.
acr, short for authentication context class reference, names the kind of authentication that took place. Its values are strings that the authorization server and the API have agreed on, usually written as URIs. The photo service defines two:
| Value | Meaning at the photo service |
|---|---|
urn:example:photos:acr:basic | A sign-in with a password. |
urn:example:photos:acr:phishing-resistant | A sign-in with a passkey or security key. |
There is no universal ladder of acr values. "Stronger" means only what the parties have agreed, and the word "step-up" is a metaphor for moving from a level the API will not accept to one it will.
All domains, identifiers, and tokens in these examples are fictional. Decoded, the editor's current access token reads:
{
"iss": "https://auth.photos.example",
"sub": "user-2048",
"aud": "https://api.photos.example",
"client_id": "photo-editor",
"scope": "photos.read photos.delete",
"iat": 1790845200,
"exp": 1790845800,
"jti": "demo-token-id-31",
"auth_time": 1790758800,
"acr": "urn:example:photos:acr:basic"
}
The token was issued at 09:00 UTC today, when the editor last refreshed, but auth_time is 09:00 yesterday. Tokens obtained by refreshing carry the same auth_time and acr as the sign-in behind the authorization, because no new authentication happened. An API that relies on introspection can receive the same two values in the introspection response.
These claims are for the API. As the Using access tokens lesson explained, the editor treats the token as opaque and does not read them.
Letting the API ask
The OAuth 2.0 Step Up Authentication Challenge Protocol, published as RFC 9470 in September 2023, gives the API a standard way to say that a token's authentication is not enough. The exchange has four steps:
- The API refuses the request with the error
insufficient_user_authenticationand states what it requires: acceptableacrvalues, a maximum age for the sign-in, or both. - The client sends you back to the authorization server with a new authorization request carrying those requirements.
- The authorization server authenticates you as required and issues a new access token whose
acrandauth_timedescribe that sign-in. - The client repeats the original request with the new token, and the API checks again.
The requirements travel in two OpenID Connect authorization request parameters, acr_values and max_age. An authorization server that already implements OpenID Connect can take part with little or no change, and it advertises support by listing acr_values_supported in its metadata.
Step-up changes what the API knows, not what the editor knows. The editor still receives an access token, not a statement about who you are, and the specification is explicit that this mechanism does not turn OAuth into a sign-in protocol. The API's other checks stay in place as well. A fresh passkey sign-in does not let the editor delete photos in an album that is not yours.