Tại sao mật khẩu thôi là chưa đủ?
Cách đây 2 năm, khi đang làm dự án cho một ví điện tử, sếp hỏi mình: “Nếu hacker bê được cả database chứa mật khẩu đã hash, tiền của khách có bay màu không?”. Mình im lặng. Thực tế, với một dàn máy trang bị RTX 4090, việc brute-force các mã hash MD5 hay SHA1 đơn giản chỉ là chuyện trong vài nốt nhạc. Đó là lúc mình hiểu 2FA (Two-Factor Authentication) không phải tùy chọn, mà là sống còn.
TOTP (Time-based One-Time Password) hiện là tiêu chuẩn vàng cho việc này. Bạn hẳn đã quen với việc mở Google Authenticator lấy 6 con số thay đổi mỗi 30 giây. Cơ chế này cực thông minh: không tốn 0.05 USD cho mỗi tin nhắn SMS, cũng chẳng lo bị SIM swap. Mọi thứ vận hành dựa trên một thuật toán đồng bộ giữa server và điện thoại người dùng.
Triển khai hệ thống này với Node.js thực ra rất gọn. Hãy cùng mình bắt tay vào làm.
Cài đặt công cụ
Chúng ta sẽ sử dụng bộ đôi thư viện phổ biến nhất hiện nay:
- speakeasy: “Trái tim” xử lý logic tạo secret key và kiểm tra mã OTP.
- qrcode: Biến các chuỗi ký tự khô khan thành mã QR cho người dùng quét.
Khởi tạo dự án nhanh bằng lệnh:
mkdir node-2fa-lab && cd node-2fa-lab
npm init -y
npm install speakeasy qrcode express
Quy trình triển khai 3 bước thực chiến
Trong các hệ thống lớn, mình thường chia quy trình này thành 3 giai đoạn tách biệt để dễ bảo trì và mở rộng.
Bước 1: Khởi tạo “Chìa khóa bí mật” (Secret Key)
Mỗi tài khoản cần một Secret Key riêng. Key này giống như mật khẩu cấp 2, được lưu ở cả server (database) và ứng dụng của người dùng.
const speakeasy = require('speakeasy');
const secret = speakeasy.generateSecret({
name: "MyApp ([email protected])",
});
console.log("Base32 Key:", secret.base32); // Lưu cái này vào DB
console.log("OTP Auth URL:", secret.otpauth_url);
Lưu ý quan trọng: Luôn sử dụng định dạng base32. Đây là chuẩn chung mà Google Authenticator hay Authy yêu cầu để hoạt động chính xác.
Bước 2: Tạo trải nghiệm quét mã QR mượt mà
Bắt người dùng gõ tay một chuỗi 32 ký tự là cách nhanh nhất để họ gỡ app của bạn. Thay vào đó, hãy dùng mã QR. Trong một dự án mình từng làm, việc thêm mã QR giúp giảm 80% ticket hỗ trợ liên quan đến lỗi “nhập sai key”.
const QRCode = require('qrcode');
app.get('/setup-2fa', async (req, res) => {
const secret = speakeasy.generateSecret({ name: "SecureApp" });
// Chuyển URL thành định dạng Base64 để nhúng thẳng vào thẻ img
const dataUrl = await QRCode.toDataURL(secret.otpauth_url);
res.send(`
<h3>Quét mã để kích hoạt bảo mật</h3>
<img src="${dataUrl}">
<p>Nhập 6 số từ ứng dụng để xác nhận</p>
`);
});
Bước 3: Xác thực Token (Verify)
Khi người dùng nhập 6 con số, server sẽ đối chiếu nó với Secret Key đã lưu. Đây là nơi phép màu xảy ra.
app.post('/verify-2fa', (req, res) => {
const { userToken, storedSecret } = req.body;
const verified = speakeasy.totp.verify({
secret: storedSecret,
encoding: 'base32',
token: userToken,
window: 1 // Cho phép lệch 30 giây
});
if (verified) {
return res.json({ success: true, message: "Xác thực thành công!" });
}
res.status(400).json({ success: false, message: "Mã không hợp lệ." });
});
Mẹo nhỏ: Tham số window: 1 cực kỳ đáng giá. Thời gian giữa điện thoại và server thường lệch nhau vài giây. Nếu để mặc định (window: 0), người dùng sẽ rất ức chế vì mã vừa nhảy số mới đã bị báo lỗi.
Những “cái bẫy” cần tránh khi lên Production
Code chạy được ở Local mới chỉ là 50% chặng đường. Dưới đây là những bài học “xương máu” mình rút ra sau nhiều lần xử lý sự cố:
1. Mã khôi phục (Backup Codes) là bắt buộc
User mất điện thoại là chuyện thường ngày. Nếu không có mã khôi phục, họ sẽ bị khóa tài khoản vĩnh viễn. Hãy tạo sẵn 10 mã ngẫu nhiên, hash chúng như mật khẩu và yêu cầu user lưu vào nơi an toàn ngay khi bật 2FA.
2. Tuyệt đối không lưu Plain-text Secret Key
Nếu DB bị lộ, Secret Key lộ theo thì 2FA vô tác dụng. Hãy mã hóa Key bằng AES-256 trước khi cất vào DB. Chỉ giải mã nó ở tầng logic khi cần verify.
3. Chặn đứng tấn công Brute-force
Mã OTP chỉ có 1 triệu khả năng (000000 – 999999). Một script đơn giản có thể quét sạch trong vài phút. Bạn phải giới hạn số lần thử. Ví dụ: Sai 5 lần thì khóa chức năng verify trong 15 phút. Sử dụng express-rate-limit là cách triển khai nhanh nhất.
4. Lệch đồng hồ server
Vì TOTP sống dựa vào thời gian, đồng hồ server sai 1 phút là hệ thống “sập” hoàn toàn. Hãy luôn cài đặt NTP (Network Time Protocol) để đồng bộ giờ. Trên Linux, hãy chạy timedatectl để chắc chắn mọi thứ đang chuẩn xác.
Hy vọng những chia sẻ này giúp bạn tự tin hơn khi triển khai bảo mật cho dự án. Nhớ rằng, trong bảo mật, cẩn thận không bao giờ là thừa!

