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

Handling unmet authentication requirements

The challenge in Following a step-up challenge ended well because you had a passkey and used it. Not every attempt ends that way. You might never have set up a passkey, or you might cancel. A server might be unable to provide the kind of sign-in an API asks for, and even a successful step-up stops being fresh after a few minutes.

Each of these needs an ending that leaves your photos either deleted with the evidence the photo service asked for, or not deleted, and leaves you knowing which.

When the requirement cannot be met

All domains and values in these examples are fictional. Suppose you have no passkey. The photo service asks for one, and you have nothing to offer except your password. The server could sign you in with the password and issue a token marked basic, but the photo API has already said it will not accept that. The step-up specification recommends that the server fail the request instead, with the error unmet_authentication_requirements. It comes from a short OpenID Foundation companion to OpenID Connect, written for an authorization server that cannot authenticate someone in the way the client asked, and Requesting stronger authentication meets it again in OpenID Connect:

https://editor.example/oauth/callback
  ?error=unmet_authentication_requirements
  &state=demo-attempt-31
  &iss=https%3A%2F%2Fauth.photos.example

If you had cancelled instead, the response would carry access_denied. Either way, the editor validates state and iss as it would for any response, then tells you what happened in plain terms: the photos were not deleted, deleting photos needs a recent sign-in with a passkey, and you can add a passkey in your photo account settings and try again or keep the photos. The editor cannot change the requirement, and it should not hint at ways around it.

A requirement can be impossible for some people to meet, and the specification leaves those policy choices to each deployment. If the photo service made deletion depend on passkeys without giving people a way to add one, every deletion would fail. The Authentication policy and SSO lesson described the same problem when Cedar Inc. required security keys before payroll staff could enroll them. The fix belongs in the policy and enrollment, not in the protocol.

Avoiding loops

Picture a server that ignores acr_values and issues another basic token. The API challenges, the editor sends you to sign in, the server issues another basic token, and the API challenges again. You would keep signing in and nothing would change. That loop is why the specification asks the server to fail the request rather than issue a token that does not meet the requirement.

The editor protects against loops on its side too:

  • It starts a step-up only from something you chose to do, such as deleting photos. A background request, such as loading thumbnails, never sends you to a sign-in page.
  • It makes one step-up attempt for each action. If the API challenges the new token as well, the editor stops and explains instead of redirecting again.
  • It does not refresh to satisfy a challenge. A refreshed token keeps the auth_time and acr of the original sign-in, so it would fail the same check.
  • Before sending you anywhere, it can check that the requested values appear in the server's acr_values_supported metadata. A requirement the server does not support cannot be met by trying.

Keeping separate tokens

After a successful step-up, the editor holds two kinds of token: the everyday one from yesterday's password sign-in, and the stepped-up one from this morning's passkey. The stepped-up token meets the deletion policy only for five minutes. After that it is no better for deleting than the everyday one.

The specification notes that a client may do better by keeping both and choosing the right one for each request, rather than replacing the old token with the new. Using the stepped-up token for everything gains nothing once it is stale, and a server that issues stepped-up tokens with short lifetimes would otherwise interrupt everyday browsing with frequent prompts. The editor uses the everyday token for reading and the stepped-up token for deleting, and it starts a new step-up if a later deletion is challenged.

The editor chooses between them by remembering why it obtained each token, not by reading the acr inside it. The token is opaque to the client, and the photo service could change its format at any time.

The step-up request can also ask for less. If it requested only photos.delete, the token from the passkey sign-in would carry only the access that needed it, while reading stayed with the everyday token.

The API's checks

On the photo API's side, the deletion policy becomes a short check that runs after the token has been validated and its scope confirmed:

const deletePolicy = {
  acr: ['urn:example:photos:acr:phishing-resistant'],
  maxAge: 300,
};

// Returns null when the sign-in is good enough, or a challenge to send back.
function checkSignIn(claims: AccessTokenClaims, now: number): string | null {
  const strongEnough = typeof claims.acr === 'string'
    && deletePolicy.acr.includes(claims.acr);
  const recentEnough = typeof claims.auth_time === 'number'
    && now - claims.auth_time <= deletePolicy.maxAge;
  if (strongEnough && recentEnough) return null;
  return 'Bearer error="insufficient_user_authentication", '
    + `acr_values="${deletePolicy.acr.join(' ')}", max_age="${deletePolicy.maxAge}"`;
}

A missing claim counts as not meeting the requirement. The acr value is compared exactly with the accepted list, and the challenge is built only from the API's own policy, never from anything in the request. A real implementation would also allow a few seconds of tolerance for clock differences, as it does for exp.

The order matters. The specification allows an API to return a challenge before validating the token, but then anyone can learn the requirements without holding a valid token. The requirements themselves can say more than intended: an API that demands a stronger sign-in only for some accounts tells an observer which accounts are worth targeting with phishing.

A challenge is a request for a better token, not a source of authority. The API never relaxes its rule because a client says you signed in again; only the token counts. In the other direction, a challenge can make a client send you to a sign-in prompt, so a malicious API could use challenges to interrupt you with prompts, or to make unexpected prompts seem normal. The editor follows challenges only from the photo API it was built to call, and the prompt itself appears at the photo service, where you can see what you are being asked to do.

Try it in the Lab

PUT IT INTO PRACTICE

Check your understanding

Try these questions before moving on. If an answer isn't right, use the feedback and try again.

0 of 2 answered correctly

Enable JavaScript to answer these questions and save progress in this browser.

QUESTION 1 OF 2You have no passkey, and the step-up request asks for a phishing-resistant sign-in. What should the authorization server do?

QUESTION 2 OF 2After one step-up, the photo API challenges the editor's new token as well. What should the editor do?

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

Learn identity