Skip to content
TDACorpIDENTITYSign in

acr and amr values

What the acr and amr claims say about how a person signed in, how to demand a stronger level, and how to check it.

On this page

Two claims describe how a person authenticated. acr is the level reached, a single value you can compare. amr is the list of methods used, which you can log or display. Use acr to make decisions.

acr#

ValueLevelReached by
aal1One factorA password only (including a limited session the person chose with "Continue with my password only"), a magic link, or a sign-in through an external provider without a second factor.
aal2Two factors, or one strong oneA passkey with user verification, or a password followed by a second factor such as an authenticator app code, or an external provider that asserts a second factor.
aal3Hardware-backedA passkey with user verification from an attested hardware authenticator. Identity does not verify attestation yet, so no sign-in reaches this level today. Status: Planned.

The levels are ordered: aal3 is stronger than aal2, which is stronger than aal1. Identity also has second steps that do not raise the level: a password typed after a magic link or an external sign-in, and an emailed code typed after a password, each stay at aal1.

acr_values_supported in discovery lists the three values.

The acr in a token is the level of the session at the moment the token was issued. A token obtained by refresh reports the session's current level.

amr#

amr is an array of short method names. A session usually has more than one.

ValueMethod
pwdA password.
otpA one-time code from an authenticator app.
hwkProof of possession of a hardware-bound key, as a passkey does.
userThe authenticator verified the person (a PIN or biometric). Appears with hwk.
fedA sign-in through an external identity provider.
mfaA second factor was done at that provider. Appears with fed.
mlA magic link sent by email.
rcvA recovery code used as a second factor. Not an RFC 8176 value.
emlA code sent by email as a second step. Not an RFC 8176 value.

Do not assume the list is closed, and do not parse meaning out of its order. Log what you receive and compare only against values you know.

Asking for a level#

Send acr_values when you push the authorization request, with the lowest level you will accept. Identity then makes sure the session reaches it, asking the person for a stronger method if the session is weaker.

An account that holds a passkey or an authenticator app is not asked for it on every sign-in: a correct password signs in at aal1 on a browser the account knows, and an unusual sign-in can end as a limited aal1 session the person chose. An application that needs two factors must therefore send acr_values=aal2, and check acr on what comes back.

You sendWhat identity does
acr_values=aal2A session at aal1 is stepped up. The person is asked to confirm with a stronger method before a code is issued.
max_age=300A session whose last sign-in is older than 300 seconds is asked to sign in again, or, when acr_values asks for aal2 or more, to confirm a fresh second factor.
prompt=loginAlways asks again, even with a live session.
prompt=none with a level the session lacksNo page: the error login_required comes back to your redirect address.

acr_values takes a space-separated list of aal1, aal2, aal3. Any other value is invalid_request. With several, identity enforces the highest one for the session. The SDK's ID token check only requires the token to reach the lowest.

With the SDK:

const { url, transaction } = await rp.startAuthorization({  scope: 'openid',  acrValues: ['aal2'],  maxAge: 300,})

Checking the result#

Never trust that identity did what you asked. Check the token.

  • ID token. Verify that acr reaches the lowest value you asked for, and that auth_time is within max_age. The SDK does both in handleCallback; the failures are IdTokenError with reasons acr and max_age.
  • Access token at your API. For an action that needs a stronger level, read acr and auth_time from the verified token and refuse with a 401 that asks the client to authenticate again if they fall short. The standard form is the RFC 9470 challenge error="insufficient_user_authentication" with the acr_values and max_age you need. The SDK's requireAuthentication sends it, and a client answers it with rp.stepUpUrl; see Resource-server verification.

A token's level can be higher than you asked for. It is a floor, not an exact match.

An organization that requires single sign-on (behind the sso_enforcement flag, which defaults OFF) can answer a code exchange, a refresh or a token exchange with insufficient_user_authentication, acr_values=aal1 and a max_age. The level asked for is the lowest because the requirement is a recent sign-in through the organization's connection, which max_age expresses. For such an organization the auth_time of a token issued from a new authorization is the time of that single sign-on, so a max_age check by your API judges it.

Worked example#

An API requires aal2 fresher than ten minutes for a payout.

const token = await resourceServer.verifyRequest(request)const age = Date.now() / 1000 - Number(token.claims.auth_time)if (token.acr !== 'aal2' && token.acr !== 'aal3') return stepUpRequired('aal2')if (age > 600) return stepUpRequired('aal2')

stepUpRequired is yours (with the SDK, requireAuthentication does this check and sends the challenge). It tells your client to start a new authorization with acr_values=aal2 and max_age=600, then retry.

Security notes#

  • Compare levels as an ordered set, not strings. aal3 satisfies a demand for aal2.
  • Do not decide from amr alone. A method name does not say how strong the whole sign-in was. acr does.
  • Re-check at the point of use. A level reached at sign-in does not mean the person is still at the keyboard. Use auth_time for freshness.

Verified against commit ca46b57