2026-10-01 · 7 min read
- oauth
- jwt
- security
Step-up authentication asks a user who already has a token to authenticate again at a higher level. The resource server makes that demand with HTTP 401 and the OAuth error insufficient_user_authentication, specified in RFC 9470. The token is not necessarily expired, and the scope is not necessarily missing.
The client answers by starting a new authorization request with the acr_values and max_age from the challenge. Presenting the same access token again will not raise its authentication level.
Publish the classes your API will accept
acr is an authentication context class reference agreed with the authorization server. Document the strings that satisfy each route. Do not compare them as integers. A payroll route might require a class that means phishing-resistant multi-factor, while a profile route accepts a class that means a session cookie.
auth_time is the time the user authenticated, as seconds since the Unix epoch. max_age on the challenge says how old that event may be. A payment route can demand max_age of zero. A settings route can allow several hours. Refreshing an access token does not refresh auth_time unless the authorization server says it does.
- Write the accepted acr strings next to each sensitive route.
- Write the max_age in seconds, not milliseconds.
- Treat a missing acr as no assurance, not as the strongest class.
- Keep amr, the method references, as extra detail rather than a silent substitute for acr.
Return a challenge the client can follow
The WWW-Authenticate header uses the Bearer scheme and error insufficient_user_authentication. Add acr_values as a space-separated list when a class is required. Add max_age when freshness is required. Keep insufficient_scope for missing authorization, so clients do not send the user through a password prompt when they only need a different grant.
The resource server should check signature, issuer, audience, and expiry before it asks for step-up. A token that fails those checks is invalid_token. Step-up is for a token that is otherwise acceptable and still not strong enough.
Make the new authorization request match the challenge
Copy acr_values and max_age onto the authorization request. When max_age is zero, the user must authenticate in that request. After the new token arrives, the resource server checks the claims again. A token whose acr is not in the list, or whose auth_time is older than max_age, is still insufficient.
Store the step-up decision beside the route, not in a global client cache keyed only by subject. A token that is strong enough for one API can be weak for another that shares the same authorization server.
Avoid logging tokens while you test
Log the error code, the requested acr_values, the max_age, and a correlation id. Do not log the bearer token, the refresh token, or a reversible encoding of either. Use synthetic tokens in tickets.
Test a missing acr, an unlisted acr, an auth_time older than max_age, a refreshed token that kept the old auth_time, a scope failure, and an expired token. Only the first four are step-up cases.
Inspect claims locally
Use Flashman's JWT decoder for synthetic access tokens, the JSON formatter for the challenge parameters, the timestamp converter for auth_time and max_age, the URL tool for the authorization endpoint, and diff to compare the rejected claims with the claims issued after the user authenticates again.