HelloJohn / docs
MFA

TOTP (Authenticator App)

Set up TOTP-based MFA in HelloJohn — enrollment flow, QR code generation, verification, and SDK integration for Google Authenticator, Authy, and 1Password.

TOTP (Time-based One-Time Password, RFC 6238) generates a 6-digit code that changes every 30 seconds. Users install any standard authenticator app — Google Authenticator, Authy, 1Password, Bitwarden — and scan a QR code to enroll.

Enrollment flow

Initiate enrollment

POST /v2/auth/mfa/totp/enroll
Authorization: Bearer {access_token}

Response:

{
  "enrollment_id": "enroll_01HX...",
  "totp_uri": "otpauth://totp/HelloJohn:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=HelloJohn&algorithm=SHA1&digits=6&period=30",
  "secret": "JBSWY3DPEHPK3PXP",
  "qr_code_svg": "<svg>...</svg>"
}

Display the QR code (qr_code_svg) or the raw totp_uri for the user to scan.

User scans QR code

The user opens their authenticator app and scans the QR code. The app displays a 6-digit code.

Verify and complete enrollment

The user enters the code to confirm they scanned it correctly:

POST /v2/auth/mfa/totp/enroll/verify
Authorization: Bearer {access_token}
Content-Type: application/json

{
  "enrollment_id": "enroll_01HX...",
  "code": "123456"
}

Response 200 OK:

{
  "method_id": "mfa_01HX...",
  "backup_codes": ["XXXX-XXXX", "YYYY-YYYY", ...]
}

Save the backup codes — they're only shown once. See Backup Codes →.

Verification (login challenge)

When the user logs in and TOTP is their enrolled MFA method:

POST /v2/auth/mfa/totp/verify
Content-Type: application/json

{
  "challenge_id": "challenge_01HX...",
  "code": "123456"
}

Response 200 OK — full session tokens issued:

{
  "access_token": "eyJ...",
  "refresh_token": "rt_...",
  "expires_in": 900
}

SDK integration

import { useMFA, useTOTPEnrollment } from '@hellojohn/react'

// Enrollment component
function TOTPEnrollment() {
  const { startTOTPEnrollment, verifyTOTPEnrollment } = useTOTPEnrollment()
  const [qrCode, setQrCode] = useState<string | null>(null)
  const [enrollmentId, setEnrollmentId] = useState<string | null>(null)

  async function start() {
    const { enrollment_id, qr_code_svg } = await startTOTPEnrollment()
    setEnrollmentId(enrollment_id)
    setQrCode(qr_code_svg)
  }

  async function verify(code: string) {
    await verifyTOTPEnrollment(enrollmentId!, code)
    // Enrollment complete
  }

  if (!qrCode) {
    return <button onClick={start}>Set up authenticator app</button>
  }

  return (
    <div>
      <div dangerouslySetInnerHTML={{ __html: qrCode }} />
      <p>Scan this with your authenticator app, then enter the code:</p>
      <TOTPCodeInput onSubmit={verify} />
    </div>
  )
}

// Challenge component (at login)
function TOTPChallenge({ challengeId }: { challengeId: string }) {
  const { submitTOTP } = useMFA()

  return (
    <form onSubmit={e => {
      e.preventDefault()
      submitTOTP(challengeId, (e.target as any).code.value)
    }}>
      <input
        name="code"
        type="text"
        inputMode="numeric"
        pattern="[0-9]{6}"
        maxLength={6}
        autoComplete="one-time-code"
        placeholder="000000"
        required
      />
      <button type="submit">Verify</button>
    </form>
  )
}
'use client'
import { useHelloJohnMFA } from '@hellojohn/nextjs'

export function TOTPChallenge({ challengeId }: { challengeId: string }) {
  const { verifyTOTP } = useHelloJohnMFA()

  async function action(formData: FormData) {
    const code = formData.get('code') as string
    await verifyTOTP(challengeId, code)
    // Automatically redirects to original destination
  }

  return (
    <form action={action}>
      <input
        name="code"
        type="text"
        inputMode="numeric"
        maxLength={6}
        autoComplete="one-time-code"
        required
      />
      <button type="submit">Verify</button>
    </form>
  )
}

Clock drift tolerance

TOTP codes are time-based. HelloJohn accepts codes from the current window ± 1 window (±30 seconds) to account for clock drift between the server and the user's device.

If a user's codes are consistently rejected, check:

  1. The server's system clock is synced (NTP)
  2. The user's device clock is accurate

Removing TOTP

Users can remove TOTP from their account:

DELETE /v2/auth/mfa/{methodId}
Authorization: Bearer {access_token}

If TOTP is the user's only MFA method and MFA is required for their tenant, this request will fail. They must enroll a different method first.

Admin override:

DELETE /v2/admin/users/{userId}/mfa/{methodId}
Authorization: Bearer $ADMIN_TOKEN

On this page