Triển khai khoá truy cập bằng tính năng tự động điền biểu mẫu trong ứng dụng web

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

Việc sử dụng khoá truy cập thay vì mật khẩu là một cách hiệu quả để các trang web giúp tài khoản người dùng an toàn, đơn giản và dễ sử dụng hơn. Với khoá truy cập, người dùng có thể đăng nhập vào một trang web hoặc ứng dụng bằng cách sử dụng tính năng khoá màn hình của thiết bị, chẳng hạn như vân tay, khuôn mặt hoặc mã PIN của thiết bị. Khoá truy cập phải được tạo, liên kết với tài khoản người dùng và có khoá công khai tương ứng được lưu trữ trên một máy chủ trước khi người dùng có thể dùng để đăng nhập.

Trong lớp học lập trình này, bạn sẽ chuyển đổi một quy trình đăng nhập cơ bản bằng tên người dùng và mật khẩu dựa trên biểu mẫu thành một quy trình hỗ trợ khoá truy cập và bao gồm những nội dung sau:

  • Một nút tạo khoá truy cập sau khi người dùng đăng nhập.
  • Giao diện người dùng hiển thị danh sách các khoá truy cập đã đăng ký.
  • Biểu mẫu đăng nhập hiện có cho phép người dùng đăng nhập bằng khoá truy cập đã đăng ký thông qua tính năng tự động điền biểu mẫu.

Điều kiện tiên quyết

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

  • Cách tạo khoá truy cập.
  • Cách xác thực người dùng bằng khoá truy cập.
  • Cách cho phép biểu mẫu đề xuất khoá truy cập làm phương thức đăng nhập.

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

Trong lớp học lập trình này, bạn sẽ sao chép một ứng dụng minh hoạ chưa hoàn chỉnh từ GitHub rồi hoàn tất việc triển khai tính năng hỗ trợ khoá truy cập.

Nhân bản dự án

  1. Mở dự án trên GitHub.
  2. Nhân bản hoặc tải dự án xuống.

ac587c53b746785a.png

Chạy dự án

  1. Mở một cửa sổ dòng lệnh và cd start để thay đổi thư mục.
  2. Chạy npm install để cài đặt các phần phụ thuộc của dự án.
  3. Tạo và chạy dự án bằng npm run build && IS_LOCAL=1 npm run start.
  4. Mở http://localhost:8080/ trong trình duyệt.

Kiểm tra trạng thái ban đầu của trang web

  1. Trên trang web đó, hãy nhập một tên người dùng ngẫu nhiên rồi nhấp vào Tiếp theo.
  2. Nhập một mật khẩu ngẫu nhiên rồi nhấp vào Đăng nhập. Mật khẩu sẽ bị bỏ qua, nhưng bạn vẫn được xác thực và chuyển đến trang chủ.
  3. Nếu muốn thay đổi tên hiển thị, hãy thực hiện. Đó là tất cả những gì bạn có thể làm ở trạng thái ban đầu.
  4. Nhấp vào Đăng xuất.

Ở trạng thái này, người dùng phải nhập mật khẩu mỗi khi đăng nhập. Bạn thêm tính năng hỗ trợ khoá truy cập vào biểu mẫu này để người dùng có thể đăng nhập bằng phương thức khoá màn hình của thiết bị.

Để biết thêm thông tin về cách hoạt động của khoá truy cập, hãy xem bài viết Khoá truy cập hoạt động như thế nào?

3. Thêm khả năng tạo khoá truy cập

Để cho phép người dùng xác thực bằng khoá truy cập, bạn cần cho phép họ tạo và đăng ký khoá truy cập, đồng thời lưu trữ khoá công khai của khoá truy cập đó trên máy chủ.

9b84dbaec66afe9c.png

Bạn muốn cho phép tạo khoá truy cập sau khi người dùng đăng nhập bằng mật khẩu và thêm một giao diện người dùng cho phép người dùng tạo khoá truy cập và xem danh sách tất cả các khoá truy cập đã đăng ký trên trang /home. Trong phần tiếp theo, bạn sẽ tạo một hàm để tạo và đăng ký khoá truy cập.

Tạo hàm registerCredential()

  1. Trong trình soạn thảo mã mà bạn chọn, hãy mở thư mục start.
  2. Chuyển đến tệp public/client.js rồi di chuyển đến cuối tệp.
  3. Sau phần nhận xét có liên quan, hãy thêm hàm registerCredential() sau:

public/client.js

// TODO: Add an ability to create a passkey: Create the registerCredential() function.
export async function registerCredential() {

  // TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.

  // TODO: Add an ability to create a passkey: Create a credential.

  // TODO: Add an ability to create a passkey: Register the credential to the server endpoint.

};

Hàm này tạo và đăng ký một khoá truy cập trên máy chủ.

Nhận thử thách và các lựa chọn khác từ điểm cuối của máy chủ

Trước khi tạo khoá truy cập, bạn cần yêu cầu các tham số cần truyền vào WebAuthn từ máy chủ, trong đó có một thử thách. WebAuthn là một API trình duyệt cho phép người dùng tạo khoá truy cập và xác thực người dùng bằng khoá truy cập. May mắn là bạn đã có một điểm cuối của máy chủ phản hồi bằng những tham số như vậy trong lớp học lập trình này.

  • Để lấy thử thách và các lựa chọn khác từ điểm cuối của máy chủ, hãy thêm mã sau vào phần nội dung của hàm registerCredential() sau phần nhận xét có liên quan:

