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 hơn, đơn giản hơ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 lựa chọn đăng nhập.

Bạn cần có

Một trong những tổ hợp thiết bị sau:

  • Google Chrome trên thiết bị Android chạy Android 9 trở lên, tốt nhất là có cảm biến sinh trắc học.
  • Chrome trên thiết bị Windows chạy Windows 10 trở lên.
  • Safari 16 trở lên trên iPhone chạy iOS 16 trở lên hoặc iPad chạy iPadOS 16 trở lên.
  • Safari 16 trở lên hoặc Chrome trên thiết bị máy tính để bàn của Apple chạy macOS Ventura trở lên.

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

Trong lớp học lập trình này, bạn sẽ sử dụng một dịch vụ có tên là Glitch. Dịch vụ này cho phép bạn chỉnh sửa mã phía máy khách và phía máy chủ bằng JavaScript, đồng thời triển khai mã đó chỉ từ trình duyệt.

Mở dự án

  1. Mở dự án trong Glitch.
  2. Nhấp vào Remix (Tái tạo) để phân nhánh dự án Glitch.
  3. Trong trình đơn điều hướng ở cuối Glitch, hãy nhấp vào Xem trước > Xem trước trong cửa sổ mới. Một thẻ khác sẽ mở ra trong trình duyệt.

Nút Xem trước trong cửa sổ mới trong trình đơn điều hướng ở cuối Glitch

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

  1. Trong thẻ xem trước, 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 chức năng khoá màn hình của thiết bị. Bạn có thể thử trạng thái cuối cùng tại https://passkeys-codelab.glitch.me/.

Để 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 tính 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ủ.

