# Multi-factor authentication Add a second factor to your users' accounts without building any screens. Your tenant's hosted pages run enrollment, the sign-in challenge, and recovery; you choose the policy. The factor is **TOTP** — any standard authenticator app, enrolled by QR code or by typing the secret. Enrollment issues **10 single-use recovery codes**, shown once at that moment and stored only as hashes, so a user who loses the authenticator can still get in. Enrolled secrets are sealed with authenticated encryption at rest. Attempt limits follow your tenant's protection mode, described below. ## Where your users meet it - **Enrollment and management** live on the hosted `/security` page, under **Two-step sign-in**: set it up, see its status, and turn it off — when your policy allows either (see below). - **The challenge** happens before Udibo creates a new tenant session. Password, social, and passwordless sign-in all pass through the same gate, so no method skips the second factor. - **Recovery codes** are accepted at the challenge in place of a TOTP code; each works once. To enroll while already signed in: 1. Open `https://{tenantId}.udibo.com/security` as an end user in your tenant. This is the person's account page, separate from your Udibo dashboard account. 2. Under **Two-step sign-in**, choose **Set up authenticator app**, enter the current password, and choose **Continue**. An account with no password first chooses **Confirm by signing in with** a provider the account already uses and comes back to the page; an account that signs in only by emailed code or link chooses **Confirm by email** instead, then enters the emailed sign-in code (or opens the emailed link) in the same browser. Either then chooses **Set up authenticator app**. An open session alone cannot add a factor. 3. Scan the QR code in an authenticator app, or enter the displayed key manually. 4. Enter the app's six-digit **Authenticator code** and choose **Verify**, in the same signed-in session and within 10 minutes of confirming it's you. A mistyped code can be retried. 5. Save the ten recovery codes. They are shown only once; the page then shows that two-step sign-in is on and how many recovery codes remain. Whenever an authenticator is added, here or during sign-in, the account's verified email address receives a notice with a link to `/security`, where the factor can be turned off. With required policy, an unenrolled person meets the same enrollment steps during sign-in, right after their first sign-in step, and is not asked to confirm again. With off policy, an unenrolled account has no setup control. An account with no password and no connected provider (one that signs in only by emailed code or link) cannot set up two-step sign-in from `/security`. ## Save and replace recovery codes At the sign-in challenge, choose **Use a recovery code instead**, enter one saved code, and choose **Verify**. Using a recovery code satisfies the second factor; the person still needs to complete the first sign-in step. That recovery code cannot be used again. Once signed in, open the tenant's `/security` page and choose **Regenerate recovery codes**. Confirm with **Current password** if the account has a password, or with **Authenticator or recovery code** if it does not, then choose **Regenerate**. Save the new set: it is shown once and immediately replaces every old recovery code, including unused ones. The page shows the remaining count and warns when codes run low or run out. **Turn off two-step sign-in** asks for the same confirmation, then **Turn off**. Required policy refuses this action. Changing authenticator apps requires removing the enrolled factor and enrolling again when policy allows. Under required policy, an [administrator reset](https://www.udibo.com/docs/identity/mfa#resetting-a-users-mfa) starts enrollment again at the next sign-in. Use that reset too when a person cannot recover the account. ## Setting the policy In the dashboard, open your tenant's **Security** page. Under **Sign-in methods** → **Two-step sign-in**, set **Two-step sign-in policy** to **Optional**, **Required**, or **Off**, then choose **Save two-step sign-in policy**. Saving requires **Manage tenant settings**. The built-in Owner and Admin roles carry it; Support does not, and sees the current policy without a save. ## The policy One tenant-level knob, `mfaPolicy`, with three values: | Value | Meaning | | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `off` | No one can newly enroll. **A factor that is already enrolled is still challenged** — see below. | | `optional` | Users may enroll themselves; enrolled users are challenged at sign-in. The default. | | `required` | Everyone is challenged: an unenrolled user is taken through enrollment at their next sign-in, and users cannot turn their own factor off. | **Switching to `off` does not bypass enrolled factors.** A user who set up two-step sign-in is still challenged at sign-in until their factor is removed — by them on `/security`, or by you (below). A policy flip silently disabling a protection a user chose for themselves is the wrong failure mode, so the policy governs enrollment, not whether an existing factor counts. MFA verification has a fixed ten-attempt budget per user, per flow, in a 15-minute window. Confirming a new enrollment has its own budget, and each of its guesses also counts toward the sign-in challenge's budget. Under **Enforce**, excess attempts are refused. Under **Monitor**, the rate-limit decision is recorded without enforcing that budget; a valid factor is still required. Password confirmation when replacing recovery codes or turning off two-step sign-in has its own attempt limit, which applies in both modes. See [account protection](https://www.udibo.com/docs/identity/account-protection#enforce-or-monitor). ## When an application asks for a second factor An application can request `acr_values=urn:udibo:acr:mfa` during authorization. An unenrolled password user under optional policy sees enrollment with a **Continue without two-step sign-in** choice. Declining preserves the original password proof time and returns baseline assurance; it does not claim MFA. Required policy has no decline path, and off policy offers no new enrollment. An already enrolled factor remains mandatory under every policy. The request is a preference, not a guarantee. The application must check the verified credential's achieved `acr` and `auth_time` before allowing its sensitive action. See [Request fresh authentication](https://www.udibo.com/docs/identity/applications#request-fresh-authentication). ## Resetting a user's MFA When someone loses both the authenticator and their recovery codes, open their page in the dashboard: **Users** → the person → **Danger zone** → **Reset two-step sign-in**. Confirm the reset in the dialog. This requires **Manage users**, which the built-in Owner and Admin roles carry; Support cannot perform it. The reset removes the person's authenticator and recovery codes **and revokes their tenant sessions**, so the next sign-in starts clean — and re-enrolls, if your policy is `required`. Your application's session and token behavior is described in [sessions and sign-out](https://www.udibo.com/docs/identity/sessions). ## Management API reference **Dashboard only today.** The operations below are reference: they accept only the dashboard's own session or a token from Udibo's own sign-in, which you cannot get outside the dashboard. A token your tenant issues, including a client-credentials token, is refused with `401`; see [the management API](https://www.udibo.com/docs/identity/apis#the-management-api). `{tenantId}` / `{id}` is your tenant's id — the same one in your dashboard URL — and the token must hold administrative authority over that tenant. ### Reset MFA | Method | Path | Scope | | ------ | ----------------------------------------------- | ---------------------- | | `POST` | `/api/identity/{tenantId}/users/{id}/reset-mfa` | `identity:users:write` | _Reference only — dashboard today._ ```ts const tenantId = process.env.UDIBO_TENANT_ID; const userId = process.env.USER_ID; const response = await fetch( `https://www.udibo.com/api/identity/${tenantId}/users/${userId}/reset-mfa`, { method: "POST", headers: { authorization: `Bearer ${process.env.UDIBO_API_TOKEN}` }, }, ); if (!response.ok) throw new Error(`reset failed: ${response.status}`); const result: { success: true; sessionsRevoked: number } = await response .json(); console.log(`sessions revoked: ${result.sessionsRevoked}`); ``` ### Update the policy The tenant's `mfaPolicy` field accepts `off`, `optional`, or `required`: | Method | Path | Scope | | ------- | ---------------------------- | ------------------------ | | `PATCH` | `/api/identity/tenants/{id}` | `identity:tenants:write` | _Reference only — dashboard today._ ```ts const tenantId = process.env.UDIBO_TENANT_ID; const response = await fetch( `https://www.udibo.com/api/identity/tenants/${tenantId}`, { method: "PATCH", headers: { authorization: `Bearer ${process.env.UDIBO_API_TOKEN}`, "content-type": "application/json", }, body: JSON.stringify({ mfaPolicy: "required" }), }, ); if (!response.ok) throw new Error(`update failed: ${response.status}`); ``` _Last verified 2026-10-29._