public/client.js

// TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.
const _options = await _fetch('/auth/registerRequest');

Đoạn mã sau đây chứa các lựa chọn mẫu mà bạn sẽ nhận được từ máy chủ:

{
  challenge: *****,
  rp: {
    id: "example.com",
  },
  user: {
    id: *****,
    name: "john78",
    displayName: "John",
  },  
  pubKeyCredParams: [{
    alg: -7, type: "public-key"
  },{
    alg: -257, type: "public-key"
  }],
  excludeCredentials: [{
    id: *****,
    type: 'public-key',
    transports: ['internal', 'hybrid'],
  }],
  authenticatorSelection: {
    authenticatorAttachment: "platform",
    requireResidentKey: true,
  }
}

Giao thức giữa máy chủ và ứng dụng không thuộc quy cách WebAuthn. Tuy nhiên, máy chủ của lớp học lập trình này được thiết kế để trả về một JSON giống nhất có thể với từ điển PublicKeyCredentialCreationOptions được truyền tới API navigator.credentials.create() WebAuthn.

Bảng sau đây không đầy đủ nhưng có chứa các tham số quan trọng trong từ điển PublicKeyCredentialCreationOptions:

Tham số

Mô tả

challenge

Một thử thách do máy chủ tạo trong một đối tượng ArrayBuffer cho quá trình đăng ký này. Đây là chuỗi bắt buộc nhưng sẽ không được dùng trong quá trình đăng ký, trừ phi để thực hiện việc chứng thực – một chủ đề nâng cao không được đề cập trong lớp học lập trình này.

user.id

Mã nhận dạng duy nhất của người dùng. Giá trị này phải là một đối tượng ArrayBuffer không chứa thông tin nhận dạng cá nhân, chẳng hạn như địa chỉ email hoặc tên người dùng. Có thể là một giá trị 16 byte ngẫu nhiên được tạo cho mỗi tài khoản.

user.name

Trường này phải chứa một giá trị nhận dạng duy nhất mà người dùng có thể nhận ra đối với tài khoản của họ, chẳng hạn như địa chỉ email hoặc tên người dùng. Thông tin này sẽ xuất hiện trong bộ chọn tài khoản. (Nếu bạn dùng tên người dùng, hãy sử dụng chính giá trị như khi xác thực mật khẩu.)

user.displayName

Trường này là một tên tài khoản không bắt buộc và thân thiện với người dùng. Tên này không cần phải là duy nhất và có thể là tên mà người dùng chọn. Nếu trang web của bạn không có giá trị phù hợp để thêm vào đây, hãy truyền một chuỗi trống. Thông tin này có thể xuất hiện trên bộ chọn tài khoản, tuỳ thuộc vào trình duyệt.

rp.id

Mã nhận dạng bên tin cậy (RP) là một miền. Một trang web có thể chỉ định miền của trang web đó hoặc một hậu tố có thể đăng ký. Ví dụ: nếu nguồn gốc của RP là https://login.example.com:1337, thì mã nhận dạng RP có thể là login.example.com hoặc example.com. Nếu mã nhận dạng RP được chỉ định là example.com, thì người dùng có thể xác thực trên login.example.com hoặc trên bất kỳ miền con nào khác của example.com.

pubKeyCredParams

Trường này chỉ định các thuật toán khoá công khai được RP hỗ trợ. Bạn nên đặt giá trị này thành [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}]. Điều này chỉ định khả năng hỗ trợ ECDSA bằng P-256 và RSA PKCS#1, đồng thời hỗ trợ những thuật toán này sẽ giúp bạn có được phạm vi hỗ trợ đầy đủ.

excludeCredentials

Cung cấp danh sách mã thông tin xác thực đã đăng ký để ngăn việc đăng ký cùng một thiết bị hai lần. Nếu được cung cấp, thành phần transports sẽ chứa kết quả của lệnh gọi hàm getTransports() trong quá trình đăng ký từng thông tin đăng nhập. Tìm hiểu thêm trong tài liệu của chúng tôi về cách ngăn việc tạo khoá truy cập mới nếu đã có một khoá truy cập.

authenticatorSelection.authenticatorAttachment

Đặt thành giá trị "platform". Điều này cho biết rằng bạn muốn một trình xác thực được nhúng vào thiết bị nền tảng để người dùng không được nhắc về việc gắn một thứ gì đó như khoá bảo mật USB.

authenticatorSelection.requireResidentKey

Đặt thành giá trị Boolean true. Bạn có thể sử dụng một thông tin đăng nhập có thể phát hiện (khoá cư trú) mà không cần máy chủ cung cấp mã nhận dạng của thông tin đăng nhập và do đó, thông tin đăng nhập này tương thích với tính năng tự động điền. Tìm hiểu thêm trong bài viết nghiên cứu chuyên sâu về thông tin đăng nhập có thể khám phá.

authenticatorSelection.userVerification

Đặt thành giá trị "preferred" hoặc bỏ qua vì đây là giá trị mặc định. Điều này cho biết liệu quy trình xác minh người dùng sử dụng khoá màn hình của thiết bị là "required", "preferred" hay "discouraged". Khi bạn đặt thành giá trị "preferred", hệ thống sẽ yêu cầu người dùng xác minh khi thiết bị có thể. Tìm hiểu thêm trong bài viết userVerification nghiên cứu chuyên sâu.

