BuildBaseBuildBase

Two-step sign-in

Ask the people who sign in to your app for a code from their phone as well, so a stolen password alone is not enough.

Two-step sign-in asks for a second proof after the first one. Someone signs in with their password, an email link or another method as usual, then types a short code from their phone. A password can leak, be guessed or be reused on a site that was hacked. With two-step sign-in, whoever has the password still cannot get in without the person's phone. You may also know it as two-factor authentication, 2FA or MFA.

It applies to the people who sign in to your app, not to your team in the BuildBase console. It runs on your hosted sign-in page, so signing in needs nothing new in your app.

Step by step, with screenshots: Set up two-step sign-in.

Before you start

Choose the policy under User Management → Access → Authentication → Security, in the Two-step sign-in card. People turn it on for themselves from the Security screen of your app's settings.

Choose who is asked

PolicyWhat your users see
OffNobody is asked for a code, and your app's Security screen offers nothing to set up. What people set up before is kept, and they are asked for codes again if you turn the policy back on.
Optional (default)Each person chooses. Whoever turned it on is asked for a code when they sign in, apart from the cases in Who is never asked and on devices they trust. Nobody else sees anything new.
RequiredEveryone is asked, apart from those same cases. Someone with no second step yet sets up an authenticator app on the sign-in page, at their next sign-in, before they reach your app. A new sign-up does it too.

Saving Required asks you to confirm first. Nobody already signed in is signed out: the policy is read at each sign-in, so it takes effect from each person's next one. While it is Required, people cannot remove their authenticator app, and cannot turn off text message codes when those are their only second step.

Also ask social sign-ins, in the same card, is off by default, because Google, GitHub and the other providers run their own checks. Turn it on to ask for the code after a social sign-in too, Google One Tap included.

The second steps

Authenticator app

An authenticator app is an app on the person's phone, such as Google Authenticator, Microsoft Authenticator, 1Password or Authy. It shows a new 6-digit code every 30 seconds, and works without signal. The person sets it up once, by scanning a QR code with the app or typing in a key. The entry in their app carries your app's name: the Name on your sign-in page, or your organization's name.

Each code works once. A code from just before or just after the current one is accepted too, in case the phone's clock is a little off. The authenticator app is always allowed: it is the stronger choice, and the card does not let you save a list without it.

Backup codes

Setting up an authenticator app gives the person 10 backup codes, shown once. They are spare codes for the day the phone is lost, and they look like k7m2p-x9rqa: ten letters and digits, without the ones that are easy to confuse, such as 0 and o. Capitals, spaces and the dash do not matter when typing one.

  • Each code works once, in place of a code from the app.
  • The hosted page counts down. After a backup code it says when 3 or fewer are left, and when the last one is gone.
  • New backup codes come from the Security screen of your app, after the person types a code from the app or a backup code. The old set stops working at once.
  • Text messages come without backup codes. Only the authenticator app has them.

Text message codes

Tick Text message codes in the card to let people get their code by text message instead. It is the weaker choice: a phone number can be moved to another SIM card by someone who talks a phone company into it, and a text can be intercepted. Offer it for people who would otherwise have no second step at all.

  • Never the only one. The authenticator app stays allowed beside it. Saving a list of text messages alone is refused with sms_only_factor.
  • It needs your own SMS provider, connected on the SMS tab, and a plan that includes phone sign-in codes. Each text is billed by your provider and counts toward your daily cap and the per-number limits. See Phone sign-in.
  • People turn it on from the Security screen of your app, with a verified phone number on their account. The same screen's Phone number section adds one. The sign-in page's own set-up offers only the authenticator app.
  • At sign-in, the page offers "Text a code to" the person's masked number, marked as the less secure choice, and sends nothing until they ask. A new code can be sent after 30 seconds.
  • Never after a phone code. Someone who signed in with a code sent to their phone is not offered a text message as the second step: the same phone would be both steps.
  • If you untick it or disconnect your provider, people whose only second step is text messages stop being asked for one under Optional. Under Required, they set up an authenticator app at their next sign-in.

A person cannot remove the phone number their text messages go to until they turn text message codes off.

Passkeys

A passkey sign-in where the device checks the person, with a fingerprint, a face or the screen lock PIN, already counts as both steps, so no code is asked for. A passkey sign-in without that check is asked for a code like a password sign-in. A passkey is not offered as the second step after another way of signing in.

Who is never asked

  • Passkey sign-ins that checked the person, as above.
  • Social sign-ins, unless Also ask social sign-ins is on.
  • Agent accounts, made through agent sign-up, under every policy. A program has nobody to read a code.
  • Guests. A guest has no address to set a second step up against. It starts once they create an account.
  • You, signing in as the person from the console. Signing in as a user makes its session without their code.

