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
| Change | Route |
|---|---|
| Deleting the account | POST /api/v1/public/account/delete |
| Removing a connected account or wallet | DELETE /api/v1/public/accounts/:id |
| Removing the phone number | DELETE /api/v1/public/profile/phone |
| Turning off text message codes | POST /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.
| Option | Default | What it does |
|---|---|---|
returnUrl | The page the person is on | Where to come back to. See the rule below |
factors | none | ['mfa'] also asks for the second step, from someone who has one |
maxAgeSeconds | none | Resolve '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_endedand 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:
| Field | What it holds |
|---|---|
authTime | ISO 8601: the sign-in, or the latest re-confirmation. Absent for a session from the API-key exchange |
amr | How, 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.
| Refusal | When |
|---|---|
400 invalid_return_url | returnUrl is not on the origin of one of the client's redirect URLs |
400 reverify_unavailable | Nobody can re-confirm this session: one from an API key, an owner's, a guest's or an agent client's |
Next steps
- Deleting accounts - the change that asks for a recent sign-in most often.
- Connected Accounts - removing a sign-in method, and the last way in.
- Sessions & Tokens - how long a session lasts, and where it is stored.