Tạo thông tin đăng nhập

  1. Trong phần nội dung của hàm registerCredential() sau nhận xét có liên quan, hãy chuyển đổi một số tham số được mã hoá bằng Base64URL trở lại thành nhị phân, cụ thể là các chuỗi user.idchallenge, cũng như các thực thể của chuỗi id có trong mảng excludeCredentials. Bạn có thể thực hiện việc này bằng hàm PublicKeyCredential.parseCreationOptionsFromJSON():

public/client.js

// TODO: Add an ability to create a passkey: Create a credential.

// Deserialize and decode the `PublicKeyCredential.parseCreationOptionsFromJSON()`.
const options = PublicKeyCredential.parseCreationOptionsFromJSON(_options);
  1. Ở dòng tiếp theo, hãy đặt authenticatorSelection.authenticatorAttachment thành "platform"authenticatorSelection.requireResidentKey thành true. Điều này chỉ cho phép sử dụng trình xác thực nền tảng (chính thiết bị) có khả năng về thông tin đăng nhập có thể phát hiện.

public/client.js

// Use platform authenticator and discoverable credential.
options.authenticatorSelection = {
  authenticatorAttachment: 'platform',
  requireResidentKey: true
}
  1. Trên dòng tiếp theo, hãy gọi phương thức navigator.credentials.create() để tạo thông tin đăng nhập.

public/client.js

// Invoke the WebAuthn create() method.
const cred = await navigator.credentials.create({
  publicKey: options,
});

Với lệnh gọi này, trình duyệt sẽ cố gắng xác minh danh tính của người dùng bằng phương thức khoá màn hình của thiết bị.

Đăng ký thông tin đăng nhập vào điểm cuối máy chủ

Sau khi người dùng xác minh danh tính, một khoá truy cập sẽ được tạo và lưu trữ. Trang web sẽ nhận được một đối tượng thông tin đăng nhập chứa khoá công khai mà bạn có thể gửi đến máy chủ để đăng ký khoá truy cập.

Đoạn mã sau đây chứa một đối tượng thông tin đăng nhập mẫu:

{
  "id": *****,
  "rawId": *****,
  "type": "public-key",
  "response": {
    "clientDataJSON": *****,
    "attestationObject": *****,
    "transports": ["internal", "hybrid"]
  },
  "authenticatorAttachment": "platform"
}

Bảng sau đây không đầy đủ nhưng có chứa các tham số quan trọng trong đối tượng PublicKeyCredential:

Tham số

Mô tả

id

Mã nhận dạng được mã hoá Base64URL của khoá truy cập đã tạo. Trong quá trình xác thực thì mã nhận dạng này sẽ giúp trình duyệt xác định xem thiết bị có khoá truy cập phù hợp hay không. Giá trị này phải được lưu trữ trong cơ sở dữ liệu ở máy chủ phụ trợ.

rawId

Một phiên bản đối tượng ArrayBuffer của mã nhận dạng thông tin đăng nhập (credential ID).

response.clientDataJSON

Một đối tượng ArrayBuffer mã hoá dữ liệu ứng dụng khách.

response.attestationObject

Một đối tượng chứng thực mã hoá bằng ArrayBuffer. Đối tượng này có chứa thông tin quan trọng, chẳng hạn như mã nhận dạng RP, cờ và khoá công khai.

response.transports

Danh sách các phương thức truyền dữ liệu mà thiết bị hỗ trợ: "internal" có nghĩa là thiết bị hỗ trợ khoá truy cập. "hybrid" có nghĩa là thiết bị này cũng hỗ trợ xác thực trên một thiết bị khác.

authenticatorAttachment

Trả về "platform" khi thông tin đăng nhập này được tạo trên một thiết bị có hỗ trợ khoá truy cập.

Để gửi đối tượng thông tin xác thực đến máy chủ, hãy làm theo các bước sau:

  1. Mã hoá các tham số nhị phân của thông tin đăng nhập dưới dạng Base64URL để có thể gửi đến máy chủ dưới dạng một chuỗi. Bạn có thể sử dụng .toJSON() để thực hiện việc này:

public/client.js

// TODO: Add an ability to create a passkey: Register the credential to the server endpoint.

// Encode and serialize the `PublicKeyCredential`.
const credential = JSON.stringify(cred);
  1. Trên dòng tiếp theo, hãy gửi đối tượng đến máy chủ:

public/client.js

return await _fetch('/auth/registerResponse', credential);

Khi bạn chạy chương trình, máy chủ sẽ trả về HTTP code 200, cho biết rằng thông tin đăng nhập đã được đăng ký.

Bây giờ, bạn đã có hàm registerCredential() hoàn chỉnh!

Xem mã giải pháp cho phần này

public/client.js

// TODO: Add an ability to create a passkey: Create the registerCredential() function.

export async function registerCredential() {

  // TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.

  const _options = await _fetch('/auth/registerRequest');

  // TODO: Add an ability to create a passkey: Create a credential.

  // Deserialize and decode the `PublicKeyCredential.parseCreationOptionsFromJSON()`.
  const options = PublicKeyCredential.parseCreationOptionsFromJSON(_options);

  // Use platform authenticator and discoverable credential.
  options.authenticatorSelection = {
    authenticatorAttachment: 'platform',
    requireResidentKey: true
  }

  // Invoke the WebAuthn create() method.
  const cred = await navigator.credentials.create({
    publicKey: options,
  });

  // TODO: Add an ability to create a passkey: Register the credential to the server endpoint.

  // Encode and serialize the `PublicKeyCredential`.
  const credential = JSON.stringify(cred);

  return await _fetch('/auth/registerResponse', credential);
};

