Forget about password recovery. SMS authorization via phone number solves the problem: enter a number, get an OTP code, log in. We implement this flow turnkey — from provider integration to a frontend with a timer. This is standard for e-commerce, delivery, and fintech in Russia: no passwords, only a verified number.
Overview of OTP authorization
OTP (One-Time Password) is a one-time password sent via SMS and valid for 5 minutes. It's an alternative to password authentication: the user enters a phone number, receives a code, and logs in instantly. No passwords, no hash leaks. Based on our data, login time is halved compared to the password scheme. OTP authorization saves budget: conversion increased by 25% on one project, and support requests for password reset dropped by 60% on another.
SMS providers for Russia and CIS
| Provider | Features |
|---|---|
| SMSC.ru | Popular, has HTTP API and SMPP |
| SMS.ru | Simple API, good deliverability |
| Exolve (MTS) | Carrier-level, virtual numbers |
| Infobip | International, expensive, reliable |
| Twilio | International, unavailable in Russia without VPN |
| Firebase SMS | For mobile apps, not for web |
Security and reliability of SMS authorization
Passwords are a pain: users set weak combinations, reuse them across sites, store them in notes. An OTP code lives for 5 minutes, is tied to a device, and cannot be stolen from another device. We enhance protection with:
- Storage of the code hash in Redis, not the code itself.
- Rate limiting: no more than 3 SMS per hour from one number, 1 verification attempt per minute.
- Attempt limit: 3 failures — block the number for 5 minutes.
Ensuring SMS deliverability
Without code delivery, authorization doesn't work. Therefore, we guarantee deliverability through monitoring the provider's balance and fallback to a backup provider, number normalization using the libphonenumber library (E.164 format), and handling provider API errors. If the response is error, we log it and notify the administrator. Additionally, we configure automatic retry after 60 seconds and user notification. We guarantee 99.9% deliverability with a fallback provider.
Step-by-step implementation: 5 steps
- OTP service: Generate a 6-digit code, store its SHA-256 hash in Redis with a 5-minute TTL.
- SMS provider integration: Connect to SMSC.ru or SMS.ru via HTTP API and handle responses.
-
API endpoints: Create
/auth/phone/send-code(with rate limiting) and/auth/phone/verify. - Frontend form: Build a form with phone input, code input, resend timer, and auto-submit.
- Edge case handling: Test with invalid numbers, expired OTPs, and high load (1000 concurrent requests).
Architecture and implementation flow
Step 1: Send code
POST /auth/phone/send-code { phone: "+79001234567" } → phone validation → OTP generation → store hash(OTP) in Redis with TTL 5 min → send SMS → response: { expires_in: 300 } Step 2: Verify
POST /auth/phone/verify { phone: "...", code: "123456" } → check OTP from Redis → create/find user → issue session or JWT OTP generation and storage
class PhoneOtpService { public function sendOtp(string $phone): int { $this->checkRateLimit($phone); $code = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT); // Store hash, not the code itself Cache::put( "phone_otp:{$phone}", [ 'hash' => hash('sha256', $code), 'attempts' => 0, ], now()->addMinutes(5) ); $this->smsProvider->send($phone, "Your code: {$code}"); return 300; // expires_in seconds } public function verifyOtp(string $phone, string $code): bool { $data = Cache::get("phone_otp:{$phone}"); if (!$data) { throw new OtpExpiredException(); } // Attempt limit if ($data['attempts'] >= 3) { Cache::forget("phone_otp:{$phone}"); throw new OtpAttemptsExceededException(); } if (!hash_equals($data['hash'], hash('sha256', $code))) { Cache::put("phone_otp:{$phone}", array_merge($data, [ 'attempts' => $data['attempts'] + 1, ]), now()->addMinutes(5)); return false; } Cache::forget("phone_otp:{$phone}"); return true; } } Rate limiting
// No more than 3 SMS per hour from one number RateLimiter::for('sms-otp', function (Request $request) { return [ Limit::perHour(3)->by('phone:' . $request->phone), Limit::perMinute(1)->by('phone:' . $request->phone), ]; }); Phone number normalization
use libphonenumber\PhoneNumberUtil; $phoneUtil = PhoneNumberUtil::getInstance(); $parsed = $phoneUtil->parse($rawPhone, 'RU'); if (!$phoneUtil->isValidNumber($parsed)) { throw new InvalidPhoneNumberException(); } $normalized = $phoneUtil->format($parsed, \libphonenumber\PhoneNumberFormat::E164); // +79001234567 The library giggsey/libphonenumber-for-php is a PHP port of Google libphonenumber.
User creation on first login
Upon successful code verification, the user is created in the system if they don't exist yet. The phone field is the unique identifier. Additional fields (name, email) are requested after the first login as needed.
What's included in turnkey development?
- OTP service (generation, hashing, rate limiting).
- Integration with a selected SMS provider.
- API endpoints for sending and verification.
- Frontend form with timer and auto-submit.
- Documentation and test coverage (edge cases, load).
Timeline
| Stage | Time |
|---|---|
| OTP service + Redis | 1 day |
| SMS provider integration | 0.5 day |
| API endpoints + rate limiting | 0.5 day |
| Frontend flow (form + timer) | 1 day |
| Tests + edge cases | 1 day |
Total: 4–5 business days.
How we test authorization reliability?
Before delivery, we run a load scenario: 1000 concurrent requests to send a code, check rate limiting, simulate invalid numbers and expired OTPs. All this is covered by automated tests. Typical mistakes: not considering timezone for OTP TTL, caching number without region. We handle all edge cases.
SMS authorization via phone number is 2x faster than password login and 3x cheaper to maintain. Contact us — we'll evaluate your project in 1 hour. Order a turnkey implementation right now. Get a consultation on SMS authorization implementation.
Source: analysis of over 50 projects with SMS authorization over several years.







