Two-Step Sign-In

A person with two-step sign-in on must confirm a code from their authenticator app — Google Authenticator, Authy, 1Password — after their first factor, before they get a session. It is optional per user and off by default.

How it changes sign-in

Every sign-in method — email and password, email code, phone, Google, Apple — is unchanged for people without two-step sign-in on. For someone who has it on, the same call throws MfaRequiredException / MfaRequiredError instead of returning a session:

Flutter
dart
try {
  final user = await Koolbase.auth.login(
    email: '[email protected]',
    password: 'her-password',
  );
} on MfaRequiredException catch (e) {
  // Ask for the code, then:
  final user = await Koolbase.auth.verifyMfa(
    challengeToken: e.challengeToken,
    code: '123456',
  );
}
JavaScript / React Native
typescript
try {
  const session = await Koolbase.auth.login({ email: '[email protected]', password: 'her-password' });
} catch (err) {
  if (err instanceof MfaRequiredError) {
    const session = await Koolbase.auth.verifyMfa({
      challengeToken: err.challengeToken,
      code: '123456',
    });
  }
}

The challenge token lasts five minutes and allows three attempts. A wrong code returns otp_invalid; three wrong codes exhaust the challenge and the person must sign in again. If they use a recovery code instead, verifyRecoveryCode does the same thing and also returns how many remain — so the app can prompt for a new set when they run out.

For every sign-in method

The same exception comes from email codes, phone sign-in, Google, and Apple — not only email and password. Apps that switch sign-in methods on the same account need to handle it in each path.

Turning it on

Enrolment needs a sign-in within the last ten minutes — so a session found on an unattended device cannot quietly add its own authenticator.

Flutter
dart
// 1. Start — returns the setup link and key
final enrolment = await Koolbase.auth.enrollMfa();
// show enrolment.otpauthUri as a QR code
// show enrolment.secret for people who type it in

// 2. Confirm — the first code from the app
final recoveryCodes = await Koolbase.auth.confirmMfaEnrollment('123456');
// Save these — they are shown once only
JavaScript / React Native
typescript
const { otpauthUri, secret } = await Koolbase.auth.enrollMfa();
// show the QR code

const codes = await Koolbase.auth.confirmMfaEnrollment('123456');
// save the recovery codes

Confirming turns MFA on, returns ten recovery codes and signs every other device out. Sessions created before MFA was on would otherwise keep working; they are revoked so MFA protects the account from the moment it is enabled.

Recovery codes

Ten codes are generated when MFA is turned on. Each works once, in place of a TOTP code, for someone who has lost access to their authenticator app. Regenerating replaces the whole set; the old ones stop working immediately. To regenerate, a second factor confirmed in the last ten minutes is required — use stepUpMfa first.

No email bypass

Recovery by email is not offered. Email is often the first factor, so emailing a recovery link would defeat two-step sign-in against the exact attack it exists for: someone who has taken over the inbox.

Changing MFA needs a recent second factor

Turning MFA off or regenerating recovery codes needs the second factor confirmed in the last ten minutes — not just a session. A session found on an unattended device cannot make these changes. Call stepUpMfa first with a current authenticator code or a recovery code, then make the change:

Flutter
dart
await Koolbase.auth.stepUpMfa(code: '123456');
// or: await Koolbase.auth.stepUpMfa(recoveryCode: 'abcde-fghij');

await Koolbase.auth.disableMfa();
// or: final codes = await Koolbase.auth.regenerateRecoveryCodes();
JavaScript / React Native
typescript
await Koolbase.auth.stepUpMfa({ code: '123456' });

await Koolbase.auth.disableMfa();

Admin reset

For someone who has lost both their authenticator and their recovery codes: a project admin can reset their MFA from the Users page, or through the API:

HTTP
HTTP
POST /v1/projects/{project_id}/users/{user_id}/mfa/reset
Authorization: Bearer <dashboard session>

This removes the authenticator and recovery codes, signs the person out on every device, and emails them. They can sign in with their first factor and enrol again. An admin never sees a secret or a code — reset means removal and re-enrolment, not impersonation.

Only do this once you're sure

An admin reset is what an attacker would try to talk support into. Verify it is really the account holder asking before confirming.

Errors

  • mfa_required — first factor passed; a second is needed. The exception carries the challenge token
  • otp_invalid — wrong code; attempts remain
  • otp_max_attempts — three wrong attempts; sign in again
  • otp_expired — the challenge expired or was already used
  • recent_auth_required — starting enrolment needs a sign-in within ten minutes
  • recent_mfa_required — turning MFA off or regenerating codes needs stepUpMfa first
  • mfa_already_enabled — an authenticator is already on for this account
  • mfa_enrollment_not_found — no enrolment in progress, or it expired after 15 minutes
  • mfa_not_enabled — the account has no authenticator to change

Requirements

  • koolbase_flutter 12.8.0 or later
  • @koolbase/js / @koolbase/react-native 12.2.0 or later