4. Tạo giao diện người dùng để đăng ký và quản lý thông tin đăng nhập bằng khoá truy cập

Bây giờ, khi có hàm registerCredential(), bạn cần một nút để gọi hàm này. Ngoài ra, bạn cần hiển thị danh sách các khoá truy cập đã đăng ký.

bfa4e7cdda47669e.png

Thêm HTML giữ chỗ

  1. Trong trình chỉnh sửa, hãy chuyển đến tệp views/home.html.
  2. Sau nhận xét có liên quan, hãy thêm một phần giữ chỗ giao diện người dùng hiển thị nút đăng ký khoá truy cập và danh sách khoá truy cập:

views/home.html

​​<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
  <h3>Your registered passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mdui-button id="create-passkey" class="hidden" icon="fingerprint" type="button">Create a passkey</mdui-button>

Phần tử div#list là phần giữ chỗ cho danh sách.

Kiểm tra xem có hỗ trợ khoá truy cập hay không

Để chỉ cho người dùng có thiết bị hỗ trợ khoá truy cập thấy lựa chọn tạo khoá truy cập, trước tiên, bạn cần kiểm tra xem WebAuthn có dùng được hay không. Nếu có, bạn cần xoá lớp hidden để hiện nút Tạo khoá truy cập.

Để kiểm tra xem một môi trường có hỗ trợ khoá truy cập hay không, hãy làm theo các bước sau:

  1. Ở cuối tệp views/home.html sau nhận xét có liên quan, hãy viết một điều kiện sẽ thực thi nếu window.PublicKeyCredential, PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailablePublicKeyCredential.isConditionalMediationAvailabletrue.

views/home.html

// TODO: Add an ability to create a passkey: Check for passkey support.
const createPasskey = $('#create-passkey');
// Feature detections
if (window.PublicKeyCredential &&
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  1. Trong phần nội dung của điều kiện, hãy kiểm tra xem thiết bị có thể tạo khoá truy cập hay không, sau đó kiểm tra xem khoá truy cập có thể được đề xuất trong tính năng tự động điền biểu mẫu hay không.

views/home.html

try {
    const capabilities = await PublicKeyCredential.getClientCapabilities();
    // Is conditional UI available in this browser?
    if (capabilities.conditionalGet === true &&
        capabilities.passkeyPlatformAuthenticator === true) {
  1. Nếu đáp ứng tất cả các điều kiện, hãy hiện nút tạo khoá truy cập. Nếu không, hãy hiện một thông báo cảnh báo.

views/home.html

      createPasskey.classList.remove('hidden');
    } else {

      // If conditional UI isn't available, show a message.
      $('#message').innerText = 'This device does not support passkeys.';
    }
  } catch (e) {
    console.error(e);
  }
} else {

  // If WebAuthn isn't available, show a message.
  $('#message').innerText = 'This device does not support passkeys.';
}

Kết xuất các khoá truy cập đã đăng ký trong một danh sách

  1. Xác định một hàm renderCredentials() để tìm nạp các khoá truy cập đã đăng ký từ máy chủ và hiển thị các khoá đó trong một danh sách. May mắn thay, bạn đã có điểm cuối máy chủ /auth/getKeys để tìm nạp khoá truy cập đã đăng ký cho người dùng đã đăng nhập.

views/home.html

// TODO: Add an ability to create a passkey: Render registered passkeys in a list.
async function renderCredentials() {
  const res = await _fetch('/auth/getKeys');
  const list = $('#list');
  const creds = res.length > 0 ? html`
    <mdui-list>
      ${res.map(cred => html`
        <mdui-list-item>
          ${cred.name || 'Unnamed'}
          <mdui-button-icon data-cred-id="${cred.id}" data-name="${cred.name || 'Unnamed'}" @click="${rename}" icon="edit" slot="end-icon"></mdui-button-icon>
          <mdui-button-icon data-cred-id="${cred.id}" @click="${remove}" icon="delete" slot="end-icon"></mdui-button-icon>
        </mdui-list-item>`)}
    </mdui-list>` : html`
    <mdui-list>
      <mdui-list-item>No credentials found.</mdui-list-item>
    </mdui-list>`;
  render(creds, list);
};
  1. Trên dòng tiếp theo, hãy gọi hàm renderCredentials() để hiển thị khoá truy cập đã đăng ký ngay khi người dùng truy cập vào trang /home dưới dạng một quy trình khởi tạo.

views/home.html

renderCredentials();

Tạo và đăng ký khoá truy cập

Để tạo và đăng ký khoá truy cập, bạn cần gọi hàm registerCredential() mà bạn đã triển khai trước đó.

Để kích hoạt hàm registerCredential() khi bạn nhấp vào nút Tạo khoá truy cập, hãy làm theo các bước sau:

  1. Trong tệp sau HTML giữ chỗ, hãy tìm câu lệnh import sau đây:

views/home.html

