Olympus Docs
CookbookMFA & step-up

SMS 2FA, secure if you must

SMS is weak; here's how to use it less badly

SMS-based 2FA is the weakest mainstream MFA factor. SIM swap attacks make it bypassable. Yet, it's familiar to users and works without an app.

If you decide to offer SMS 2FA, here's how to mitigate risks.

Why SMS is weak

  1. SIM swap: attacker convinces carrier to transfer the victim's number. Attacker now receives SMS.
  2. SS7 attacks: telecom-level interception. Rare but documented.
  3. Public exposure: phone numbers easily found / inferred.
  4. One-time codes can be phished: user types code into phishing site.

When SMS is "fine"

  • Low-value accounts (free tier, social media casual).
  • One factor of many (paired with password / passkey).
  • User has no other option.

When NOT to use SMS

  • Financial / crypto.
  • Healthcare.
  • Admin / privileged accounts.
  • Anywhere you'd be sued if compromised.

For these, require WebAuthn / TOTP.

In Olympus

Kratos supports SMS via the code method + SMS courier:

selfservice:
  methods:
    code:
      enabled: true
      config:
        lifespan: 10m

courier:
  smtp:
    # ... email
  channels:
    - id: sms
      type: http
      request_config:
        url: https://api.twilio.com/...
        method: POST
        body: ...

Twilio webhook config triggers SMS on Kratos requests.

Best practices

Don't allow SMS-only

Always pair with another factor. SMS alone is single-factor (something you have, temporarily, while it's your number).

selfservice:
  methods:
    password:
      enabled: true     # primary
    code:
      enabled: true     # SMS as 2FA

Enforce strong password

If SMS is the 2nd factor, password is the 1st. Weak password = trivial bypass after SIM swap.

methods:
  password:
    config:
      haveibeenpwned_enabled: true
      min_password_length: 12

Prefer TOTP / WebAuthn in UI

<RadioGroup label="Pick MFA method">
  <Radio value="webauthn">
    Passkey (recommended)
  </Radio>
  <Radio value="totp">
    Authenticator app
  </Radio>
  <Radio value="sms">
    SMS (less secure, only if you can't use the above)
  </Radio>
</RadioGroup>

Label SMS clearly as weaker.

Limit SMS for high-value actions

User has SMS 2FA enrolled. Wants to do something sensitive:

if (action.requiresStrongMfa && session.mfa_method === "sms") {
  return forceStrongMfaEnrollment("This action requires app-based or hardware 2FA");
}

Push users to upgrade for sensitive paths.

Rate limit SMS

Bots can rack up your Twilio bill by triggering SMS:

const smsKey = `sms:${phoneNumber}`;
const count = await redis.incr(smsKey);
await redis.expire(smsKey, 3600);
if (count > 5) {
  // block, too many SMS to this number in 1h
  return Response.json({ error: "sms_rate_limit" });
}

Also limit by IP (signups requesting SMS):

const ipKey = `sms_ip:${ip}`;
const ipCount = await redis.incr(ipKey);
if (ipCount > 10) return blocked;

Limit length and format of phone numbers

function isValidPhone(p: string) {
  return /^\+[1-9]\d{1,14}$/.test(p);  // E.164
}

No formatting characters that could break SMS provider.

Don't expose carrier metadata

Some SMS providers expose carrier (Verizon, T-Mobile). Don't show this in UI, privacy.

Watch for SIM-swap signals

Tools like Twilio Lookup return "porting status" (recently ported = potential SIM swap).

const lookup = await twilio.lookups.v2.phoneNumbers(phone).fetch({ fields: "line_type_intelligence" });
if (lookup.lineTypeIntelligence?.recentlyPorted) {
  // Possible SIM swap, require stronger verification
  forceStrongMfa();
}

Twilio Lookup costs ~$0.005/check. Worth for accounts > free tier.

Costs

Twilio SMS: ~$0.0075/SMS (US). For 100k SMS/mo = $750/mo.

Compare to TOTP / WebAuthn: free.

Promote free / strong alternatives. SMS becomes the fallback.

Communication

User enrolls SMS:

"SMS 2FA enabled. You'll get a code via text when you sign in.
For stronger security, consider enabling an authenticator app or passkey:
[Set up app] [Set up passkey]"

Don't trash SMS, but guide users to better.

Voice fallback

For users who can't get SMS (international, etc.), voice call:

courier:
  channels:
    - id: voice
      type: http
      request_config:
        url: https://api.twilio.com/.../Calls

Same weaknesses as SMS plus carrier-level call interception (rare).

Number changes

User changes phone. Update:

<form>
  <label>New phone number</label>
  <input name="phone" type="tel" />
  <button>Verify</button>
</form>

Send verification to NEW number. Confirm. THEN replace OLD.

Until verified, both numbers can receive 2FA. After: only new.

Notify OLD number that it's been removed:

"Your 2FA SMS for [Your App] has been moved to a new number."

If wasn't them, they have time to act.

On this page