Everyone else is asked whenever the policy says so: after a password, an email link or code, a phone code, a wallet signature, or a device code approval.

Trusted devices

After someone signs in on a device they have not trusted yet, the sign-in page asks Trust this device?. Saying yes keeps them signed in there for the Trust a device for window and, when that sign-in included a code, skips the code on that device the next times they sign in.

Both settings are in the Security & sessions card on the same tab:

SettingDefaultWhat it does
Ask to trust new devicesOnShows the question. Off, nobody is asked, and every device asks for the code again, including devices trusted before.
Trust a device for90 daysHow long a trusted device stays signed in and skips the code, from 1 to 180 days. Without trust, a sign-in lasts the session lifetime, 30 days by default.

A trusted device asks for the code again when:

  • the Trust a device for window has run out
  • the person turned on a second step after their last code there, such as text messages beside the app
  • the person removed their authenticator app, or you reset their two-step sign-in
  • your app asks for the second step on purpose, with Confirm it's you and factors: ['mfa']

The page tells people to trust only a device that is theirs, never a shared or public computer.

What your users do

Turning it on in your app

The Security screen of the SDK's settings has a Two-step sign-in section. Open it from your own button:

import { useSaaSAuth } from '@buildbase/sdk/react';

function SecurityButton() {
  const { user, openWorkspaceSettings } = useSaaSAuth();
  return (
    <button onClick={() => openWorkspaceSettings('security')}>
      {user?.mfaEnabled ? 'Security settings' : 'Turn on two-step sign-in'}
    </button>
  );
}
  1. Next to Authenticator app, press Set up.
  2. Scan the QR code with the authenticator app, or type in the key shown under it.
  3. Type the 6-digit code the app shows and press Turn on.
  4. Save the backup codes with Copy, in a password manager or on paper, then press Done. They are not shown again.

For text messages, the person first adds and verifies a phone number in the Phone number section of the same screen, then presses Turn on beside Text message and types the code that arrives. Turning text messages off asks them to have signed in within the last 10 minutes, or to confirm it's them first.

The section hides itself while your policy is Off and nothing is set up. Hide it yourself with ui.settings.security.twoStep: false on SaaSOSProvider.

Setting it up at sign-in

Under Required, someone with no second step sees Set up two-step sign-in after their first step. They scan the QR code with an authenticator app, type its code, then save their backup codes with Copy or Download .txt and press I've saved these codes to carry on into your app.

Signing in with a code

After the first step, the Two-step sign-in page asks for the 6-digit code from the app. Links under it switch to Use a backup code or, for someone with text message codes, Text a code to their number.

  • The second step has to be finished within 5 minutes of the first.
  • After 5 wrong codes, that sign-in ends and the person starts again.
  • After 10 wrong codes within 15 minutes, across any number of sign-ins, the account takes no more codes until 15 minutes after the first of them, and a new sign-in stops at the first step with "Too many incorrect codes. Please wait up to 15 minutes and try again." Removing the app and making new backup codes count toward the same limit.

A lost phone

  1. With a backup code, the person signs in with it, then sets up the app on their new phone from the Security screen: Turn off, with another backup code, then Set up. Under Required, Turn off is not offered, so they ask you for a reset instead.
  2. Without backup codes, they ask you. Make sure the person is who they say they are, then open them under Users, press More and choose Reset two-step sign-in.

A reset removes the authenticator app, the backup codes and text message codes. Every trusted device asks for a code again, and a sign-in already in progress has to start again. It does not sign the person out of your app: end their sessions from the Devices tab if you need to. Under Required, they set up a new app at their next sign-in. Resetting needs a console role that can both update and delete users.

The two-step page tells someone who lost everything to contact your support team, with your support link if your sign-in page has one.

For developers

Your own screens

The Security screen's calls are on WorkspaceApi. Their routes are under /api/v1/public, authorized by the session:

import { useMemo } from 'react';
import { WorkspaceApi } from '@buildbase/sdk';
import { useSaaSOs } from '@buildbase/sdk/react';

export function useWorkspaceApi() {
  const os = useSaaSOs();
  return useMemo(
    () =>
      new WorkspaceApi({
        serverUrl: os.serverUrl,
        version: os.version,
        orgId: os.orgId,
      }),
    [os.serverUrl, os.version, os.orgId]
  );
}