import { 
  $, 
  _fetch, 
  loading, 
  updateCredential, 
  unregisterCredential, 
} from '/client.js';
  1. Ở cuối phần thân của câu lệnh import, hãy thêm hàm registerCredential().

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
import {
  $,
  _fetch,
  loading,
  updateCredential,
  unregisterCredential,
  registerCredential
} from '/client.js';
  1. Ở cuối tệp sau nhận xét có liên quan, hãy xác định một hàm register() gọi hàm registerCredential() và giao diện người dùng đang tải, đồng thời gọi renderCredentials() sau khi đăng ký. Điều này làm rõ rằng trình duyệt sẽ tạo một khoá truy cập và hiện thông báo lỗi khi có vấn đề xảy ra.

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
async function register() {
  try {

    // Start the loading UI.
    loading.start();

    // Start creating a passkey.
    await registerCredential();

    // Stop the loading UI.
    loading.stop();

    // Render the updated passkey list.
    renderCredentials();
  1. Trong phần nội dung của hàm register(), hãy bắt các ngoại lệ. Phương thức navigator.credentials.create() sẽ gửi lỗi InvalidStateError khi khoá đăng nhập đã tồn tại trên thiết bị. Việc này được kiểm tra bằng mảng excludeCredentials. Trong trường hợp này, bạn sẽ cho người dùng thấy một thông báo phù hợp. Thao tác này cũng sẽ gửi lỗi NotAllowedError khi người dùng huỷ hộp thoại xác thực. Trong trường hợp này, bạn sẽ bỏ qua nó một cách âm thầm.

views/home.html

  } catch (e) {

    // Stop the loading UI.
    loading.stop();

    // An InvalidStateError indicates that a passkey already exists on the device.
    if (e.name === 'InvalidStateError') {
      alert('A passkey already exists for this device.');

    // A NotAllowedError indicates that the user canceled the operation.
    } else if (e.name === 'NotAllowedError') {
      Return;

    // Show other errors in an alert.
    } else {
      alert(e.message);
      console.error(e);
    }
  }
};
  1. Trên dòng sau hàm register(), hãy đính kèm hàm register() vào một sự kiện click cho nút Tạo khoá truy cập.

views/home.html

createPasskey.addEventListener('click', register);

Xem mã giải pháp cho phần này

views/home.html

<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
  <h3>Your registered passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mdui-button id="create-passkey" icon="fingerprint" type="button">Create a passkey</mdui-button>

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
import { 
  $, 
  _fetch, 
  loading, 
  updateCredential, 
  unregisterCredential, 
  registerCredential 
} from '/client.js';

views/home.html

// TODO: Add an ability to create a passkey: Check for passkey support.
const createPasskey = $('#create-passkey');

// Is WebAuthn available in this browser?
if (window.PublicKeyCredential &&
  PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
  PublicKeyCredential.isConditionalMediationAvailable) {
  try {
    const capabilities = await PublicKeyCredential.getClientCapabilities();
    // Is conditional UI available in this browser?
    if (capabilities.conditionalGet === true &&
      capabilities.passkeyPlatformAuthenticator === true) {
      // If conditional UI is available, reveal the Create a passkey button.
      createPasskey.classList.remove('hidden');
    } else {
      // If conditional UI isn't available, show a message.
      $('#message').innerText = 'This device does not support passkeys.';
    }
  } catch (e) {
    console.error(e);
  }
} else {
  // If WebAuthn isn't available, show a message.
  $('#message').innerText = 'This device does not support passkeys.';
}

// TODO: Add an ability to create a passkey: Render registered passkeys in a list.

async function renderCredentials() {
  const res = await _fetch('/auth/getKeys');
  const list = $('#list');
  const creds = html`${res.length > 0 ? html`
    <mdui-list>
      ${res.map(cred => html`
        <mdui-list-item>
          ${cred.name || 'Unnamed'}
          <mdui-button-icon data-cred-id="${cred.id}" data-name="${cred.name || 'Unnamed'}" @click="${rename}" icon="edit" slot="end-icon"></mdui-button-icon>
          <mdui-button-icon data-cred-id="${cred.id}" @click="${remove}" icon="delete" slot="end-icon"></mdui-button-icon>
        </mdui-list-item>`)}
    </mdui-list>` : html`
    <mdui-list>
      <mdui-list-item>No credentials found.</mdui-list-item>
    </mdui-list>`}`;
  render(creds, list);
};

renderCredentials();

// TODO: Add an ability to create a passkey: Create and register a passkey.

async function register() {
  try {
    // Start the loading UI.
    loading.start();
    // Start creating a passkey.
    await registerCredential();
    // Stop the loading UI.
    loading.stop();
    // Render the updated passkey list.
    renderCredentials();
  } catch (e) {
    // Stop the loading UI.
    loading.stop();
    // An InvalidStateError indicates that a passkey already exists on the device.
    if (e.name === 'InvalidStateError') {
      alert('A passkey already exists for this device.');
      // A NotAllowedError indicates the user canceled the operation.
    } else if (e.name === 'NotAllowedError') {
      return;
      // Show other errors in an alert.
    } else {
      alert(e.message);
      console.error(e);
    }
  }
};

createPasskey.addEventListener('click', register);

Dùng thử

Nếu đã làm theo tất cả các bước cho đến nay, bạn đã triển khai khả năng tạo, đăng ký và hiển thị khoá truy cập trên trang web!

Để dùng thử, hãy làm theo các bước sau:

  1. Trên trang web đó, hãy đăng nhập bằng một tên người dùng và mật khẩu ngẫu nhiên.
  2. Nhấp vào Tạo khoá truy cập.
  3. Xác minh danh tính bằng phương thức khoá màn hình của thiết bị.
  4. Xác nhận rằng khoá truy cập đã được đăng ký và xuất hiện trong phần Khoá truy cập đã đăng ký của bạn trên trang web.

Khoá truy cập đã đăng ký xuất hiện trên trang /home.

