Multi-factor authentication
Ask for a second factor after the first: a code by e-mail, an approval in the Authio app or a passkey, and step up when your app needs a stronger sign-in.
In Authio, multi-factor is not a switch on the account: it comes from the sign-in steps each client asks for. Put a second-factor step after the first one and the hosted login asks for it, skipping any factor the sign-in already presented.
This page covers the three second factors, when each one is asked, the Authio Authenticator app that answers push approvals, what to do when a phone is lost, and how your app can ask for a stronger sign-in.
The second factors
| Factor | Step | What the person does | Recorded in amr |
|---|---|---|---|
| One-time code by e-mail | mfa | Types the 6-digit code sent to the account’s e-mail address. | otp |
| Push approval | push | Taps, in the Authio app, the number shown on the sign-in screen. | mca, plus user when the phone verified the person |
| Passkey | passkey | Unlocks a passkey with the device’s fingerprint, face or screen lock. | swk, plus user when the device verified the person |
When the second factor is asked
The client’s steps decide, in the order they are listed. The first step that can answer the factor still owed is the one shown:
| Step | Asked of |
|---|---|
mfa | Every sign-in that reaches it. The step cannot be Optional: a code that may be skipped is no factor at all. |
push, Preferred | Accounts with a paired phone. Others are offered to pair one, unless registration is Never. |
push, Required | Every account. One without a phone has to pair before continuing, so registration has to allow it. |
passkey, Preferred | Accounts that already registered a passkey. See Passkeys. |
passkey, Required | Every account. |
| Any step, Optional | Nobody, unless an authorization request asks for a higher level with acr_values (see step-up). |
A sign-in through an identity provider counts as one factor, whatever the provider did on its side, and the client’s second-factor steps apply after it. Single sign-on into another client is only offered when nothing that client requires is missing.
One-time code by e-mail
After the first factor, Authio sends a 6-digit code to the account’s e-mail address and the screen shows six digit boxes, filled by the browser’s one-time-code autofill where it is available. Didn’t get the code? Resend it sends a new one.
- The same code is delivered to every phone paired with the account, where the Authio app shows it as Your sign-in code. The code never travels in the notification itself: the app fetches it over an authenticated connection while it is valid.
- A wrong code counts toward the realm’s lockout and is recorded in the access log.
- After a correct code the account is checked again — status, suspension, access to the client, sign-in address — before the session opens.
Push approval
Push approval uses the Authio Authenticator app for iOS and Android, called “the Authio app” on the sign-in screens. It defends against push fatigue — someone who has the password sending requests until the victim approves one to make the phone stop — with number matching:
The screen shows a number
After the first factor, the sign-in screen shows Tap this number in the Authio app with a two-digit number, and waits.
Every paired phone is asked
Each phone of the account receives a notification. The app shows the application, the IP address and browser of the attempt, and three numbers.
The person taps the matching number
Tapping the number is the approval. The app asks for biometrics or the screen lock when the phone has one, then signs the answer with a key created when it was paired. It wasn’t me denies the request.
The sign-in continues by itself
The screen sees the answer and carries on. Authio re-reads the approved request on the server before completing the sign-in; nothing the browser sends is taken as proof.
| Rule | Value |
|---|---|
| A request is valid for | 2 minutes |
| Numbers offered on the phone | Three distinct numbers from 10 to 99 |
| Tapping the wrong number | Denies the request, with no second try |
| Requests per account | At most 10 in 5 minutes, counted per account rather than per IP |
| Several paired phones | All are asked with the same number; the first answer decides |
| Approval | Accepted only with a valid signature from the phone’s own key |
While waiting, the screen offers Ask my phone again, which sends a new request with a new number, and Use another way to sign in whenever another of the client’s steps can answer, such as the e-mail code. A notification that never arrives does not block the sign-in: the app also finds pending requests when it is opened.
Pairing a phone
Pairing is always started by the account holder. There are two places to do it:
- During sign-in, when the
pushstep offers or requires registration: the screen shows Set up your phone with a QR code to scan in the app, and Can’t scan it? reveals the same content as a line to paste. With registration Offer, Set it up later skips it. - From the account pages, on the Authio app tab, with Show the pairing code. See Account self-service.
A pairing code works once, for 5 minutes, and showing a new one retires the previous one. An account can have up to 5 active phones.
Lost phone or no access to the factor
- On the sign-in screen, Use another way to sign in moves to the e-mail code when the client also has the
mfastep. - When a passkey is owed and is not on the device, the screen offers to send a code by e-mail instead. See Passkeys.
- The person removes a lost phone from the Authio app tab of the account pages, or from another paired phone. A removed phone stops approving anything at once.
- An administrator removes it from the user’s Credentials tab in Users, or from Push Authenticator.
Phones are revoked, never deleted, so the history of what each one approved stays readable. Using a revoked phone again means pairing it again.
Step-up from your app
The ID token says how the person signed in: amr lists the methods, acr sums them up as a level, and auth_time says when. A single sign-on reports the original sign-in, never a fresh one.
acr | Meaning | Example |
|---|---|---|
1 | One factor | A password; a sign-in through Google |
2 | More than one factor | Password and e-mail code; Google and a push approval |
3 | Multi-factor where one factor resists phishing | A passkey verified by the device, alone or after a password |
When an action needs more than the current session proves, send the person through the authorization endpoint again with what you need:
acr_values=2oracr_values=3asks for a level. When the client’s own steps would not reach it, one Optional second-factor step the account can answer is asked as if it were Preferred.max_age=300asks for a sign-in no older than five minutes.prompt=loginasks for the credentials again whatever the session.
https://authio.api.hyperio.tech/realms/acme/protocol/openid-connect/auth
?client_id=acme-web
&response_type=code
&redirect_uri=http://localhost:3000/api/auth/callback/authio
&scope=openid profile email
&state=STATE
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
&acr_values=2
&max_age=300acr_values never refuses a sign-in
interface SignInClaims {
acr?: string
auth_time?: number
}
export function isStrongEnough(
claims: SignInClaims,
minimumAcr: number,
maxAgeSeconds: number,
): boolean {
const level = Number(claims.acr ?? '0')
const ageSeconds = Math.floor(Date.now() / 1000) - (claims.auth_time ?? 0)
return level >= minimumAcr && ageSeconds <= maxAgeSeconds
}Watching push approval
Push Authenticator in the console shows, for a realm or all of them, every request sent to a phone and how it ended — approved, denied, unanswered or never delivered — with response times, the most active applications and the paired phones, including revoked ones and phones that cannot be notified. Requests that never reached a phone are highlighted, because neither the person nor the administrator would notice otherwise.