Bảo mật trang web của bạn với xác thực hai yếu tố bằng khóa bảo mật (WebAuthn)

1. Sản phẩm bạn sẽ tạo ra

Bạn sẽ bắt đầu với ứng dụng web cơ bản hỗ trợ đăng nhập dựa trên mật khẩu.

Sau đó, bạn sẽ hỗ trợ tính năng xác thực hai yếu tố thông qua khóa bảo mật dựa trên WebAuthn. Để làm như vậy, bạn sẽ triển khai như sau:

  • Cách để người dùng đăng ký thông tin xác thực WebAuthn.
  • Một quy trình xác thực hai yếu tố mà trong đó người dùng được yêu cầu cung cấp thông tin xác thực thứ hai – thông tin đăng nhập WebAuthn – nếu họ đã đăng ký một yếu tố.
  • Giao diện quản lý thông tin đăng nhập: danh sách thông tin đăng nhập cho phép người dùng đổi tên và xóa thông tin đăng nhập.

16ce77744061c5f7.png

Xem ứng dụng web đã hoàn thiện và dùng thử.

2. Giới thiệu về WebAuthn

Kiến thức cơ bản về WebAuthn

Tại sao nên sử dụng WebAuthn?

Lừa đảo là một vấn đề bảo mật nghiêm trọng trên web: hầu hết các hành vi vi phạm tài khoản đều sử dụng mật khẩu yếu hoặc bị đánh cắp để sử dụng lại trên các trang web. Phản hồi chung của ngành đối với vấn đề này là xác thực nhiều yếu tố, nhưng việc triển khai bị phân mảnh và nhiều việc vẫn chưa giải quyết được hành vi lừa đảo một cách đầy đủ.

API xác thực web hay WebAuthn là một giao thức chống lừa đảo được chuẩn hóa, có thể sử dụng cho bất kỳ ứng dụng web nào.

Cách hoạt động

Nguồn: webauthn.guide

WebAuthn cho phép máy chủ đăng ký và xác thực người dùng bằng cách sử dụng phương thức mã hóa khóa công khai thay vì mật khẩu. Các trang web có thể tạo thông tin xác thực, bao gồm một cặp khóa riêng tư-công khai.

  • Khóa riêng tư được lưu trữ an toàn trên thiết bị của người dùng.
  • Khoá công khai và mã thông tin xác thực được tạo ngẫu nhiên sẽ được gửi đến máy chủ để lưu trữ.

Khóa công khai được máy chủ sử dụng để chứng minh danh tính của người dùng. Tính năng này không bí mật vì không hữu ích nếu không có khóa riêng tư tương ứng.

Lợi ích

WebAuthn có hai lợi ích chính:

  • Không có thông tin mật bí mật được chia sẻ: máy chủ không lưu trữ thông tin mật. Điều này khiến cơ sở dữ liệu kém hấp dẫn hơn với tin tặc, vì các khoá công khai không hữu ích với chúng.
  • Thông tin đăng nhập theo phạm vi: bạn không thể sử dụng thông tin đăng nhập cho site.example theo evil-site.example. Điều này giúp WebAuthn chống lừa đảo.

Trường hợp sử dụng

Một trường hợp sử dụng cho WebAuthn là xác thực hai yếu tố bằng khóa bảo mật. Điều này có thể đặc biệt phù hợp với các ứng dụng web dành cho doanh nghiệp.

Hỗ trợ trình duyệt

Ứng dụng này được viết bởi W3C và FIDO, với sự tham gia của Google, Mozilla, Microsoft, Jibe và Google.

Bảng thuật ngữ

  • Authenticator: một thực thể phần mềm hoặc phần cứng có thể đăng ký người dùng và sau đó xác nhận quyền sở hữu thông tin đăng nhập. Có hai loại trình xác thực:
  • Trình xác thực chuyển vùng: trình xác thực có thể sử dụng với bất kỳ thiết bị nào mà người dùng đang cố đăng nhập sử dụng. Ví dụ: khoá bảo mật USB, điện thoại thông minh.
  • Trình xác thực nền tảng: một trình xác thực được tích hợp sẵn vào thiết bị của người dùng. Ví dụ: Touch ID của Apple.
  • Thông tin xác thực: cặp khóa riêng tư công khai
  • Bên chuyển tiếp: (máy chủ cho) trang web đang cố xác thực người dùng
  • Máy chủ FIDO: máy chủ dùng để xác thực. FIDO là một nhóm giao thức do liên minh FIDO phát triển, một trong những giao thức này là WebAuthn.

Trong hội thảo này, chúng ta sẽ dùng trình xác thực chuyển vùng.

3. Trước khi bắt đầu

Bạn cần có

Để hoàn tất lớp học lập trình này, bạn cần có:

  • Kiến thức cơ bản về WebAuthn.
  • Kiến thức cơ bản về JavaScript và HTML.
  • Trình duyệt cập nhật hỗ trợ WebAuthn.
  • Khóa bảo mật tương thích với U2F.

Bạn có thể sử dụng một trong các khóa sau làm khóa bảo mật:

  • Điện thoại Android chạy Android>=7 (Nougat) chạy Chrome. Trong trường hợp này, bạn cũng cần có máy chạy Windows, macOS hoặc Chrome OS có Bluetooth đang hoạt động.
  • Khóa USB, chẳng hạn như YubiKey.

6539dc7ffec2538c.png

Nguồn: https://www.yubico.com/products/security-key/

dd56e2cfe0f7ced2.png

Kiến thức bạn sẽ học được

Bạn sẽ học ✔

  • Cách đăng ký và sử dụng khoá bảo mật làm yếu tố thứ hai để xác thực WebAuthn.
  • Cách làm cho quy trình này thân thiện với người dùng.

