Require two-factor authentication
This guide makes everyone who signs in to your app enter a one-time code as well as their password.
That’s two-factor authentication: the code is six digits from an authenticator app on the user’s phone, such as Google Authenticator, Microsoft Authenticator or FreeOTP, and it changes every 30 seconds. How strongly a user must sign in to a client is the client’s ACR level, and the strongest one asks for a code at every sign-in that starts a new session.
Require a code for your app
Section titled “Require a code for your app”-
In the admin console, open your client under Admin, Clients. On the Settings tab, set Default ACR level to Level 3: password and a one-time code, always, which is
urn:goiabada:level2_mandatory, and click Save. Sign-ins that start after the change use it. -
Sign in to your app as a user with no authenticator. After the password, the sign-in page asks you to set one up: scan the QR code with an authenticator app, then enter the code it shows. From then on, a sign-in that starts a new session asks for your password and then a code. While your session lasts, it’s reused: signing in to your app again, or to another app at the same level, asks for neither. A request that asks for a fresh sign-in, with
prompt=login, asks for both, whatever the session already gave. See Users who are already signed in. -
In your app, check how the user signed in before you let them in. The ID token says it in two claims,
acr, the level, andamr, the methods the user used:{"acr": "urn:goiabada:level2_mandatory","amr": ["pwd", "otp"]}Accept the sign-in only when
amrhasotp. The client’s setting already makes the auth server ask for the code, so this check is what keeps your app safe if someone lowers that setting later. -
If your app calls an API of yours, the access token carries the same
acrandamr, so the API can make the same check. See Protect an API.
To ask for a code only before one sensitive operation, such as a payment, leave the client’s level as it is and send acr_values=urn:goiabada:level2_mandatory on that one authorization request. ACR and AMR shows how. It asks for a level, not for a new code: a user whose session already reached that level isn’t asked again. To require a recent sign-in as well, add max_age, as Sessions explains.
Require a code for administrators
Section titled “Require a code for administrators”The admin console signs administrators in through its own client, admin-console-client, so the same setting makes every administrator use a code.
-
Set up an authenticator for yourself first, under Account, Two-factor authentication. That way you know it works before it’s required.
-
Open Admin, Clients,
admin-console-client, and on the Settings tab set Default ACR level to Level 3: password and a one-time code, always, then click Save. Only an administrator withauthserver:managecan change this client. -
The next time each administrator signs in to the admin console, they’re asked for a code, and one without an authenticator sets one up first.
Setting up an authenticator
Section titled “Setting up an authenticator”A user can set up an authenticator before any client requires one, in the admin console under Account, Two-factor authentication: they scan the QR code, enter their password and the code the app shows, and click Turn on two-factor authentication. A client at level 2, the level new clients start with, then asks them for a code too.
At level 3, a user with no authenticator sets one up during the sign-in, on the page that asks for the code. Nothing is stored if their two-factor settings changed while they were doing it, for example in another tab: the sign-in ends, and they sign in again. See ACR and AMR.
The authenticator app lists the account under the auth server’s App name and the user’s email address.
The code
Section titled “The code”The code is a time-based one-time password (RFC 6238): six digits, a new one every 30 seconds. The auth server also accepts the code from the 30 seconds before and after, so a phone whose clock is a little off still works.
Each code works once. Sent again, it’s refused, and the audit log records otp_code_replay_detected. Each user gets 5 wrong codes in 15 minutes, and then waits. See Too many attempts or 429.
100 wrong codes in a row, with no right one between them, disable the user’s authenticator. From then on every code is refused, the right one included, and the audit log records otp_authenticator_disabled. Someone who knew the user’s password, or held one of their sessions, sent those codes, so treat the password as known. The user’s Authentication tab in the admin console says the authenticator is disabled, and A user who lost their authenticator is the way back.
Users who are already signed in
Section titled “Users who are already signed in”A user whose session already reached the client’s level isn’t asked for anything: the session is reused, and the tokens say urn:goiabada:level2_mandatory and ["pwd", "otp"] from the sign-in that earned it.
prompt=login doesn’t reuse the session: the user signs in again with their password and a code, whatever their session already gave, and the tokens say ["pwd", "otp"] and auth_time of this sign-in. A session that has idled out, reached its maximum lifetime or fallen outside the request’s max_age doesn’t count either, and the user is asked for both.
A user who signed in to another app with a password alone isn’t asked for their password again. They’re asked only for the code, which is a step-up, and their session keeps the higher level from then on.
When a user sets up or removes an authenticator, each of their sessions is checked again for a second factor before it’s used for a level above urn:goiabada:level1: a user with an authenticator enters a code, and a user with none left is asked for nothing at level 2 and sets up a new one at level 3.
A user who lost their authenticator
Section titled “A user who lost their authenticator”An administrator opens Admin, Users, the user, and their Authentication tab, turns off Enabled under Two-factor authentication, clicks Save, and confirms. At level 3, the user sets up a new authenticator at their next sign-in.
The same steps bring back a user whose authenticator was disabled after 100 wrong codes, with one more. Whoever sent the codes knew the password, so before clicking Save, the administrator also types a new one in New password on the same tab. The save sets the new password first, which signs the user out everywhere and revokes every refresh token they hold, and then turns two-factor authentication off. The user signs in with the new password and sets up a new authenticator.
Turning it off lowers each of the user’s sessions to what a password alone reaches, so no token issued from them afterwards claims a code. Nobody is signed out, though, and apps the user was already signed in to keep refreshing their tokens with the claims they had. If the device could be in someone else’s hands, also end the user’s sessions: on their Sessions tab, choose End session beside each one, and confirm. That signs every browser out and revokes the refresh tokens each session gave out. A session left open is enough to set up a new authenticator at a level 3 sign-in, since that asks only for what the session doesn’t already have. See Ending sessions.
A user who still has their authenticator can turn it off themselves, with their password, under Account, Two-factor authentication and Turn off two-factor authentication. At level 3, they then set up a new one at their next sign-in to your app.
What a code can’t protect
Section titled “What a code can’t protect”- A silent request (
prompt=none) can’t show the code page. When a code is due, it getsinteraction_required. See prompt. - The password grant (ROPC) can’t ask for a code, so it refuses every user who has an authenticator, and everyone at a level 3 client.
- The client credentials flow has no user, so nothing asks for a code.