Đổi tên và xoá khoá truy cập đã đăng ký

Bạn có thể đổi tên hoặc xoá khoá truy cập đã đăng ký trong danh sách. Bạn có thể kiểm tra cách hoạt động của các thành phần này trong mã nguồn đi kèm với lớp học lập trình.

Trong Chrome, bạn có thể xoá khoá truy cập đã đăng ký trên trang chrome://settings/passkeys trên máy tính hoặc trong trình quản lý mật khẩu trong phần cài đặt trên Android.

Để biết thông tin về cách đổi tên và xoá khoá truy cập đã đăng ký trên các nền tảng khác, hãy xem các trang hỗ trợ tương ứng của những nền tảng đó.

5. Thêm tính năng xác thực bằng khoá truy cập

Giờ đây, người dùng có thể tạo và đăng ký khoá truy cập, đồng thời sẵn sàng sử dụng khoá truy cập này như một cách để xác thực trang web của bạn một cách an toàn. Bây giờ, bạn cần thêm khả năng xác thực bằng khoá truy cập vào trang web của mình.

Tạo hàm authenticate()

  • Trong tệp public/client.js sau nhận xét có liên quan, hãy tạo một hàm có tên là authenticate() để xác minh người dùng cục bộ, sau đó xác minh với máy chủ:

public/client.js

// TODO: Add an ability to authenticate with a passkey: Create the authenticate() function.
export async function authenticate() {

  // TODO: Add an ability to authenticate with a passkey: Obtain the challenge and other options from the server endpoint.

  // TODO: Add an ability to authenticate with a passkey: Locally verify the user and get a credential.

  // TODO: Add an ability to authenticate with a passkey: Verify the credential.

};

Nhận thử thách và các lựa chọn khác từ điểm cuối máy chủ

Trước khi yêu cầu người dùng xác thực, bạn cần yêu cầu các tham số cần truyền vào WebAuthn từ máy chủ, trong đó có một thử thách (challenge).

  • Trong phần thân của hàm authenticate() sau nhận xét có liên quan, hãy gọi hàm _fetch() để gửi yêu cầu POST đến máy chủ:

public/client.js

// TODO: Add an ability to authenticate with a passkey: Obtain the challenge and other options from the server endpoint.

// Base64URL decode the challenge.
const options = PublicKeyCredential.parseRequestOptionsFromJSON(_options);

Máy chủ của lớp học lập trình này được thiết kế để trả về JSON giống nhất có thể với từ điển PublicKeyCredentialRequestOptions được truyền tới API navigator.credentials.get() WebAuthn. Đoạn mã sau đây chứa các lựa chọn ví dụ mà bạn sẽ nhận được:

{
  "challenge": *****,
  "rpId": "localhost",
  "allowCredentials": []
}

Bảng sau đây không đầy đủ nhưng có chứa các tham số quan trọng trong từ điển PublicKeyCredentialRequestOptions:

Tham số

Mô tả

challenge

Một thử thách do máy chủ tạo trong một đối tượng ArrayBuffer. Điều này là cần thiết để ngăn chặn các cuộc tấn công phát lại (replay attack). Đừng bao giờ chấp nhận cùng một thử thách hai lần trong một phản hồi.

rpId

Mã nhận dạng RP là một miền. Một trang web có thể chỉ định miền của nó hoặc chỉ định một hậu tố có thể đăng ký. Giá trị này phải khớp với tham số rp.id được dùng khi tạo khoá truy cập.

allowCredentials

Thuộc tính này được dùng để tìm những trình xác thực đủ điều kiện cho quy trình xác thực này. Truyền một mảng trống hoặc để trống để trình duyệt hiển thị bộ chọn tài khoản. Tìm hiểu thêm về cách allowCredentials hoạt động.

userVerification

Đặt thành giá trị "preferred" hoặc bỏ qua vì đây là giá trị mặc định. Thông tin này cho biết trạng thái xác minh người dùng bằng phương thức khoá màn hình của thiết bị là "required", "preferred" hay "discouraged". Khi bạn đặt thành giá trị "preferred", hệ thống sẽ yêu cầu người dùng xác minh khi thiết bị có thể. Tìm hiểu thêm về hành vi xác minh người dùng.

Xác minh người dùng trên thiết bị và nhận thông tin đăng nhập

  1. Trong phần nội dung của hàm authenticate() sau bình luận có liên quan, hãy chuyển đổi tham số challenge trở lại thành dạng nhị phân:

public/client.js

// TODO: Add an ability to authenticate with a passkey: Locally verify the user and get a credential.
// Base64URL decode the challenge.
options.challenge = base64url.decode(options.challenge);
  1. Truyền một mảng trống vào tham số allowCredentials để mở bộ chọn tài khoản khi người dùng xác thực:

public/client.js

// An empty allowCredentials array invokes an account selector by discoverable credentials.
options.allowCredentials = [];

Bộ chọn tài khoản sử dụng thông tin của người dùng được lưu trữ bằng khoá truy cập.

  1. Gọi phương thức navigator.credentials.get() cùng với một lựa chọn mediation: 'conditional':

public/client.js

// Invoke the WebAuthn get() method.
const cred = await navigator.credentials.get({
  publicKey: options,

  // Request a conditional UI.
  mediation: 'conditional'
});

Lựa chọn này hướng dẫn trình duyệt đề xuất khoá truy cập có điều kiện trong quá trình tự động điền biểu mẫu.