Bạn sẽ không học được ❌

  • Cách xây dựng máy chủ FIDO — máy chủ được sử dụng để xác thực. Điều này không sao cả, thường là với tư cách là nhà phát triển trang web hoặc ứng dụng web, bạn sẽ dựa vào việc triển khai máy chủ FIDO hiện có. Hãy đảm bảo luôn xác minh chức năng và chất lượng của quá trình triển khai máy chủ mà bạn dựa vào. Trong lớp học lập trình này, máy chủ FIDO sử dụng SimpleWebAuthn. Để biết các tùy chọn khác, hãy xem trang chính thức của Liên minh FIDO. Đối với thư viện nguồn mở, hãy truy cập vào webauthn.io hoặc AwesomeWebAuthn.

Tuyên bố từ chối trách nhiệm

Người dùng phải nhập mật khẩu để đăng nhập. Tuy nhiên, để đơn giản trong lớp học lập trình này, mật khẩu không được lưu trữ hoặc kiểm tra. Trong một ứng dụng thực tế, bạn sẽ kiểm tra xem phía máy chủ có đúng không.

Các quá trình kiểm tra cơ bản về bảo mật, chẳng hạn như kiểm tra CSRF, xác thực phiên và dọn dẹp dữ liệu đầu vào được triển khai trong lớp học lập trình này. Tuy nhiên, nhiều biện pháp bảo mật thì không. Ví dụ: không có giới hạn đầu vào đối với mật khẩu để ngăn các cuộc tấn công brute force. Ở đây không quan trọng vì mật khẩu không được lưu trữ, nhưng hãy đảm bảo không sử dụng mã này như trong quá trình sản xuất.

4. Thiết lập ứng dụng xác thực

Nếu bạn đang sử dụng điện thoại Android làm trình xác thực

  • Nhớ cập nhật Chrome trên cả máy tính và điện thoại.
  • Trên cả máy tính và điện thoại, hãy mở Chrome và đăng nhập bằng cùng một hồ sơ⏤hồ sơ bạn muốn sử dụng cho hội thảo này.
  • Bật tính năng Đồng bộ hóa cho hồ sơ này, trên máy tínhđiện thoại. Hãy dùng chrome://settings/syncSetup khi dùng.
  • Bật Bluetooth trên cả máy tính và điện thoại.
  • Trong Chrome dành cho máy tính để bàn đã đăng nhập bằng cùng một hồ sơ, hãy mở webauthn.io.
  • Hãy nhập một tên người dùng đơn giản. Để nguyên giá trị Loại chứng thựcLoại xác thực thành giá trị Không cóKhông xác định (mặc định). Nhấp vào Đăng ký.

6b49ff0298f5a0af.png

  • Một cửa sổ trình duyệt sẽ mở ra, yêu cầu bạn xác minh danh tính. Chọn điện thoại của bạn trong danh sách.

ffebe58ac826eaf2.png 852de328fcd4eb42.png

  • Trên điện thoại, bạn sẽ nhận được thông báo có tiêu đề Xác minh danh tính của bạn. Nhấn vào ứng dụng đó.
  • Trên điện thoại, bạn sẽ được yêu cầu cung cấp mã PIN của điện thoại (hoặc chạm vào cảm biến vân tay). Nhập tên.
  • Trên webauthn.io trên máy tính, chỉ báo "Success" sẽ xuất hiện.

fc0acf00a4d412fa.png

  • Trên webauthn.io trên máy tính, hãy nhấp vào nút Đăng nhập.
  • Một lần nữa, cửa sổ trình duyệt sẽ mở ra; chọn điện thoại của bạn trong danh sách.
  • Trên điện thoại, hãy nhấn vào thông báo bật lên và nhập mã PIN (hoặc chạm vào cảm biến vân tay).
  • webauthn.io phải cho bạn biết rằng bạn đã đăng nhập. Điện thoại của bạn đang hoạt động như một chìa khóa bảo mật; bạn đã sẵn sàng để tham gia hội thảo!

Nếu bạn đang sử dụng khóa bảo mật USB làm trình xác thực

  • Trong Chrome dành cho máy tính, hãy mở webauthn.io.
  • Hãy nhập một tên người dùng đơn giản. Để nguyên giá trị Loại chứng thựcLoại xác thực thành giá trị Không cóKhông xác định (mặc định). Nhấp vào Đăng ký.
  • Một cửa sổ trình duyệt sẽ mở ra, yêu cầu bạn xác minh danh tính. Chọn Khóa bảo mật USB trong danh sách.

ffebe58ac826eaf2.png 9fe75f04e43da035.png

  • Hãy cắm khoá bảo mật vào màn hình rồi chạm vào đó.

923d5adb8aa8286c.png

  • Trên webauthn.io trên máy tính, chỉ báo "Success" sẽ xuất hiện.

fc0acf00a4d412fa.png

  • Trên webauthn.io trên máy tính, hãy nhấp vào nút Đăng nhập.
  • Một lần nữa, cửa sổ trình duyệt sẽ mở ra; hãy chọn Khóa bảo mật USB trong danh sách.
  • Chạm vào phím.
  • Webauthn.io sẽ thông báo cho bạn rằng bạn đã đăng nhập. Khóa bảo mật USB đang hoạt động bình thường; bạn đã sẵn sàng để tham gia hội thảo!

7e1c0bb19c9f3043.png

5. Bắt đầu thiết lập

Trong lớp học lập trình này, bạn sẽ sử dụng Gl App, một trình soạn thảo mã trực tuyến, tự động triển khai mã của bạn ngay lập tức.

Sử dụng mã khởi động

Mở dự án dành cho người mới bắt đầu.

Nhấp vào nút Remix.

Thao tác này sẽ tạo ra một bản sao của mã dành cho người mới bắt đầu. Giờ đây, bạn đã có mã của riêng mình để chỉnh sửa. Phiên bản fork (được gọi là "remix" trong Glcab) là nơi bạn sẽ thực hiện mọi công việc cho lớp học lập trình này.

{4}2b9f552c9809b6.png

Khám phá mã dành cho người mới bắt đầu

Khám phá mã dành cho người mới bắt đầu mà bạn vừa phân phát một chút.

Hãy lưu ý rằng trong libs, một thư viện có tên là auth.js đã được cung cấp. Đó là một thư viện tùy chỉnh đảm nhận logic xác thực phía máy chủ. Ứng dụng này sử dụng thư viện fido làm phần phụ thuộc.

