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:
- Sign in →
mfa_requirederror withchallenge_id - Send SMS code to the user's phone
- 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.