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#
| Value | Level | Reached by |
|---|---|---|
aal1 | One factor | A 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. |
aal2 | Two factors, or one strong one | A 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. |
aal3 | Hardware-backed | A 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.
| Value | Method |
|---|---|
pwd | A password. |
otp | A one-time code from an authenticator app. |
hwk | Proof of possession of a hardware-bound key, as a passkey does. |
user | The authenticator verified the person (a PIN or biometric). Appears with hwk. |
fed | A sign-in through an external identity provider. |
mfa | A second factor was done at that provider. Appears with fed. |
ml | A magic link sent by email. |
rcv | A recovery code used as a second factor. Not an RFC 8176 value. |
eml | A 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 send | What identity does |
|---|---|
acr_values=aal2 | A session at aal1 is stepped up. The person is asked to confirm with a stronger method before a code is issued. |
max_age=300 | A 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=login | Always asks again, even with a live session. |
prompt=none with a level the session lacks | No 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
acrreaches the lowest value you asked for, and thatauth_timeis withinmax_age. The SDK does both inhandleCallback; the failures areIdTokenErrorwith reasonsacrandmax_age. - Access token at your API. For an action that needs a stronger level, read
acrandauth_timefrom the verified token and refuse with a401that asks the client to authenticate again if they fall short. The standard form is the RFC 9470 challengeerror="insufficient_user_authentication"with theacr_valuesandmax_ageyou need. The SDK'srequireAuthenticationsends it, and a client answers it withrp.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.
aal3satisfies a demand foraal2. - Do not decide from
amralone. A method name does not say how strong the whole sign-in was.acrdoes. - 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_timefor freshness.