6. Triển khai đăng ký thông tin xác thực

Triển khai đăng ký thông tin xác thực

Điều đầu tiên chúng tôi cần để thiết lập xác thực hai yếu tố bằng khóa bảo mật là cho phép người dùng tạo thông tin xác thực.

Trước tiên, hãy thêm một hàm để thực hiện việc này trong mã phía máy khách của chúng ta.

Trong public/auth.client.js, hãy lưu ý rằng một hàm có tên là registerCredential()chưa làm được gì. Thêm mã sau vào đó:

async function registerCredential() {
  // Fetch the credential creation options from the backend
  const credentialCreationOptionsFromServer = await _fetch(
    "/auth/credential-options",
    "POST"
  );
  // Decode the credential creation options
  const credentialCreationOptions = decodeServerOptions(
    credentialCreationOptionsFromServer
  );
  // Create a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
  const credential = await navigator.credentials.create({
    publicKey: {
      ...credentialCreationOptions,
    }
  });
  // Encode the newly created credential to send it to the backend
  const encodedCredential = encodeCredential(credential);
  // Send the encoded credential to the backend for storage
  return await _fetch("/auth/credential", "POST", encodedCredential);
}

Xin lưu ý rằng chức năng này đã được xuất cho bạn.

Đây là những việc registerCredential làm:

  • Phương thức này tìm nạp các tùy chọn tạo thông tin xác thực từ máy chủ (/auth/credential-options)
  • Vì các tùy chọn máy chủ quay trở lại, nên nó sử dụng hàm tiện ích decodeServerOptions để giải mã các tùy chọn này.
  • Lệnh này tạo thông tin xác thực bằng cách gọi API web navigator.credential.create. Khi navigator.credential.create được gọi, trình duyệt sẽ tiếp quản và nhắc người dùng chọn khóa bảo mật.
  • Tệp này giải mã thông tin đăng nhập mới tạo
  • Thẻ này đăng ký phía máy chủ thông tin đăng nhập mới bằng cách gửi yêu cầu tới /auth/credential chứa thông tin đăng nhập đã mã hóa.

Ngoài ra: hãy xem mã máy chủ

registerCredential() thực hiện hai lệnh gọi đến máy chủ, vì vậy, hãy dành một chút thời gian để xem điều gì đang xảy ra trong phần phụ trợ.

Tùy chọn tạo thông tin xác thực

Khi ứng dụng gửi yêu cầu tới (/auth/credential-options), máy chủ sẽ tạo một đối tượng tùy chọn và gửi lại đối tượng này cho ứng dụng.

Sau đó, ứng dụng sẽ sử dụng đối tượng này trong lệnh gọi tạo thông tin xác thực thực tế:

