Tạm biệt Mật khẩu: Triển khai Passkey (WebAuthn) với Node.js và SimpleWebAuthn

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Nỗi lo mang tên “Quên mật khẩu”

Bạn đã bao giờ thống kê xem người dùng tốn bao nhiêu thời gian chỉ để reset mật khẩu chưa? Với anh em dev, việc quản lý password cũng chẳng dễ dàng gì. Chúng ta phải lo chống Brute-force, ngăn Phishing, rồi cân nhắc chọn Bcrypt hay Argon2 để hash dữ liệu sao cho an toàn nhất.

Thực tế, mật khẩu truyền thống đang dần trở nên lỗi thời. Dù yêu cầu độ phức tạp cao đến đâu, người dùng vẫn có thói quen dùng lại pass cũ hoặc dễ dàng bị lừa nhập vào các trang giả mạo. Passkey (dựa trên WebAuthn) xuất hiện như một giải pháp triệt để. Nó cho phép đăng nhập bằng vân tay, FaceID hoặc mã PIN thiết bị mà không cần gửi bất kỳ chuỗi ký tự nhạy cảm nào lên server.

Tại sao Passkey lại an toàn vượt trội?

Cơ chế này hoạt động dựa trên hạ tầng khóa công khai (Public Key Cryptography). Server của bạn chỉ lưu Public Key. Khi người dùng đăng nhập, thiết bị của họ sẽ dùng Private Key nằm trong chip bảo mật để ký một bản tin xác thực.

Theo báo cáo từ FIDO Alliance, các hệ thống dùng Passkey có thể giảm tới 50% thời gian đăng nhập. Trong dự án gần nhất, team mình đã thử nghiệm và kết quả rất ấn tượng. Hacker dù có chiếm được database cũng vô dụng vì không có Private Key nằm trên điện thoại của khách hàng.

So sánh các phương thức xác thực

Tiêu chí Mật khẩu truyền thống 2FA (OTP/SMS) Passkey (WebAuthn)
Trải nghiệm Kém (phải nhớ, phải nhập) Phiền (đợi tin nhắn, nhập mã) Mượt (chạm vân tay/FaceID)
Chống Phishing Không Một phần Tuyệt đối
Độ khó triển khai Thấp Trung bình Trung bình

Thiết lập môi trường

Để triển khai nhanh nhất, mình khuyên dùng thư viện @simplewebauthn. Nó giúp xử lý các công đoạn chuyển đổi dữ liệu nhị phân (Binary) sang JSON rất gọn gàng.

Bắt đầu bằng việc khởi tạo dự án:

mkdir node-passkey-demo
cd node-passkey-demo
npm init -y
npm install express @simplewebauthn/server @simplewebauthn/browser express-session

Các bước triển khai thực tế

1. Đăng ký Passkey (Registration)

Đầu tiên, server cần tạo ra một bộ tùy chọn (Options) gửi xuống Client. Lưu ý quan trọng nhất là challenge. Đây là chuỗi ngẫu nhiên dùng để chống lại các cuộc tấn công replay (phát lại bản tin cũ).

const { generateRegistrationOptions } = require('@simplewebauthn/server');

app.get('/generate-registration-options', (req, res) => {
  const options = generateRegistrationOptions({
    rpName: 'My App',
    rpID: 'localhost',
    userID: 'user123',
    userName: '[email protected]',
    attestationType: 'none',
    authenticatorSelection: {
      residentKey: 'required',
      userVerification: 'preferred',
    },
  });

  req.session.currentChallenge = options.challenge;
  res.json(options);
});

Tại trình duyệt, bạn gọi hộp thoại xác thực của hệ điều hành bằng mã sau:

import { startRegistration } from '@simplewebauthn/browser';

async function register() {
  const resp = await fetch('/generate-registration-options');
  const options = await resp.json();

  // Trình duyệt hiển thị quét vân tay/FaceID
  const regResp = await startRegistration(options);

  await fetch('/verify-registration', {
    method: 'POST',
    body: JSON.stringify(regResp),
    headers: { 'Content-Type': 'application/json' }
  });
}

2. Xác thực và lưu trữ khóa

Khi người dùng quét vân tay xong, server phải kiểm tra chữ ký đó. Nếu hợp lệ, hãy lưu PublicKeyCredentialID vào database để dùng cho những lần đăng nhập sau.

const { verifyRegistrationResponse } = require('@simplewebauthn/server');

app.post('/verify-registration', async (req, res) => {
  const verification = await verifyRegistrationResponse({
    response: req.body,
    expectedChallenge: req.session.currentChallenge,
    expectedOrigin: 'http://localhost:3000',
    expectedRPID: 'localhost',
  });

  if (verification.verified) {
    // Lưu registrationInfo.credentialPublicKey vào DB
    res.status(200).send({ ok: true });
  }
});

Kinh nghiệm thực tế khi triển khai

Trong quá trình làm việc với WebAuthn, mình rút ra 3 lưu ý quan trọng:

  • Bắt buộc dùng HTTPS: WebAuthn sẽ từ chối hoạt động trên HTTP. Ngoại lệ duy nhất là localhost để anh em thuận tiện debug.
  • Đừng tự parse dữ liệu: Các khóa trả về thường ở dạng ArrayBuffer rất khó chịu. Hãy tin dùng thư viện để tránh các lỗi logic bảo mật đáng tiếc.
  • Luôn có phương án dự phòng: Đừng vội vã gỡ bỏ mật khẩu ngay lập tức. Hãy triển khai Passkey như một tùy chọn song song để người dùng làm quen dần.

Passkey có đáng để đầu tư?

Nếu bạn đang làm các ứng dụng về tài chính, SaaS hoặc thương mại điện tử, Passkey là một bước tiến lớn. Nó không chỉ nâng cấp bảo mật mà còn tạo ra trải nghiệm đăng nhập cực kỳ “xịn xò”.

Mình đã thử áp dụng cho một trang landing page và thấy tỷ lệ người dùng quay lại tăng rõ rệt. Đơn giản vì họ không còn ngại việc phải gõ những chuỗi ký tự dài ngoằng trên màn hình điện thoại nữa. Tương lai không mật khẩu đã ở ngay trước mắt, và việc làm chủ nó ngay từ bây giờ sẽ là lợi thế lớn cho sự nghiệp của bạn.

Share: