HelloJohn / docs
MFA

SMS OTP

Set up and use SMS one-time passwords as an MFA factor in HelloJohn.

SMS OTP

SMS OTP sends a 6-digit code to the user's phone number. It is simpler to set up than TOTP but requires a phone number and depends on SMS delivery reliability.

Prerequisites

  • SMS provider configured in Tenant Settings (Twilio, AWS SNS, or custom)
  • User has a verified phone number on their account

Enabling SMS OTP

Enable SMS as an available MFA factor in Tenant Settings → MFA → Allowed Factors.

Or via the Admin API:

curl -X PATCH "https://api.hellojohn.dev/v1/admin/config" \
  -H "Authorization: Bearer sk_live_abc123" \
  -H "X-Tenant-ID: tnt_01HABCDEF654321" \
  -H "Content-Type: application/json" \
  -d '{"mfa_allowed_factors": ["totp", "sms", "backup_codes"]}'

Enrolling SMS OTP

Step 1: Start enrollment

curl -X POST "https://api.hellojohn.dev/v1/users/usr_01HABCDEF123456/mfa/factors" \
  -H "Authorization: Bearer sk_live_abc123" \
  -H "X-Tenant-ID: tnt_01HABCDEF654321" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "sms",
    "phone_number": "+14155552671"
  }'

HelloJohn sends a verification code to the specified number.

Response:

{
  "factor_id": "fac_01HABCDEF555445",
  "type": "sms",
  "status": "pending",
  "phone_number": "+1***5671"
}

Step 2: Confirm the code

curl -X POST "https://api.hellojohn.dev/v1/users/usr_01HABCDEF123456/mfa/factors/fac_01HABCDEF555445/confirm" \
  -H "Authorization: Bearer sk_live_abc123" \
  -H "X-Tenant-ID: tnt_01HABCDEF654321" \
  -H "Content-Type: application/json" \
  -d '{"code": "123456"}'

The factor becomes active after successful confirmation.

Via SDK

import { useUser } from "@hellojohn/react";

function EnrollSMS() {
  const { user } = useUser();
  const [factorId, setFactorId] = useState<string | null>(null);
  const [step, setStep] = useState<"phone" | "code">("phone");

  const startEnrollment = async (phoneNumber: string) => {
    const factor = await user.mfa.enroll({ type: "sms", phoneNumber });
    setFactorId(factor.id);
    setStep("code");
  };

  const confirmEnrollment = async (code: string) => {
    await user.mfa.confirm(factorId!, code);
    alert("SMS MFA enabled!");
  };

  if (step === "phone") {
    return (
      <form onSubmit={(e) => {
        e.preventDefault();
        startEnrollment(new FormData(e.currentTarget).get("phone") as string);
      }}>
        <input name="phone" type="tel" placeholder="+14155552671" />
        <button type="submit">Send code</button>
      </form>
    );
  }

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      confirmEnrollment(new FormData(e.currentTarget).get("code") as string);
    }}>
      <input name="code" type="text" maxLength={6} placeholder="123456" />
      <button type="submit">Verify</button>
    </form>
  );
}

Verifying SMS OTP During Sign-In

When a user with SMS MFA signs in, the flow is:

  1. Sign in → mfa_required error with challenge_id
  2. Send SMS code to the user's phone
  3. User enters the code → token promoted to mfa_verified
// Step 1: Sign in
const signInResult = await hj.auth.signIn({ email, password });

if (signInResult.error?.code === "mfa_required") {
  const { challenge_id, available_factors } = signInResult.error;
  const smsFactor = available_factors.find((f) => f.type === "sms");

  // Step 2: Trigger SMS send
  await hj.mfa.challenge({
    challenge_id,
    factor_id: smsFactor.id,
  });

  // Step 3: User enters code → verify
  const { access_token } = await hj.mfa.verify({
    challenge_id,
    factor_id: smsFactor.id,
    code: userEnteredCode,
  });
}

SMS Provider Configuration

Configure your SMS provider in Tenant Settings → Email & SMS → SMS Provider.

Twilio

{
  "provider": "twilio",
  "account_sid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "auth_token": "your_auth_token",
  "from_number": "+15005550006"
}

AWS SNS

{
  "provider": "aws_sns",
  "region": "us-east-1",
  "access_key_id": "AKIAIOSFODNN7EXAMPLE",
  "secret_access_key": "wJalrXUtnFEMI..."
}

Limitations

  • SMS delivery is not guaranteed (carrier filtering, international numbers)
  • More susceptible to SIM-swap attacks than TOTP or WebAuthn
  • Requires a valid phone number and SMS credits

For high-security requirements, consider TOTP or WebAuthn instead.


On this page