HelloJohn / docs
MFA

Multi-Factor Authentication

Add MFA to HelloJohn — TOTP, WebAuthn/passkeys, SMS, email OTP, and backup codes. Enable globally, per tenant, per org, or per role.

MFA in HelloJohn is a second verification step after the user authenticates with their primary method (email/password, OAuth, etc.).

MFA methods

MethodDescriptionRequires
TOTP6-digit code from an authenticator appAuthenticator app (Google Authenticator, Authy, 1Password)
WebAuthn / PasskeysHardware key or biometricBrowser support + compatible device
SMS OTPOne-time code via SMSSMS provider (Twilio, etc.)
Email OTPOne-time code via emailSMTP configuration
Backup codesRecovery codes (last resort)Generated at enrollment time

MFA enforcement levels

MFA can be required at different levels:

LevelHow to set
GlobalMFA_REQUIRED=true in environment
Per tenantPATCH /v2/admin/tenants/{id}/auth/config
Per roleRole policy in tenant config
OptionalUsers can self-enroll but aren't required to

When MFA is required, users who haven't enrolled are redirected to the MFA setup flow on their next login.

MFA flow

1. User authenticates (email/password, OAuth, etc.)
2. HelloJohn checks if MFA is required
3. If yes:
   a. User has enrolled  → challenge step (enter TOTP code, tap key, etc.)
   b. User hasn't enrolled → redirect to enrollment flow
4. MFA verified → access + refresh tokens issued
5. MFA failed → 401, user can retry

Enabling MFA globally

# Require MFA for all users across all tenants
MFA_REQUIRED=true

# Allow users to self-enroll but not require it
MFA_OPTIONAL=true  # default

Enabling MFA per tenant

PATCH /v2/admin/tenants/{tenantId}/auth/config
Authorization: Bearer $ADMIN_TOKEN
Content-Type: application/json

{
  "mfa_required": true,
  "mfa_methods": ["totp", "webauthn", "backup_codes"]
}

SDK: handling MFA in the auth flow

When MFA is required, the SDK handles the challenge automatically:

import { useMFA } from '@hellojohn/react'

function MFAChallenge({ challengeId }) {
  const { submitTOTP } = useMFA()

  return (
    <form onSubmit={e => {
      e.preventDefault()
      submitTOTP(challengeId, e.target.code.value)
    }}>
      <input name="code" placeholder="000000" maxLength={6} />
      <button type="submit">Verify</button>
    </form>
  )
}

Admin: manage user MFA

# List a user's enrolled MFA methods
GET /v2/admin/users/{userId}/mfa

# Reset all MFA for a user (requires re-enrollment on next login)
DELETE /v2/admin/users/{userId}/mfa

Resetting MFA is a privileged operation. Require admin MFA confirmation to perform it.

Next steps

On this page