Back to blog

Phone login and SMS security


Own Auth SMS login normalizes a phone string, limits code requests and guesses, and stores a peppered code hash. Successful verification returns a database session or an MFA challenge when another factor is required. The flow proves current access to the number, not control of the mobile account or resistance to SIM-swap and number-porting attacks.

Normalize before identity or abuse checks

Own Auth removes every character except digits and + before phone storage, lookup, and recipient rate limiting. It does not validate the complete E.164 structure, so the application must reject invalid country codes, length, and plus-sign placement before calling the package. This keeps cosmetic variants in one account and abuse bucket without treating normalization as validation.

auth.requestSmsOtp does not reveal whether the phone number is registered. Own Auth creates a user only after a valid code is verified. Set allowPhoneSignup: false when the product should accept existing phone accounts without creating new ones.

Treat every code as a short-lived credential

A numeric code has a small search space, so expiry and attempt limits carry more security weight than hashing alone. NIST requires out-of-band secrets to be accepted once, completed within ten minutes, generated with an approved random source, and protected by rate limiting when the value has less than 64 bits of entropy. A new code must not erase the history of failed attempts against the account.

Own Auth uses six digits, a ten-minute lifetime, and five wrong attempts per code by default. For the default phone_login purpose, auth.verifySmsOtp consumes the code and verifies the phone number, then returns status: "complete" with a session or status: "mfa_required" with a challenge. Wrong, expired, consumed, and missing codes share the invalid_otp error, while an exhausted attempt limit produces otp_attempts_exceeded. Atomic consumption admits one successful verification when submissions arrive concurrently.

Own Auth hashes codes with OWN_AUTH_TOKEN_PEPPER before storage. By default, auth.requestSmsOtp returns sent and expiresAt; the configured SMS provider receives the raw code.

Separate authentication limits from delivery limits

Own Auth limits each normalized phone number to five code requests every fifteen minutes and ten verification attempts every fifteen minutes. Those identifier-based counters are stored in Postgres by default, so they are shared by application instances and survive restarts. They protect a targeted number even when requests originate from many addresses.

The application edge must separately constrain one network source or device rotating through many phone numbers. Provider controls cover geographic allowlists, destination risk, spend ceilings, and an emergency stop. Own Auth's recipient counters do not enforce those delivery limits.

Use SMS at the right assurance level

NIST classifies authentication over the public switched telephone network as restricted. Services using it should offer another authenticator, explain the risk, account for SIM changes and number porting when signals are available, and maintain a migration path. OWASP similarly recommends stronger methods for applications handling high-value transactions or sensitive personal data.

A valid SMS code can be appropriate for routine access in a product whose threat model accepts the channel. It should not automatically authorize ownership transfer, recovery-factor replacement, payout changes, bulk data export, or a new administrative role. Require recent authentication and a stronger established factor for those operations. Passkeys or another phishing-resistant authenticator provide a materially different security property.