// In a component: const api = useWorkspaceApi();
// then, in an effect or a click handler:
const status = await api.getMfaStatus();
// { enabled, enforcement, allowedFactors, backupCodesRemaining?, sms?: { allowed, enrolled, phone? } }
CallRouteWhat it does
getMfaStatus()GET /mfaThe person's second steps and your policy
startTotpEnrollment()POST /mfa/totp/enrollA new key, its otpauth:// URL and a QR code as SVG. Show the SVG as an image
confirmTotpEnrollment(code)POST /mfa/totp/confirmTurns the app on with its first code, and answers the backup codes, this once
regenerateBackupCodes(code)POST /mfa/backup-codes/regenerateNew backup codes for a current code or backup code. The old ones stop working
disableTotp(code)POST /mfa/totp/disableRemoves the app, for a current code or backup code
startSmsEnrollment()POST /mfa/sms/enrollTexts a code to the verified phone. Answers retryAfter
confirmSmsEnrollment(code)POST /mfa/sms/enroll/confirmTurns text message codes on
disableSms()POST /mfa/sms/disableTurns them off. Needs a sign-in within the last 10 minutes

A refusal keeps the server's sentence as its message and carries the reason as code. None of them signs the person out.

codeStatusWhen
mfa-off403Your policy is Off, so nothing new can be set up
already-enrolled409An authenticator app is already on. Remove it first
not-enrolling409No set-up is waiting to be confirmed. Start again
not-enrolled404Nothing to remove or replace
invalid-code400The app code or backup code is wrong
required-by-org403Your policy is Required: the app cannot be removed, nor text messages that are the only second step
locked42910 wrong codes in 15 minutes
factor_not_allowed403Text message codes are not allowed, or no provider is connected
phone_required409The account has no verified phone number
invalid_code400The texted code is wrong
code_expired410The texted code has lapsed. Send a new one
sms_unavailable503Your SMS provider cannot send or check codes right now
reauth_required403Turning text messages off, more than 10 minutes after signing in

Sending a text can also be refused with the phone sign-in send refusals, such as cooldown with retryAfter.

useSaaSAuth().user.mfaEnabled is true while the person has either second step on.

What the token says

The session's amr lists how the person signed in, as RFC 8176 values. A code at sign-in adds otp and mfa to the first step's value, so a password and an app code give ['pwd', 'otp', 'mfa']. A backup code gives the same. A texted code adds sms, otp and mfa. A trusted device and a passkey that counted as both steps skip the code, so their amr has no mfa. Read it as useSaaSAuth().session.amr; see How recently the session signed in.

If you call the auth API yourself

The hosted page does this for you. When a second step is needed, the first step answers with this instead of a token:

{
  "success": true,
  "mfaRequired": true,
  "mfaToken": "<40 characters>",
  "factors": [
    { "type": "totp", "backupCodesRemaining": 10 },
    { "type": "sms", "backupCodesRemaining": 0, "phone": "+1 ••• ••• 0123" }
  ],
  "enrollRequired": false
}

With enrollRequired: true, factors is empty and the person sets up an app first. The step after the first, POST /api/v1/auth/redirect, can answer 403 with reason: "mfa_required" and the same fields, for a sign-in that has not passed the second step your policy now asks for, such as a new sign-up under Required.

RouteBodyAnswers
POST /api/v1/auth/mfa/verifymfaToken, code, and factor: "sms" for a text{ success, data: { user, token } }, with backupCodesRemaining after a backup code
POST /api/v1/auth/mfa/sms/sendmfaToken{ success, retryAfter: 30 }
POST /api/v1/auth/mfa/totp/enrollmfaTokenThe key, URL and QR code, as startTotpEnrollment does
POST /api/v1/auth/mfa/totp/enroll/confirmmfaToken, codeThe token, with backupCodes, this once
RefusalWhen
400 invalid_codeThe code is wrong. The message says which kind it took the code for
401 mfa_expiredThe 5 minutes have passed, the sign-in had 5 wrong codes, or it already finished
429 mfa_locked10 wrong codes on the account within 15 minutes
400 enroll_required/verify while the person has to set up an app first
400 challenge_required/totp/enroll or its confirm while the person already has a second step
400 factor_unavailableA text message is not offered for this sign-in
400 code_expiredThe texted code has lapsed
503 sms_unavailableYour SMS provider cannot send or check the code right now
403 blockedThe account is blocked

The text send can also answer 429 cooldown and rate_limited, and 422 number_refused, as phone sign-in does.

Events

EventWhen
user.mfa_enrolledA person turns on an authenticator app or text messages; factor says which
user.mfa_disabledA person turns one off
user.mfa_reset_by_adminYou reset a person's two-step sign-in; factors lists what was removed
auth.login_failedA wrong code, with stage: "second_factor", and locked: true once the account stops taking codes

Subscribe to any of them as a webhook event.

Next steps