Xác minh thông tin đăng nhập

Sau khi người dùng xác minh danh tính của họ trên thiết bị, bạn sẽ nhận được một đối tượng thông tin đăng nhập chứa chữ ký mà bạn có thể xác minh trên máy chủ.

Đoạn mã sau đây chứa một đối tượng PublicKeyCredential mẫu:

{
  "id": *****,
  "rawId": *****,
  "type": "public-key",
  "response": {
    "clientDataJSON": *****,
    "authenticatorData": *****,
    "signature": *****,
    "userHandle": *****
  },
  authenticatorAttachment: "platform"
}

Bảng sau đây không đầy đủ nhưng có chứa các tham số quan trọng trong đối tượng PublicKeyCredential:

Tham số

Mô tả

id

Mã nhận dạng được mã hoá Base64URL của thông tin đăng nhập bằng khoá truy cập đã được xác thực.

rawId

Một phiên bản đối tượng ArrayBuffer của mã nhận dạng thông tin đăng nhập (credential ID).

response.clientDataJSON

Một đối tượng ArrayBuffer của dữ liệu ứng dụng khách. Trường này chứa thông tin, chẳng hạn như thử thách và nguồn gốc mà máy chủ RP cần xác minh.

response.authenticatorData

Một đối tượng ArrayBuffer của dữ liệu trình xác thực. Trường này chứa thông tin như mã nhận dạng RP.

response.signature

Một đối tượng ArrayBuffer của chữ ký. Giá trị này là cốt lõi của thông tin đăng nhập và phải được xác minh trên máy chủ.

response.userHandle

Một đối tượng ArrayBuffer chứa mã người dùng được thiết lập tại thời điểm tạo. Giá trị này có thể được sử dụng thay cho mã nhận dạng thông tin đăng nhập nếu máy chủ cần chọn giá trị mã nhận dạng để sử dụng hoặc nếu máy chủ phụ trợ muốn tránh tạo chỉ mục cho mã nhận dạng thông tin đăng nhập.

authenticatorAttachment

Trả về một chuỗi "platform" khi thông tin đăng nhập này đến từ thiết bị cục bộ. Nếu không, hàm này sẽ trả về một chuỗi "cross-platform", đặc biệt là khi người dùng sử dụng điện thoại để đăng nhập. Nếu người dùng cần sử dụng điện thoại để đăng nhập, hãy nhắc họ tạo khoá truy cập trên thiết bị cục bộ.

Để gửi đối tượng thông tin xác thực đến máy chủ, hãy làm theo các bước sau:

  1. Trong phần nội dung của hàm authenticate() sau nhận xét có liên quan, hãy mã hoá các tham số nhị phân của thông tin đăng nhập để có thể gửi đến máy chủ dưới dạng một chuỗi. Bạn có thể sử dụng .toJSON() để thực hiện việc này:

public/client.js

// TODO: Add an ability to authenticate with a passkey: Verify the credential.
// Encode and serialize the `PublicKeyCredential`.
const credential = JSON.stringify(cred);
  1. Gửi đối tượng đến máy chủ:

public/client.js

return await _fetch(`/auth/signinResponse`, credential);

Khi bạn chạy chương trình, máy chủ sẽ trả về HTTP code 200, cho biết rằng thông tin đăng nhập đã được xác minh.

Giờ đây, bạn đã có hàm authentication() hoàn chỉnh!

Xem mã giải pháp cho phần này

public/client.js

// TODO: Add an ability to authenticate with a passkey: Create the authenticate() function.
export async function authenticate() {

  // TODO: Add an ability to authenticate with a passkey: Obtain the 
  challenge and other options from the server endpoint.
  const options = await _fetch('/auth/signinRequest');

  // TODO: Add an ability to authenticate with a passkey: Locally verify 
  the user and get a credential.
  // Base64URL decode the challenge.
  options.challenge = base64url.decode(options.challenge);

  // The empty allowCredentials array invokes an account selector 
  by discoverable credentials.
  options.allowCredentials = [];

  // Invoke the WebAuthn get() function.
  const cred = await navigator.credentials.get({
    publicKey: options,

    // Request a conditional UI.
    mediation: 'conditional'
  });

  // TODO: Add an ability to authenticate with a passkey: Verify the credential.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;

  // Base64URL encode some values.
  const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
  const authenticatorData = 
  base64url.encode(cred.response.authenticatorData);
  const signature = base64url.encode(cred.response.signature);
  const userHandle = base64url.encode(cred.response.userHandle);

  credential.response = {
    clientDataJSON,
    authenticatorData,
    signature,
    userHandle,
  };

  return await _fetch(`/auth/signinResponse`, credential);
};

6. Thêm khoá truy cập vào tính năng tự động điền của trình duyệt

Khi người dùng quay lại, bạn muốn họ đăng nhập một cách dễ dàng và an toàn nhất có thể. Nếu bạn thêm nút Đăng nhập bằng khoá truy cập vào trang đăng nhập, thì người dùng có thể nhấn nút này, chọn một khoá truy cập trong bộ chọn tài khoản của trình duyệt và sử dụng tính năng khoá màn hình để xác minh danh tính.

