Skip to Content
ConnectMulti-Factor Auth

Multi-Factor Authentication

BuildPad DaaS supports time-based one-time passwords (TOTP) from an authenticator app. The flow uses the native MFA support in Supabase Auth, so the access token carries the assurance level that the server reads.

There are two assurance levels:

LevelMeaning
aal1The user proved one factor. A password sign-in gives this level.
aal2The user also proved a second factor.

A user who enrolls no factor stays at aal1, and nothing changes for them. After a user verifies a factor, every request from that user must reach aal2.

Enroll a factor

Enrollment has two calls. The first returns the secret to show to the user. The second confirms that the user can produce a code from it.

  1. Call POST /api/auth/mfa/enroll. The optional friendly_name field names the factor.

    { "data": { "factor_id": "...", "qr_code": "data:image/svg+xml;...", "secret": "JBSW...", "uri": "otpauth://totp/..." } }

    Show qr_code for the user to scan. Show secret as well, because some users enter the key by hand.

  2. Call POST /api/auth/mfa/verify with the factor and the six-digit code:

    { "factor_id": "...", "code": "123456" }

    The response carries a new session at aal2:

    { "data": { "access_token": "...", "refresh_token": "...", "expires": 3600000 } }

    Replace the stored tokens with these. expires is in milliseconds.

The factor is unverified until step 2 succeeds. An unverified factor does not trigger the gate below.

Factor management is bound to the session of the user. An administrator cannot enroll or remove a factor for someone else.

Step up an existing session

A user who signs in with a password holds an aal1 session. If they have a verified factor, requests answer 403:

{ "error": "Multi-factor authentication required", "code": "MFA_REQUIRED", "current_aal": "aal1", "required_aal": "aal2" }

Prompt for the one-time code and call POST /api/auth/mfa/verify with it. Use the tokens from the response. Do not sign the user out — the password step is already complete, and a full sign-in is not necessary.

POST /api/auth/login also returns an mfa_required hint, so a client can ask for the code directly after the password.

Treat 403 with code: "MFA_REQUIRED" as “ask for a code”, not as “the session is invalid”. A client that clears the session on any 403 sends the user back to a password screen that cannot fix the problem.

Endpoints

EndpointPurpose
GET /api/auth/mfa/factorsList the factors of the current user, with id, status, factor_type, friendly_name, and created_at.
POST /api/auth/mfa/enrollStart enrollment. Returns factor_id, qr_code, secret, and uri.
POST /api/auth/mfa/verifyConfirm a code. Returns a session at aal2.
POST /api/auth/mfa/unenrollRemove a factor. Takes factor_id.
GET /api/users/[id]/mfa-statusRead whether another user has a verified factor. Administrators only.

GET /api/auth/mfa/factors, POST /api/auth/mfa/enroll, and POST /api/auth/mfa/verify accept an aal1 session. Without that, a user who must enroll could not reach the endpoints that let them enroll.

Require MFA for every user

Set MFA_FORCE_ENABLED=true to make MFA mandatory. Then:

  • A user with no verified factor is blocked from every route except the enroll and verify endpoints.
  • The login page shows the enrollment step directly after the password.
  • POST /api/auth/login reports mfa_enrollment_required.
  • A user cannot remove their last factor. The API rejects the call. This is not only a hidden button.

docker-compose.yml does not list MFA_FORCE_ENABLED. Add the variable to your own environment. It reads at runtime, so a change needs a restart but not a new image.

Static tokens and SSO

Static tokens are exempt. A static token (daas_users.token) authenticates without a session, so it carries no assurance level. A static token is not blocked, even under MFA_FORCE_ENABLED. Use one for service and automation accounts under this policy, and treat every issued static token as a standing exemption.

SSO auto-login is refused for enrolled users. The BuildPad SSO handoff returns an error when the account has a verified factor. Completing it would step around the second factor that the user asked for. Those users sign in with a password, and then complete the code step.

Error codes

CodeStatusMeaning
MFA_REQUIRED403The session is aal1 and the user has a verified factor. Prompt for a code.
MFA_REQUIRED403Under MFA_FORCE_ENABLED, the user has no factor. Send them to enrollment.

Both cases use the same code, because both are answered the same way: the user must reach aal2 before the request can proceed. The login response tells the two apart with mfa_required and mfa_enrollment_required.