BuildBaseBuildBase

Confirm it's you

Ask for a recent sign-in before a sensitive change, and let people re-confirm on the hosted page without losing their session.

Some changes should not ride on a session someone opened three weeks ago. Deleting the account, removing a connected account or a phone number, turning off text message codes: each needs a sign-in from the last 10 minutes. An older session is refused with 403 reauth_required, and the person confirms it is still them on your hosted sign-in page. They come back to the page they were on, in the same session, now recent. Nobody is signed out, and your backend has no code to exchange.

Step by step, with screenshots: The account security screens.

import {
  isReauthRequired,
  useSaaSAuth,
  type WorkspaceApi,
} from '@buildbase/sdk/react';

function DeleteAccountButton({ api }: { api: WorkspaceApi }) {
  const { requireReverification } = useSaaSAuth();

  async function deleteAccount() {
    try {
      await api.requestAccountDeletion();
    } catch (error) {
      if (!isReauthRequired(error)) throw error;
      // Leaves for the sign-in page and comes back here. Ask again then.
      await requireReverification({ factors: error.factors });
    }
  }

  return <button onClick={deleteAccount}>Delete my account</button>;
}

The SDK's Security screen does this for you. Every section that can be refused offers Confirm it's you, and says once on the way back that the change can go ahead.

What needs a recent sign-in

ChangeRoute
Deleting the accountPOST /api/v1/public/account/delete
Removing a connected account or walletDELETE /api/v1/public/accounts/:id
Removing the phone numberDELETE /api/v1/public/profile/phone
Turning off text message codesPOST /api/v1/public/mfa/sms/disable

Recent means the session proved who its person is in the last 10 minutes, at the sign-in or at a later re-confirmation. A guest deleting their account is the one exception: a guest has no way to sign in again, so the session is all there is to ask about.

The refusal is a 403, never a 401, so the SDK does not take it for a dead session. It can carry maxAgeSeconds, the window in seconds, and factors: ['mfa'] when the change also wants the second step.

requireReverification

useSaaSAuth().requireReverification(options?) resolves 'recent' or 'redirecting'. 'redirecting' means the browser is on its way to the hosted page: do nothing more in that call. The person comes back with reverified=1 on the URL, which the provider takes off without a reload.

OptionDefaultWhat it does
returnUrlThe page the person is onWhere to come back to. See the rule below
factorsnone['mfa'] also asks for the second step, from someone who has one
maxAgeSecondsnoneResolve 'recent' without leaving when the session proved who its person is within this many seconds. Without it, the person is always sent to confirm

The return URL has to be on the origin of one of your client's redirect URLs. When the server refuses one with invalid_return_url, the SDK comes back through your auth.redirectUrl instead and then continues to the page, as it does after a sign-in.

A change of your own can ask first:

const { requireReverification } = useSaaSAuth();

async function rotateApiKey() {
  // Signed in or re-confirmed in the last five minutes, or confirm first.
  if ((await requireReverification({ maxAgeSeconds: 300 })) === 'recent') {
    await createNewKey();
  }
}

That check runs in the browser, so treat it as the friendly half. If the change matters, your backend should refuse a stale session as well, from the authTime described below.

maxAgeSeconds with factors: ['mfa'] resolves 'recent' only when the sign-in included a second step, so someone without one is sent to confirm every time. Check user.mfaEnabled before asking for it.

On the hosted page

The sign-in page opens as Confirm it's you, with the person's address filled in. They sign in with any method your client offers, and the page sends them back.

  • The second step. With factors: ['mfa'], a person who has a second step is asked for it, even on a device they trusted to skip it.
  • Someone else. Signing in as a different account is refused with wrong_account, and the session keeps its old sign-in time.
  • An ended session. If the session was signed out while they were away, they get session_ended and sign in again from scratch.

Sessions that cannot re-confirm

Some sessions have nobody behind them who could sign in again. requireReverification throws reverify_unavailable for them:

  • a session made from an API key, which never counts as recent
  • a session an app owner opened from the console, see Signing in as a user
  • a guest's session, see Guest users
  • a session for an agent client

The Security screen falls back to signing out and in again for these. It never does that to a guest, who would lose the account.

Reading it yourself

useSaaSAuth().session carries how and when the session last proved who its person is:

FieldWhat it holds
authTimeISO 8601: the sign-in, or the latest re-confirmation. Absent for a session from the API-key exchange
amrHow, as RFC 8176 values such as ['pwd', 'mfa']

Your backend reads the same two from GET /api/v1/public/profile, with the session in the x-session-id header. There authTime is null for a session nobody signed in to, so a route of yours can hold itself to the same 10 minutes.

The API

POST /api/v1/public/session/reverify with { "returnUrl": "https://app.example.com/settings", "factors": ["mfa"] } answers { url }: the hosted page, bound to this session. factors is optional. Send the browser to url.

RefusalWhen
400 invalid_return_urlreturnUrl is not on the origin of one of the client's redirect URLs
400 reverify_unavailableNobody can re-confirm this session: one from an API key, an owner's, a guest's or an agent client's

Next steps