Tuy nhiên, quá trình chuyển đổi từ mật khẩu sang khoá truy cập sẽ không diễn ra cùng lúc cho tất cả người dùng. Điều này có nghĩa là bạn không thể loại bỏ mật khẩu cho đến khi tất cả người dùng chuyển sang khoá truy cập, vì vậy, bạn cần giữ lại biểu mẫu đăng nhập dựa trên mật khẩu cho đến lúc đó. Mặc dù vậy, nếu bạn để lại một biểu mẫu mật khẩu và một nút khoá truy cập, thì người dùng sẽ phải đưa ra một lựa chọn không cần thiết giữa việc sử dụng một trong hai để đăng nhập. Lý tưởng nhất là bạn nên có một quy trình đăng nhập đơn giản.

Đây là lúc giao diện người dùng có điều kiện phát huy tác dụng. Giao diện người dùng có điều kiện là một tính năng WebAuthn, trong đó bạn có thể tạo một trường nhập dữ liệu biểu mẫu để đề xuất khoá truy cập ngoài mật khẩu trong số các mục tự động điền. Nếu người dùng nhấn vào một khoá truy cập trong các đề xuất tự động điền, thì họ sẽ được yêu cầu sử dụng khoá màn hình của thiết bị để xác minh danh tính của mình trên thiết bị. Đây là một trải nghiệm liền mạch cho người dùng vì hành động của người dùng gần giống với hành động đăng nhập dựa trên mật khẩu.

d616744939063451.png

Bật giao diện người dùng có điều kiện

Để bật giao diện người dùng có điều kiện, bạn chỉ cần thêm mã thông báo webauthn vào thuộc tính autocomplete của một trường nhập dữ liệu. Với bộ mã thông báo này, bạn có thể gọi phương thức navigator.credentials.get() bằng chuỗi mediation: 'conditional' để kích hoạt có điều kiện giao diện người dùng phương thức khoá màn hình.

  • Để bật giao diện người dùng có điều kiện, hãy thay thế các trường nhập tên người dùng hiện có bằng HTML sau đây sau nhận xét có liên quan trong tệp view/index.html:

view/index.html

<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<mdui-text-field id="username" label="Username" name="username" autocomplete="username webauthn" autofocus></mdui-text-field>

Phát hiện các tính năng, gọi WebAuthn và bật giao diện người dùng có điều kiện

  1. Trong tệp view/index.html, sau dòng nhận xét có liên quan, hãy thay thế câu lệnh import hiện có bằng đoạn mã sau:

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
import {
  $,
  _fetch,
  loading,
  authenticate 
} from "/client.js";

Đoạn mã này nhập hàm authenticate() mà bạn đã triển khai trước đó.

  1. Xác nhận rằng đối tượng window.PulicKeyCredential có sẵn và phương thức PublicKeyCredential.isConditionalMediationAvailable() trả về giá trị true, sau đó gọi hàm authenticate():

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
if (window.PublicKeyCredential &&
    PublicKeyCredential.getClientCapabilities) {
  try {

    // Is conditional UI available in this browser?
      const capabilities = await PublicKeyCredential.getClientCapabilities();
      if (capabilities.conditionalGet) {

      // If conditional UI is available, invoke the authenticate() function.
      const user = await authenticate();
      if (user) {

        // Proceed only when authentication succeeds.
        $("#username").value = user.username;
        loading.start();
        location.href = "/home";
      } else {
        throw new Error("User not found.");
      }
    }
  } catch (e) {
    loading.stop();

    // A NotAllowedError indicates that the user canceled the operation.
    if (e.name !== "NotAllowedError") {
      console.error(e);
      alert(e.message);
    }
  }
}

Xem mã giải pháp cho phần này

view/index.html

<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<mdui-text-field id="username" label="Username" name="username" autocomplete="username webauthn" autofocus></mdui-text-field>

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
import { 
  $, 
  _fetch, 
  loading, 
  authenticate 
} from '/client.js';

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.        

// Is WebAuthn available on this browser?
if (window.PublicKeyCredential &&
    PublicKeyCredential.getClientCapabilities) {
  try {
    // Is conditional UI available in this browser?
    const capabilities = await PublicKeyCredential.getClientCapabilities();
    if (capabilities.conditionalGet) {
      // If conditional UI is available, invoke the authenticate() function.
      const user = await authenticate();
      if (user) {
        // Proceed only when authentication succeeds.
        $('#username').value = user.username;
        loading.start();
        location.href = '/home';
      } else {
        throw new Error('User not found.');
      }
    }
  } catch (e) {
    loading.stop();
    // A NotAllowedError indicates that the user canceled the operation.
    if (e.name !== 'NotAllowedError') {
      console.error(e);
      alert(e.message);
    }
  }
}

Dùng thử

Bạn đã triển khai quy trình tạo, đăng ký, hiển thị và xác thực khoá truy cập trên trang web của mình.

Để dùng thử, hãy làm theo các bước sau:

  1. Chuyển đến thẻ xem trước.
  2. Đăng xuất nếu cần.
  3. Nhấp vào hộp văn bản tên người dùng. Một hộp thoại sẽ xuất hiện.
  4. Chọn tài khoản mà bạn muốn đăng nhập.
  5. Xác minh danh tính bằng phương thức khoá màn hình của thiết bị. Bạn sẽ được chuyển hướng đến trang /home và đăng nhập.

Một hộp thoại nhắc bạn xác minh danh tính bằng mật khẩu hoặc khoá truy cập đã lưu.

7. Xin chúc mừng!

Bạn đã hoàn thành lớp học lập trình này! Nếu có thắc mắc, bạn hãy đặt câu hỏi trên danh sách gửi thư FIDO-DEV hoặc trên StackOverflow và gắn thẻ passkey.

Tìm hiểu thêm