Luồng chính:
1. User nhập số điện thoại → server chuẩn hóa về E.164 (+84...) để tránh cùng số lưu hai dạng.
2. Sinh OTP 6 chữ số bằng CSPRNG, lưu hash kèm expires_at (3–5 phút) và attempts = 0. Không lưu OTP thô.
3. Gửi SMS, trả về client một request_id — không trả OTP về response dù ở môi trường dev.
4. User nhập OTP → tra theo request_id, so hash bằng hàm so sánh hằng thời gian, kiểm hạn và số lần thử.
5. Đúng → xóa OTP ngay (dùng một lần), tạo phiên. Sai → tăng attempts, quá 5 lần thì hủy mã, bắt gửi lại.
create table otp_requests (
id uuid primary key,
phone text not null, -- E.164
code_hash text not null,
attempts int not null default 0,
expires_at timestamptz not null,
consumed_at timestamptz
);
create index on otp_requests (phone, expires_at);Các điểm bảo mật phải nêu:
- Rate limit hai chiều: theo số điện thoại (vd 1 mã/60 giây, 5 mã/giờ) và theo IP. Không có nó thì bị lạm dụng gửi SMS gây tốn tiền (SMS pumping).
- Giới hạn số lần thử — OTP 6 số chỉ có 10^6 khả năng, không giới hạn thì dò được.
- Phản hồi đồng nhất: số chưa đăng ký cũng trả về "đã gửi mã" để không lộ số nào tồn tại.
- Không tái sử dụng mã khi user bấm gửi lại; mã cũ phải bị vô hiệu.
Một lưu ý nghề: OWASP xếp SMS là yếu tố yếu nhất trong MFA vì rủi ro SIM swap và chặn SMS. Với tài khoản có giá trị cao nên hướng user sang TOTP hoặc passkey; OTP SMS hợp với ngữ cảnh cần độ phủ rộng như thị trường VN.