Hộp thoại xác minh người dùng bằng khoá truy cập sẽ xuất hiện khi bạn tạo khoá truy cập.

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 Glitch, hãy chuyển đến tệp public/client.js rồi di chuyển đến cuối.
  2. 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 WebAuthn navigator.credentials.create().

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 việc hỗ trợ các 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.

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 khoá bảo mật (ví dụ: 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.

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ạ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:

public/client.js

// TODO: Add an ability to create a passkey: Create a credential.
// Base64URL decode some values.
options.user.id = base64url.decode(options.user.id);
options.challenge = base64url.decode(options.challenge);

if (options.excludeCredentials) {
  for (let cred of options.excludeCredentials) {
    cred.id = base64url.decode(cred.id);
  }
}
  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 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 một 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 của 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 tải 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 xác thực 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:

public/client.js

// TODO: Add an ability to create a passkey: Register the credential to the server endpoint.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;

// The authenticatorAttachment string in the PublicKeyCredential object is a new addition in WebAuthn L3.
if (cred.authenticatorAttachment) {
  credential.authenticatorAttachment = cred.authenticatorAttachment;
}

// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const attestationObject = base64url.encode(cred.response.attestationObject);

// Obtain transports.
const transports = cred.response.getTransports ? cred.response.getTransports() : [];

credential.response = {
  clientDataJSON,
  attestationObject,
  transports
};
  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 server endpoint.
  const options = await _fetch('/auth/registerRequest');
  
  // TODO: Add an ability to create a passkey: Create a credential.
  // Base64URL decode some values.

  options.user.id = base64url.decode(options.user.id);
  options.challenge = base64url.decode(options.challenge);

  if (options.excludeCredentials) {
    for (let cred of options.excludeCredentials) {
      cred.id = base64url.decode(cred.id);
    }
  }

  // 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.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;

  // The authenticatorAttachment string in the PublicKeyCredential object is a new addition in WebAuthn L3.
  if (cred.authenticatorAttachment) {
    credential.authenticatorAttachment = cred.authenticatorAttachment;
  }

  // Base64URL encode some values.
  const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
  const attestationObject =  
  base64url.encode(cred.response.attestationObject);

  // Obtain transports.
  const transports = cred.response.getTransports ? 
  cred.response.getTransports() : [];

  credential.response = {
    clientDataJSON,
    attestationObject,
    transports
  };

  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ý.

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

Thêm HTML giữ chỗ

  1. Trong Glitch, 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 class="mdc-typography mdc-typography--headline6"> Your registered 
  passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mwc-button id="create-passkey" class="hidden" icon="fingerprint" raised>Create a passkey</mwc-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 results = await Promise.all([

    // Is platform authenticator available in this browser?
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),

    // Is conditional UI available in this browser?
    PublicKeyCredential.isConditionalMediationAvailable()
  ]);
  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

    if (results.every(r => r === 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.';
}

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 = html`${res.length > 0 ? html`
    <mwc-list>
      ${res.map(cred => html`
        <mwc-list-item>
          <div class="list-item">
            <div class="entity-name">
              <span>${cred.name || 'Unnamed' }</span>
          </div>
          <div class="buttons">
            <mwc-icon-button data-cred-id="${cred.id}"  
            data-name="${cred.name || 'Unnamed' }" @click="${rename}"  
            icon="edit"></mwc-icon-button>
            <mwc-icon-button data-cred-id="${cred.id}" @click="${remove}" 
            icon="delete"></mwc-icon-button>
          </div>
         </div>
      </mwc-list-item>`)}
  </mwc-list>` : html`
  <mwc-list>
    <mwc-list-item>No credentials found.</mwc-list-item>
  </mwc-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 của phần 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 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 đã có 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 class="mdc-typography mdc-typography--headline6"> Your registered  
  passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mwc-button id="create-passkey" class="hidden" icon="fingerprint" raised>Create a passkey</mwc-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');

// Feature detections
if (window.PublicKeyCredential &&
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  try {
    const results = await Promise.all([

      // Is platform authenticator available in this browser?
      PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),

      // Is conditional UI available in this browser?
      PublicKeyCredential.isConditionalMediationAvailable()
    ]);
    if (results.every(r => r === 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`
  <mwc-list>
    ${res.map(cred => html`
      <mwc-list-item>
        <div class="list-item">
          <div class="entity-name">
            <span>${cred.name || 'Unnamed' }</span>
          </div>
          <div class="buttons">
            <mwc-icon-button data-cred-id="${cred.id}" data-name="${cred.name || 'Unnamed' }" @click="${rename}" icon="edit"></mwc-icon-button>
            <mwc-icon-button data-cred-id="${cred.id}" @click="${remove}" icon="delete"></mwc-icon-button>
          </div>
        </div>
      </mwc-list-item>`)}
  </mwc-list>` : html`
  <mwc-list>
    <mwc-list-item>No credentials found.</mwc-list-item>
  </mwc-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 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);
    }
  }
};

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. Trong thẻ xem trước, hãy đăng nhập bằng 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á các 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ý khỏi chrome://settings/passkeys trên máy tính hoặc khỏi 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 dựa trên 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 của 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.
const options = await _fetch('/auth/signinRequest');

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 đến 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": "passkeys-codelab.glitch.me",
  "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. Hãy xem đó là một mã thông báo CSRF.

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.

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ể.

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, phương thức 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 thông tin đăng nhập đến máy chủ dưới dạng một chuỗi:

public/client.js

// 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,
};
  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 điền sẵn thông tin 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. Do đó, 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 của 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 trong số các mục tự động điền ngoài mật khẩu. 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ì người dùng 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 họ theo cách cục 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.

Khoá truy cập được đề xuất trong quá trình tự động điền biểu mẫu.

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. Với bộ mã thông báo, bạn có thể gọi phương thức navigator.credentials.get() bằng chuỗi mediation: 'conditional' để kích hoạt giao diện người dùng khoá màn hình có điều kiện.

  • Để 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. -->
<input
  type="text"
  id="username"
  class="mdc-text-field__input"
  aria-labelledby="username-label"
  name="username"
  autocomplete="username webauthn"
  autofocus />

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.isConditionalMediationAvailable
) {
  try {

    // Is conditional UI available in this browser?
    const cma =
      await PublicKeyCredential.isConditionalMediationAvailable();
    if (cma) {

      // 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. -->
<input
  type="text"
  id="username"
  class="mdc-text-field__input"
  aria-labelledby="username-label"
  name="username"
  autocomplete="username webauthn"
  autofocus 
/>

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 avaiable in this browser?
if (window.PublicKeyCredential &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  try {

    // Is a conditional UI available in this browser?
    const cma= await PublicKeyCredential.isConditionalMediationAvailable();
    if (cma) {

      // If a 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 dùng để đă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