navigator.credentials.create({
    publicKey: {
    // Options generated server-side
    ...credentialCreationOptions
// ...
}

Vậy, điều gì trong credentialCreationOptions này cuối cùng đã được sử dụng trong phía máy khách registerCredential mà bạn đã triển khai trong bước trước đó?

Hãy xem mã máy chủ trong phần bộ định tuyến."/credential-options", ...

Hãy xem xét mọi thuộc tính, nhưng dưới đây là một vài thuộc tính thú vị mà bạn có thể thấy trong đối tượng tùy chọn mã máy chủ, được tạo bằng thư viện fido2 và cuối cùng được trả về cho ứng dụng:

  • rpNamerpId mô tả tổ chức đăng ký và xác thực người dùng. Xin lưu ý rằng trong WebAuthn, thông tin đăng nhập nằm trong phạm vi một miền nhất định. Đây là một lợi ích bảo mật; rpNamerpId ở đây được dùng để xác định thông tin đăng nhập. rpId hợp lệ chẳng hạn như tên máy chủ của trang web. Hãy lưu ý cách chúng tôi tự động cập nhật những dự án này khi bạn bắt đầu dự án dành cho người mới bắt đầu 🧘🏻 ♀️
  • excludeCredentials là danh sách các thông tin đăng nhập; không thể tạo thông tin đăng nhập mới trên trình xác thực cũng chứa một trong các thông tin xác thực được liệt kê trong excludeCredentials. Trong lớp học lập trình của chúng tôi, excludeCredentials là danh sách các thông tin đăng nhập hiện có cho người dùng này. Với việc này và user.id, chúng tôi sẽ đảm bảo rằng mỗi thông tin đăng nhập mà người dùng tạo sẽ hiển thị trên một trình xác thực (khóa bảo mật) khác nhau. Đây là một phương pháp hay vì điều đó có nghĩa là nếu người dùng đã đăng ký nhiều thông tin xác thực, thì họ sẽ sử dụng nhiều trình xác thực (khóa bảo mật) khác nhau, do đó, việc mất một khóa bảo mật sẽ không khóa người dùng khỏi tài khoản của họ.
  • authenticatorSelection xác định loại trình xác thực bạn muốn cho phép trong ứng dụng web của mình. Hãy cùng tìm hiểu kỹ hơn về authenticatorSelection:
    • residentKey: preferred có nghĩa là ứng dụng này không thực thi thông tin đăng nhập có thể phát hiện phía máy khách. Thông tin đăng nhập phía máy khách là một loại thông tin xác thực đặc biệt giúp bạn có thể xác thực người dùng mà không cần xác định trước đó. Ở đây, chúng tôi đã thiết lập preferred vì lớp học lập trình này tập trung vào việc triển khai cơ bản; thông tin đăng nhập có thể khám phá dành cho các luồng nâng cao hơn.
    • requireResidentKey chỉ hiện diện cho khả năng tương thích ngược với WebAuthn phiên bản 1.
    • userVerification: preferred có nghĩa là nếu trình xác thực hỗ trợ quy trình xác minh người dùng – ví dụ: nếu đó là khóa bảo mật sinh trắc học hoặc khóa có tính năng mã PIN tích hợp, thì bên phụ thuộc sẽ yêu cầu xác thực khi tạo thông tin xác thực. Nếu trình xác thực không—khóa bảo mật cơ bản—thì máy chủ sẽ không yêu cầu xác minh người dùng.
  • ​​pubKeyCredParam mô tả các thuộc tính mật mã mong muốn theo thông tin đăng nhập, theo thứ tự ưu tiên.

Tất cả những tùy chọn này là quyết định mà ứng dụng web cần đưa ra cho mô hình bảo mật của ứng dụng. Hãy lưu ý rằng trên máy chủ, các tùy chọn này được xác định trong một đối tượng authSettings.

Thách thức

Một điều thú vị khác ở đây là req.session.challenge = options.challenge;.

Vì WebAuthn là một giao thức mã hóa nên giao thức này phụ thuộc vào các thử thách ngẫu nhiên để tránh các cuộc tấn công phát lại – khi kẻ tấn công đánh cắp một phần tải dữ liệu để phát lại quá trình xác thực, khi họ không phải là chủ sở hữu của khóa riêng tư bật chức năng xác thực.

Để giảm thiểu điều này, thử thách được tạo trên máy chủ và sẽ được ký nhanh; chữ ký sẽ được so sánh với những gì được dự kiến. Việc này sẽ xác minh rằng người dùng lưu giữ khóa riêng tư tại thời điểm tạo thông tin xác thực.

Mã đăng ký thông tin xác thực

Hãy xem mã máy chủ trong phần Router.post("/credential", ...).

Đây là nơi thông tin đăng nhập được đăng ký phía máy chủ.

Vậy, điều gì đang diễn ra ở đó?

Một trong những bước đáng chú ý nhất trong mã này là lệnh gọi xác minh, qua fido2.verifyAttestationResponse:

  • Xác thực đã ký sẽ được kiểm tra và điều này đảm bảo rằng thông tin đăng nhập đã được tạo bởi một người đã thực sự giữ khóa riêng tư vào thời điểm tạo.
  • Mã của bên dựa trên, được giới hạn về nguồn gốc, cũng được xác minh. Điều này đảm bảo rằng thông tin đăng nhập sẽ được ràng buộc với ứng dụng web này (và chỉ ứng dụng web này).

Thêm chức năng này vào giao diện người dùng

Bây giờ, hàm của bạn để tạo thông tin xác thực, ``registerregister(),đã sẵn sàng, hãy để bạn cung cấp thông tin đăng nhập cho người dùng.

Bạn sẽ thực hiện việc này từ trang Tài khoản, vì đây là vị trí thông thường để quản lý xác thực.

Trong mã đánh dấu của account.html, bên dưới tên người dùng, hiện có một div trống cho đến khi có một lớp bố cục class="flex-h-between". Chúng tôi sẽ sử dụng div này cho các phần tử giao diện người dùng liên quan đến chức năng 2FA.

Thêm ino div này:

  • Tiêu đề có nội dung "Xác thực hai yếu tố"
  • Một nút dùng để tạo thông tin đăng nhập
 <div class="flex-h-between">
    <h3>
        Two-factor authentication
    </h3>
    <button class="create" id="registerButton" raised>
        ➕ Add a credential
    </button>
</div>

Bên dưới div này, hãy thêm một div thông tin xác thực mà chúng ta sẽ cần sau này:

<div class="flex-h-between">
(HTML you've just added)
</div>
<div id="credentials"></div>

Trong tập lệnh cùng dòng account.html, hãy nhập hàm mà bạn vừa tạo và thêm một hàm register để gọi hàm đó, cũng như trình xử lý sự kiện được đính kèm vào nút mà bạn vừa tạo.

// Set up the handler for the button that registers credentials
const registerButton = document.querySelector('#registerButton');
registerButton.addEventListener('click', register);

// Register a credential
async function register() {
  let user = {};
  try {
    const user = await registerCredential();
  } catch (e) {
    // Alert the user that something went wrong
    if (Array.isArray(e)) {
      alert(
        // `msg` not `message`, this is the key's name as per the express validator API
        `Registration failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
      );
    } else {
      alert(`Registration failed. ${e}`);
    }
  }
}

Hiển thị thông tin xác thực để người dùng xem

Giờ đây, bạn đã thêm chức năng tạo thông tin đăng nhập, nên người dùng cần có một cách để xem thông tin đăng nhập mà họ đã thêm.

Trang Tài khoản là một nơi phù hợp để làm việc này.

Trong account.html, hãy tìm hàm có tên là updateCredentialList().

Thêm vào đó mã sau để thực hiện lệnh gọi phụ trợ để tìm nạp tất cả thông tin đăng nhập đã đăng ký của người dùng hiện đang đăng nhập và hiển thị thông tin xác thực được trả về:

// Update the list that displays credentials
async function updateCredentialList() {
  // Fetch the latest credential list from the backend
  const response = await _fetch('/auth/credentials', 'GET');
  const credentials = response.credentials || [];
  // Generate the credential list as HTML and pass remove/rename functions as args
  const credentialListHtml = getCredentialListHtml(
    credentials,
    removeEl,
    renameEl
  );
  // Display the list of credentials in the DOM
  const list = document.querySelector('#credentials');
  render(credentialListHtml, list);
}    

Hiện tại, đừng ngại removeElrenameEl; bạn sẽ tìm hiểu về chúng sau trong lớp học lập trình này.

Thêm một lệnh gọi vào updateCredentialList ở đầu tập lệnh cùng dòng của bạn, trong phạm vi account.html. Với cuộc gọi này, thông tin đăng nhập có sẵn sẽ được tìm nạp khi người dùng truy cập vào trang tài khoản của họ.

<script type="module">
    // ... (imports)
    // Initialize the credential list by updating it once on page load
    updateCredentialList();

Bây giờ, hãy gọi updateCredentialList khi registerCredential đã hoàn tất thành công, để danh sách hiển thị thông tin xác thực mới tạo:

async function register() {
  let user = {};
  try {
    // ...
  } catch (e) {
    // ...
  }
  // Refresh the credential list to display the new credential
  await updateCredentialList();
}

Hãy thử xem! 👩🏻 💻

Bạn đã hoàn tất việc đăng ký thông tin xác thực! Giờ đây, người dùng có thể tạo thông tin xác thực dựa trên khóa bảo mật và trực quan hóa chúng trong trang Tài khoản của mình.

Hãy thử nói:

  • Đăng xuất.
  • Đăng nhập bằng bất kỳ người dùng và mật khẩu nào. Như đã đề cập trước đó, mật khẩu chưa được kiểm tra tính chính xác để đảm bảo mọi thứ đơn giản trong lớp học lập trình này. Nhập bất kỳ mật khẩu nào không để trống.
  • Sau khi bạn ở trên trang Tài khoản, hãy nhấp vào Thêm thông tin đăng nhập.
  • Bạn sẽ được nhắc chèn và chạm vào khóa bảo mật. Hãy làm việc này ngay.
  • Khi thông tin đăng nhập được tạo thành công, thông tin đăng nhập sẽ hiển thị trên trang tài khoản.
  • Tải lại trang Tài khoản. Thông tin xác thực phải được hiển thị.
  • Nếu bạn có hai khóa này, hãy thử thêm hai khóa bảo mật khác nhau làm thông tin đăng nhập. Cả hai đều phải được hiển thị.
  • Thử tạo hai thông tin xác thực bằng cùng một trình xác thực (khóa); bạn sẽ nhận thấy rằng thông tin đăng nhập đó sẽ không được hỗ trợ. Đó là chủ ý của chúng tôi – điều này là do chúng tôi sử dụng excludeCredentials trong phần phụ trợ.

7. Bật tính năng xác thực hai yếu tố

Người dùng của bạn có thể đăng ký và hủy đăng ký thông tin đăng nhập, nhưng thông tin đăng nhập chỉ hiển thị và chưa thực sự được sử dụng.

Giờ là lúc sử dụng chúng và thiết lập tính năng xác thực hai yếu tố thực tế.

Trong phần này, bạn sẽ thay đổi quy trình xác thực trong ứng dụng web của mình từ quy trình cơ bản sau:

6ff49a7e520836d0.png

Quy trình có hai yếu tố này:

e7409946cd88efc7.png

Triển khai chế độ xác thực yếu tố thứ hai

Trước tiên, hãy thêm chức năng mà chúng tôi cần và triển khai chức năng giao tiếp với phần phụ trợ; chúng tôi sẽ thêm chức năng này vào giao diện người dùng trong bước tiếp theo.

Những gì bạn cần triển khai ở đây là hàm xác thực người dùng có thông tin đăng nhập.

Trong public/auth.client.js, hãy tìm hàm trống authenticateTwoFactor rồi thêm mã sau:

async function authenticateTwoFactor() {
  // Fetch the 2F options from the backend
  const optionsFromServer = await _fetch("/auth/two-factor-options", "POST");
  // Decode them
  const decodedOptions = decodeServerOptions(optionsFromServer);
  // Get a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
  const credential = await navigator.credentials.get({
    publicKey: decodedOptions
  });
  // Encode the credential
  const encodedCredential = encodeCredential(credential);
  // Send it to the backend for verification
  return await _fetch("/auth/authenticate-two-factor", "POST", {
    credential: encodedCredential
  });
}

Lưu ý rằng chức năng này đã được xuất cho bạn; chúng tôi sẽ cần chức năng này trong bước tiếp theo.

Đây là những việc authenticateTwoFactor làm:

  • Yêu cầu hai tùy chọn xác thực từ máy chủ. Giống như các tùy chọn tạo thông tin xác thực mà bạn thấy trước đây, các tùy chọn này được xác định trên máy chủ và tùy thuộc vào mô hình bảo mật của ứng dụng web. Tìm hiểu mã máy chủ trong router.post("/two-factors-options", ... để biết chi tiết.
  • Khi gọi navigator.credentials.get, trình duyệt sẽ tiếp nhận và nhắc người dùng chèn rồi chạm vào một khóa đã đăng ký trước đó. Việc này sẽ dẫn đến việc chọn một thông tin xác thực cho thao tác xác thực hai yếu tố cụ thể này.
  • Sau đó, thông tin đăng nhập đã chọn sẽ được chuyển vào một yêu cầu phụ trợ để tìm nạp("/auth/Authentication-two-yt"`. Nếu thông tin đăng nhập hợp lệ cho người dùng đó thì người dùng sẽ được xác thực.

Ngoài ra: hãy xem mã máy chủ

Hãy lưu ý rằng server.js đã thực hiện một số thao tác và cách truy cập: đảm bảo rằng chỉ người dùng được xác thực mới có thể truy cập vào trang Tài khoản, cũng như thực hiện một số lệnh chuyển hướng cần thiết.

Bây giờ, hãy xem mã máy chủ trong phần router.post("/initialize-authentication", ....

Có hai điểm thú vị cần lưu ý trong đó:

  • Cả mật khẩu và thông tin xác thực đều được kiểm tra đồng thời ở giai đoạn này. Đây là một biện pháp bảo mật: đối với những người dùng đã thiết lập tính năng xác thực hai yếu tố, chúng tôi không muốn luồng giao diện người dùng trông khác nhau, tùy thuộc vào việc mật khẩu có chính xác hay không. Vì vậy, chúng tôi kiểm tra cả mật khẩu và thông tin xác thực cùng một lúc trong bước này.
  • Nếu cả mật khẩu và thông tin xác thực đều hợp lệ, thì chúng ta sẽ hoàn tất quá trình xác thực bằng cách gọi completeAuthentication(req, res);. Điều này có nghĩa là chúng ta thực hiện việc chuyển các phiên , từ phiên auth tạm thời mà người dùng chưa được xác thực, sang phiên chính main, nơi người dùng được xác thực.

Thêm trang xác thực hai yếu tố vào luồng người dùng

Trong thư mục views, hãy chú ý đến trang mới second-factor.html.

Nút này có nút Sử dụng khóa bảo mật, nhưng hiện tại, nút này không thực hiện thao tác nào.

Đặt nút này vào nút gọi authenticateTwoFactor() khi nhấp vào.

  • Nếu authenticateTwoFactor() thành công, hãy chuyển hướng người dùng đến trang Tài khoản của họ.
  • Nếu cách đó không thành công, hãy thông báo cho người dùng rằng đã xảy ra lỗi. Trong một ứng dụng thực tế, bạn sẽ triển khai các thông báo lỗi hữu ích hơn – để đơn giản hóa trong bản minh họa này, chúng tôi sẽ chỉ sử dụng cảnh báo cửa sổ.
    <main>
...
    </main>
    <script type="module">
      import { authenticateTwoFactor, authStatuses } from "/auth.client.js";

      const button = document.querySelector("#authenticateButton");
      button.addEventListener("click", async e => {
        try {
          // Ask the user to authenticate with the second factor; this will trigger a browser prompt
          const response = await authenticateTwoFactor();
          const { authStatus } = response;
          if (authStatus === authStatuses.COMPLETE) {
            // The user is properly authenticated => Navigate to the Account page
            location.href = "/account";
          } else {
            throw new Error("Two-factor authentication failed");
          }
        } catch (e) {
          // Alert the user that something went wrong
          alert(`Two-factor authentication failed. ${e}`);
        }
      });
    </script>
  </body>
</html>

Sử dụng tính năng xác thực hai yếu tố

Bây giờ, bạn đã sẵn sàng thêm bước xác thực yếu tố thứ hai.

Giờ đây, bạn cần thêm bước này từ index.html cho những người dùng đã định cấu hình tính năng xác thực hai yếu tố.

322a5c49d865a0d8.png

Trong index.html, bên dưới location.href = "/account";, thêm mã điều hướng người dùng đến trang xác thực yếu tố thứ hai nếu họ đã thiết lập 2FA.

Trong lớp học lập trình này, việc tạo thông tin đăng nhập sẽ tự động chọn cho phép người dùng xác thực hai yếu tố.

Xin lưu ý rằng server.js cũng triển khai kiểm tra phiên phía máy chủ. Điều này đảm bảo rằng chỉ những người dùng đã xác thực mới có thể truy cập vào account.html.

const { authStatus } = response;
if (authStatus === authStatuses.COMPLETE) {
  // The user is properly authenticated => navigate to account
  location.href = '/account';
} else if (authStatus === authStatuses.NEED_SECOND_FACTOR) {
  // Navigate to the two-factor-auth page because two-factor-auth is set up for this user
  location.href = '/second-factor';
}

Hãy thử xem! 👩🏻 💻

  • Đăng nhập bằng người dùng mới johndoe.
  • Đăng xuất.
  • Đăng nhập vào tài khoản của bạn dưới dạng johndoe; bạn chỉ cần nhập mật khẩu.
  • Tạo thông tin đăng nhập. Điều này sẽ có nghĩa là bạn đã kích hoạt tính năng xác thực hai yếu tố thành johndoe.
  • Đăng xuất.
  • Chèn tên người dùng johndoe và mật khẩu của bạn.
  • Xem cách bạn đang tự động chuyển đến trang xác thực yếu tố thứ hai.
  • (Thử truy cập trang Tài khoản tại /account; lưu ý cách bạn chuyển hướng đến trang chỉ mục vì bạn chưa được xác thực hoàn toàn: bạn đang thiếu một yếu tố thứ hai)
  • Quay lại trang xác thực yếu tố thứ hai rồi nhấp vào Sử dụng khóa bảo mật để xác thực các yếu tố thứ hai.
  • Bạn hiện đã đăng nhập và sẽ thấy trang Tài khoản!

8. Giúp thông tin đăng nhập dễ sử dụng hơn

Bạn đã hoàn tất chức năng cơ bản của xác thực hai yếu tố bằng khóa bảo mật 🚀

Nhưng... Bạn có nhận thấy không?

Hiện tại, danh sách thông tin đăng nhập của chúng tôi không được thuận tiện lắm: mã nhận dạng và khóa công khai là các chuỗi dài, không hữu ích khi quản lý thông tin đăng nhập! Con người không quá giỏi với chuỗi và số dài 🤖

Vì vậy, hãy để cải thiện điều này và thêm chức năng đặt tên, đổi tên thông tin xác thực bằng các chuỗi mà con người có thể đọc được.

Hãy xem Đổi tên thông tin đăng nhập

Để giúp bạn tiết kiệm thời gian khi triển khai hàm này mà không thực hiện bất kỳ hành động nào mang tính đột phá, chúng tôi đã thêm một hàm để đổi tên thông tin đăng nhập cho bạn trong mã dành cho người mới bắt đầu, trong auth.client.js:

async function renameCredential(credId, newName) {
  const params = new URLSearchParams({
    credId,
    name: newName
  });
  return _fetch(
    `/auth/credential?${params}`,
    "PUT"
  );
}

Đây là lệnh gọi cập nhật cơ sở dữ liệu thông thường: ứng dụng gửi yêu cầu PUT đến phần phụ trợ, kèm theo mã thông tin xác thực và tên mới cho thông tin đăng nhập đó.

Triển khai tên thông tin xác thực tùy chỉnh

Trong account.html, hãy chú ý đến hàm trống rename.

Thêm mã đó vào:

// Rename a credential
async function rename(credentialId) {
  // Let the user input a new name
  const newName = window.prompt(`Name this credential:`);
  // Rename only if the user didn't cancel AND didn't enter an empty name
  if (newName && newName.trim()) {
    try {
      // Make the backend call to rename the credential (the name is sanitized) server-side
      await renameCredential(credentialId, newName);
    } catch (e) {
      // Alert the user that something went wrong
      if (Array.isArray(e)) {
        alert(
          // `msg` not `message`, this is the key's name as per the express validator API
          `Renaming failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
        );
      } else {
        alert(`Renaming failed. ${e}`);
      }
    }
    // Refresh the credential list to display the new name
    await updateCredentialList();
  }
}

Bạn nên đặt tên thông tin xác thực sau khi đã tạo thành công thông tin xác thực. Vì vậy, hãy tạo một thông tin xác thực không có tên và khi tạo thành công, hãy đổi tên thông tin đăng nhập. Tuy nhiên, việc này sẽ dẫn đến 2 lệnh gọi phụ trợ.

Dùng hàm rename trong register() để cho phép người dùng đặt tên cho thông tin đăng nhập khi đăng ký:

async function register() {
  let user = {};
  try {
    const user = await registerCredential();
    // Get the latest credential's ID (newly created credential)
    const allUserCredentials = user.credentials;
    const newCredential = allUserCredentials[allUserCredentials.length - 1];
    // Rename it
    await rename(newCredential.credId);
  } catch (e) {
    // ...
  }
  // Refresh the credential list to display the new credential
  await updateCredentialList();
}

Xin lưu ý rằng hệ thống sẽ xác thực và làm sạch dữ liệu đầu vào của người dùng trong phần phụ trợ:

  check("name")
    .trim()
    .escape()

Hiển thị tên thông tin xác thực

Chuyển đến getCredentialHtmltemplates.js.

Lưu ý rằng đã có mã để hiển thị tên thông tin xác thực ở đầu thẻ thông tin xác thực:

// Register credential
const getCredentialHtml = (credential, removeEl, renameEl) => {
 const { name, credId, publicKey } = credential;
 return html`
    <div class="credential-card">
      <div class="credential-name">
        ${name
          ? html`
              ${name}
            `
          : html`
              <span class="unnamed">(Unnamed)</span>
            `}
      </div>
     // ...
    </div>
  `;
};

Hãy thử xem! 👩🏻 💻

  • Tạo thông tin đăng nhập.
  • Bạn sẽ được nhắc đặt tên.
  • Nhập tên mới và nhấp vào OK.
  • Thông tin đăng nhập hiện được đổi tên.
  • Lặp lại và kiểm tra để đảm bảo mọi thứ hoạt động trơn tru khi để trống trường tên.

Bật tính năng đổi tên thông tin xác thực

Người dùng có thể cần phải đổi tên thông tin xác thực – ví dụ: họ đang thêm khóa thứ hai và muốn đổi tên khóa đầu tiên để phân biệt các khóa này một cách chính xác hơn.

Trong account.html, hãy tìm hàm trống renameEl cho đến thời điểm này và thêm vào hàm mã sau:

// Rename a credential via HTML element
async function renameEl(el) {
  // Define the ID of the credential to update
  const credentialId = el.srcElement.dataset.credentialId;
  // Rename the credential
  await rename(credentialId);
  // Refresh the credential list to display the new name
  await updateCredentialList();
}

Bây giờ, trong getCredentialHtml của templates.js, trong div class="flex-end", hãy thêm mã sau, Mã này thêm nút Đổi tên vào mẫu thẻ thông tin xác thực; khi được nhấp, nút đó sẽ gọi hàm renameEl mà chúng ta vừa tạo:

const getCredentialHtml = (credential, removeEl, renameEl) => {
// ...
 <div class="flex-end">
  <button
    data-credential-id="${credId}"
    @click="${renameEl}"
    class="secondary right"
  >
   Rename
  </button>
 </div>
 // ...
  `;
};

Hãy thử xem! 👩🏻 💻

  • Nhấp vào Đổi tên.
  • Nhập tên mới khi được nhắc.
  • Nhấp vào OK.
  • Thông tin đăng nhập phải được đổi tên thành công và danh sách sẽ tự động cập nhật.
  • Việc tải lại trang sẽ vẫn hiển thị tên mới (điều này cho thấy rằng tên mới vẫn còn ở phía máy chủ).

Hiển thị ngày tạo thông tin xác thực

Ngày tạo không có trong thông tin xác thực được tạo qua navigator.credential.create().

Tuy nhiên, vì thông tin này có thể hữu ích cho người dùng để phân biệt giữa thông tin đăng nhập, nên chúng tôi đã điều chỉnh thư viện phía máy chủ trong mã dành cho người mới bắt đầu và thêm trường creationDate bằng với Date.now() khi lưu trữ thông tin đăng nhập mới.

Trong templates.js trong class="creation-date" div, hãy thêm thông tin sau để hiển thị thông tin về ngày tạo:

<div class="creation-date">
  <label>Created:</label>
  <div class="info">
    ${new Date(creationDate).toLocaleDateString()}
    ${new Date(creationDate).toLocaleTimeString()}
  </div>
</div>

9. Làm cho mã của bạn phù hợp với tương lai

Cho đến nay, chúng tôi chỉ yêu cầu người dùng đăng ký một trình xác thực chuyển vùng đơn giản, sau đó được dùng làm yếu tố thứ hai trong quá trình đăng nhập.

Một phương pháp nâng cao hơn là sử dụng một loại xác thực mạnh mẽ hơn: trình xác thực chuyển vùng xác minh người dùng (UVRA). Chỉ số tử ngoại có thể cung cấp hai yếu tố xác thực và khả năng chống lừa đảo trong quy trình đăng nhập một bước.

Lý tưởng nhất là bạn hỗ trợ cả hai phương pháp. Để thực hiện việc này, bạn cần tùy chỉnh trải nghiệm người dùng:

  • Nếu người dùng chỉ có trình xác thực chuyển vùng đơn giản (không phải xác minh cho người dùng), hãy cho phép họ sử dụng trình xác thực này để đạt được thao tác khởi động tài khoản chống lừa đảo, nhưng họ cũng phải nhập tên người dùng và mật khẩu. Đây là những gì lớp học lập trình của chúng tôi đã thực hiện.
  • Nếu một người dùng khác có trình xác thực chuyển vùng xác minh người dùng nâng cao hơn, họ sẽ có thể bỏ qua bước mật khẩu (và có thể là cả bước tên người dùng) trong khi khởi động tài khoản.

Tìm hiểu thêm về vấn đề này trong bài viết Khởi động tài khoản chống lừa đảo bằng cách đăng nhập bằng mật khẩu không bắt buộc.

Trong lớp học lập trình này, chúng tôi sẽ không thực sự tùy chỉnh trải nghiệm người dùng, nhưng chúng tôi sẽ thiết lập cơ sở mã của bạn để bạn có dữ liệu cần thiết nhằm tùy chỉnh trải nghiệm người dùng.

Bạn cần có hai thứ:

  • Thiết lập residentKey: preferred trong phần cài đặt phụ trợ. Quá trình này đã được thực hiện cho bạn.
  • Thiết lập một cách để tìm hiểu xem có thông tin đăng nhập có thể tìm được hay không (còn gọi là khóa thường trú) đã được tạo.

Để biết liệu một thông tin đăng nhập có thể khám phá có được tạo hay không, hãy làm như sau:

  • Truy vấn giá trị của credProps khi tạo thông tin xác thực (credProps: true).
  • Truy vấn giá trị của transports khi tạo thông tin xác thực. Điều này sẽ giúp bạn xác định được liệu nền tảng cơ bản có hỗ trợ chức năng UVRA hay không, ví dụ như đó có thực sự là điện thoại di động hay không.
  • Lưu trữ giá trị của credPropstransports trong phần phụ trợ. Quá trình này đã được thực hiện cho bạn trong mã dành cho người mới bắt đầu. Hãy xem auth.js nếu bạn tò mò.

Hãy lấy giá trị của credPropstransports, rồi gửi chúng đến phần phụ trợ. Trong auth.client.js, hãy sửa đổi registerCredential như sau:

  • Thêm một trường extensions khi gọi navigator.credentials.create
  • Thiết lập encodedCredential.transportsencodedCredential.credProps trước khi gửi thông tin xác thực đến phần phụ trợ để lưu trữ.

registerCredential sẽ có dạng như sau:

async function registerCredential() {
  // Fetch the credential creation options from the backend
  const credentialCreationOptionsFromServer = await _fetch(
    '/auth/credential-options',
    'POST'
  );
  // Decode the credential creation options
  const credentialCreationOptions = decodeServerOptions(
    credentialCreationOptionsFromServer
  );
  // Create a credential via the browser API; this will prompt the user
  const credential = await navigator.credentials.create({
    publicKey: {
      ...credentialCreationOptions,
      extensions: {
        credProps: true,
      },
    },
  });
  // Encode the newly created credential to send it to the backend
  const encodedCredential = encodeCredential(credential);
  // Set transports and credProps for more advanced user flows
  encodedCredential.transports = credential.response.getTransports();
  encodedCredential.credProps =
    credential.getClientExtensionResults().credProps;
  // Send the encoded credential to the backend for storage
  return await _fetch('/auth/credential', 'POST', encodedCredential);
}

10. Đảm bảo hỗ trợ nhiều trình duyệt

Hỗ trợ các trình duyệt không phải là Chromium

Trong hàm registerCredential của public/auth.client.js, chúng tôi sẽ gọi credential.response.getTransports() trên thông tin đăng nhập mới tạo để cuối cùng lưu thông tin này trong phần phụ trợ dưới dạng gợi ý cho máy chủ.

Tuy nhiên, getTransports() hiện không được triển khai trong tất cả các trình duyệt (không giống như getClientExtensionResults được hỗ trợ trên các trình duyệt): lệnh gọi getTransports() sẽ gửi một lỗi trong Firefox và Safari, điều này sẽ ngăn việc tạo thông tin xác thực trong các trình duyệt này.

Để đảm bảo mã của bạn sẽ chạy trong tất cả các trình duyệt chính, hãy thực hiện lệnh gọi encodedCredential.transports trong một điều kiện:

if (credential.response.getTransports) {
  encodedCredential.transports = credential.response.getTransports();
}

Xin lưu ý rằng trên máy chủ, transports được đặt thành transports || []. Trong Firefox và Safari, danh sách transports sẽ không phải là undefined mà là danh sách trống []. Việc này ngăn chặn lỗi.

Cảnh báo những người dùng sử dụng trình duyệt không hỗ trợ WebAuthn

1e9c1be837d66ce8.png

Mặc dù WebAuthn được hỗ trợ trong tất cả các trình duyệt chính, nhưng bạn nên hiển thị cảnh báo trong các trình duyệt không hỗ trợ WebAuthn.

Trong index.html, hãy quan sát sự hiện diện của div này:

<div id="warningbanner" class="invisible">
⚠️ Your browser doesn't support WebAuthn. Open this demo in Chrome, Edge, Firefox or Safari.
</div>

Trong tập lệnh cùng dòng của index.html, hãy thêm mã sau để hiển thị biểu ngữ trong các trình duyệt không hỗ trợ WebAuthn:

// Display a banner in browsers that don't support WebAuthn
if (!window.PublicKeyCredential) {
  document.querySelector('#warningbanner').classList.remove('invisible');
}

Trong một ứng dụng web thực, bạn sẽ làm điều gì đó phức tạp hơn và có một cơ chế dự phòng thích hợp cho các trình duyệt này—nhưng điều này sẽ cho bạn biết cách kiểm tra hỗ trợ WebAuthn.

11. Rất tốt!

✨Bạn đã hoàn tất!

Bạn đã triển khai xác thực hai yếu tố bằng khóa bảo mật.

Trong lớp học lập trình này, chúng tôi đã nói về những điều cơ bản. Nếu bạn muốn khám phá thêm WebAuthn cho 2FA, dưới đây là một số ý tưởng về những gì bạn có thể thử tiếp theo:

  • Thêm "Lần sử dụng gần nhất" thông tin vào thẻ thông tin xác thực. Đây là thông tin hữu ích cho người dùng để xác định xem khóa bảo mật nhất định có được sử dụng hay không — đặc biệt nếu họ đã đăng ký nhiều khóa.
  • Triển khai khả năng xử lý lỗi mạnh mẽ hơn và thông báo lỗi chính xác hơn.
  • Hãy xem xét auth.js và khám phá điều gì sẽ xảy ra khi bạn thay đổi một số authSettings, đặc biệt là khi sử dụng một khóa hỗ trợ quy trình xác minh người dùng.