Tạo khoá truy cập cho hoạt động đăng nhập không cần mật khẩu

Khoá truy cập giúp tài khoản người dùng an toàn, đơn giản và dễ sử dụng hơn.

Việc sử dụng khoá truy cập giúp tăng cường bảo mật, đơn giản hoá việc đăng nhập và thay thế mật khẩu. Không giống như mật khẩu thông thường mà người dùng phải nhớ và nhập theo cách thủ công, khoá truy cập sử dụng các cơ chế khoá màn hình của thiết bị như sinh trắc học hoặc mã PIN, đồng thời giảm nguy cơ lừa đảo và đánh cắp thông tin xác thực.

Khoá truy cập đồng bộ hoá trên các thiết bị thông qua các nhà cung cấp khoá truy cập như Trình quản lý mật khẩu của Google và Chuỗi khoá iCloud.

Bạn phải tạo một khoá truy cập, lưu trữ khoá riêng tư một cách an toàn cho nhà cung cấp khoá truy cập cùng với siêu dữ liệu cần thiết và khoá công khai của khoá truy cập được lưu trữ trên máy chủ của bạn để xác thực. Khoá riêng tư sẽ phát hành chữ ký sau khi người dùng xác minh trên miền hợp lệ, giúp khoá truy cập chống lại hành vi lừa đảo. Khoá công khai xác minh chữ ký mà không cần lưu trữ thông tin xác thực nhạy cảm, giúp khoá truy cập chống lại hành vi đánh cắp thông tin xác thực.

Cách tạo khoá truy cập

Trước khi người dùng có thể đăng nhập bằng khoá truy cập, bạn nên tạo khoá truy cập, liên kết khoá truy cập đó với một tài khoản người dùng và lưu trữ khoá công khai của khoá truy cập đó trên máy chủ của bạn.

Bạn có thể yêu cầu người dùng tạo khoá truy cập trong một trong các trường hợp sau:

  • Trong hoặc sau khi đăng ký.
  • Sau khi đăng nhập.
  • Sau khi đăng nhập bằng khoá truy cập trên một thiết bị khác (tức là [authenticatorAttachment](https://web.dev/articles/passkey-form-autofill#authenticator-attachment)cross-platform).
  • Trên một trang chuyên dụng mà người dùng có thể quản lý khoá truy cập của họ.

Để tạo khoá truy cập, bạn sử dụng API WebAuthn.

Quy trình đăng ký khoá truy cập bao gồm 4 thành phần:

  • Phần phụ trợ: Lưu trữ thông tin tài khoản người dùng, bao gồm cả khoá công khai.
  • Phần giao diện người dùng: Giao tiếp với trình duyệt và tìm nạp dữ liệu cần thiết từ phần phụ trợ.
  • Trình duyệt: Chạy JavaScript và tương tác với API WebAuthn.
  • Nhà cung cấp khoá truy cập: Tạo và lưu trữ khoá truy cập. Đây thường là một trình quản lý mật khẩu như Trình quản lý mật khẩu của Google hoặc khoá bảo mật.
Quy trình tạo và đăng ký khoá truy cập
Quy trình tạo và đăng ký khoá truy cập.

Trước khi tạo khoá truy cập, hãy đảm bảo rằng hệ thống đáp ứng các điều kiện tiên quyết sau:

  • Tài khoản người dùng được xác minh thông qua một phương thức bảo mật (ví dụ: email, xác minh qua điện thoại hoặc liên kết danh tính) trong một khoảng thời gian ngắn.

  • Giao diện người dùng và phần phụ trợ có thể giao tiếp một cách an toàn để trao đổi dữ liệu thông tin xác thực.

  • Trình duyệt hỗ trợ WebAuthn và tạo khoá truy cập.

Chúng tôi có thể hướng dẫn bạn cách kiểm tra hầu hết các vấn đề đó trong các phần sau.

Khi hệ thống đáp ứng điều kiện này, quy trình sau đây sẽ diễn ra để tạo khoá truy cập:

  1. Hệ thống kích hoạt quy trình tạo khoá truy cập khi người dùng bắt đầu hành động (ví dụ: nhấp vào nút "Tạo khoá truy cập" trên trang quản lý khoá truy cập hoặc sau khi hoàn tất quy trình đăng ký).
  2. Giao diện người dùng yêu cầu dữ liệu thông tin xác thực cần thiết từ phần phụ trợ, bao gồm thông tin người dùng, một thử thách và mã thông tin xác thực để ngăn chặn thông tin trùng lặp.
  3. Giao diện người dùng gọi navigator.credentials.create() để nhắc nhà cung cấp khoá truy cập của thiết bị tạo khoá truy cập bằng thông tin từ phần phụ trợ. Lưu ý rằng lệnh gọi này trả về một lời hứa.
  4. Thiết bị của người dùng xác thực người dùng bằng phương thức sinh trắc học, mã PIN hoặc hình mở khoá để tạo khoá truy cập.
  5. Trình cung cấp khoá truy cập sẽ tạo khoá truy cập và trả về thông tin xác thực khoá công khai cho giao diện người dùng, giải quyết lời hứa.
  6. Giao diện người dùng sẽ gửi thông tin xác thực khoá công khai đã tạo đến phần phụ trợ.
  7. Phần phụ trợ lưu trữ khoá công khai và các dữ liệu quan trọng khác để xác thực trong tương lai,
  8. Phần phụ trợ sẽ thông báo cho người dùng (ví dụ: bằng email) để xác nhận việc tạo khoá truy cập và phát hiện các truy cập trái phép tiềm ẩn.

Quy trình này đảm bảo quy trình đăng ký khoá truy cập an toàn và liền mạch cho người dùng.

Khả năng tương thích

Hầu hết các trình duyệt đều hỗ trợ WebAuthn, với một số điểm thiếu sót nhỏ. Hãy xem passkeys.dev để biết thông tin chi tiết về khả năng tương thích của trình duyệt và hệ điều hành.

Tạo khoá truy cập mới

Để tạo khoá truy cập mới, đây là quy trình mà giao diện người dùng phải tuân theo:

  1. Kiểm tra khả năng tương thích.
  2. Tìm nạp thông tin từ phần phụ trợ.
  3. Gọi API WebAuth để tạo khoá truy cập.
  4. Gửi khoá công khai được trả về đến phần phụ trợ.
  5. Lưu thông tin xác thực.

Các phần sau đây sẽ hướng dẫn bạn cách thực hiện.

Kiểm tra khả năng tương thích

Trước khi hiển thị nút "Tạo khoá truy cập mới", giao diện người dùng phải kiểm tra xem:

  • Trình duyệt hỗ trợ WebAuthn bằng PublicKeyCredential.

Browser Support

  • Chrome: 67.
  • Edge: 18.
  • Firefox: 60.
  • Safari: 13.

Source

  • Thiết bị hỗ trợ trình xác thực nền tảng (có thể tạo khoá truy cập và xác thực bằng khoá truy cập) với PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable().

Browser Support

  • Chrome: 67.
  • Edge: 18.
  • Firefox: 60.
  • Safari: 13.

Source

Browser Support

  • Chrome: 108.
  • Edge: 108.
  • Firefox: 119.
  • Safari: 16.

Source

Đoạn mã sau đây cho biết cách bạn có thể kiểm tra khả năng tương thích trước khi hiển thị các tuỳ chọn liên quan đến khoá truy cập.

// Availability of `window.PublicKeyCredential` means WebAuthn is usable.  
// `isUserVerifyingPlatformAuthenticatorAvailable` means the feature detection is usable.  
// `isConditionalMediationAvailable` means the feature detection is usable.  
if (window.PublicKeyCredential &&  
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&  
    PublicKeyCredential.isConditionalMediationAvailable) {  
  // Check if user verifying platform authenticator is available.  
  Promise.all([  
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),  
    PublicKeyCredential.isConditionalMediationAvailable(),  
  ]).then(results => {  
    if (results.every(r => r === true)) {  
      // Display "Create a new passkey" button  
    }  
  });  
}  

Trong ví dụ này, nút Tạo khoá truy cập mới sẽ chỉ hiển thị nếu tất cả các điều kiện đều được đáp ứng.

Tìm nạp thông tin từ phần phụ trợ

Khi người dùng nhấp vào nút, hãy tìm nạp thông tin bắt buộc từ phần phụ trợ để gọi navigator.credentials.create().

Đoạn mã sau đây cho thấy một đối tượng JSON có thông tin bắt buộc để gọi navigator.credentials.create():

// Example `PublicKeyCredentialCreationOptions` contents
{
  challenge: *****,
  rp: {
    name: "Example",
    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'],
  }],
  authenticatorSelection: {
    authenticatorAttachment: "platform",
    requireResidentKey: true,
  }
}

Các cặp khoá-giá trị trong đối tượng chứa thông tin sau:

  • challenge: Một thử thách do máy chủ tạo trong ArrayBuffer cho lượt đăng ký này.
  • rp.id: Mã nhận dạng bên phụ thuộc (RP ID), miền và trang web có thể chỉ định miền hoặc hậu tố có thể đăng ký. Ví dụ: nếu nguồn gốc của một 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 trên example.com. Hãy xem phần Cho phép sử dụng lại khoá truy cập trên các trang web của bạn bằng Yêu cầu gốc có liên quan để biết thêm thông tin về vấn đề này.
  • rp.name: Tên của RP (Bên phụ thuộc). Phương thức này không còn được dùng nữa trong WebAuthn L3 nhưng vẫn được đưa vào vì lý do tương thích.
  • user.id: Một mã nhận dạng người dùng duy nhất trong ArrayBuffer, được tạo khi tạo tài khoản. Tên này phải là tên cố định, không giống như tên người dùng có thể chỉnh sửa. Mã nhận dạng người dùng xác định một tài khoản, nhưng không được chứa thông tin nhận dạng cá nhân (PII). Có thể bạn đã có mã nhận dạng người dùng trong hệ thống, nhưng nếu cần, hãy tạo một mã nhận dạng dành riêng cho khoá truy cập để không có PII nào trong đó.
  • user.name: Một mã nhận dạng duy nhất mà người dùng sẽ nhận ra đối với tài khoản của mình, 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.
  • user.displayName: Tên bắt buộc, dễ dùng hơn cho tài khoản. Tên này không cần phải là duy nhất và có thể là tên do người dùng chọn. Nếu trang web của bạn không có giá trị phù hợp để đưa 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.
  • pubKeyCredParams: Chỉ định các thuật toán khoá công khai được hỗ trợ bởi RP (bên phụ thuộc). 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 hỗ trợ cho ECDSA với P-256 và RSA PKCS#1 và việc hỗ trợ các tiêu chuẩn này sẽ mang lại phạm vi bảo vệ đầy đủ.
  • excludeCredentials: Danh sách mã thông tin đăng nhập đã đăng ký. Ngăn việc đăng ký cùng một thiết bị hai lần bằng cách cung cấp danh sách mã thông tin xác thực đã đăng ký. Thành phần transports (nếu được cung cấp) sẽ chứa kết quả của lệnh gọi getTransports() trong quá trình đăng ký từng thông tin đăng nhập.
  • authenticatorSelection.authenticatorAttachment: Đặt giá trị này thành "platform" cùng với hint: ['client-device'] nếu việc tạo khoá truy cập này là nâng cấp từ mật khẩu, chẳng hạn như trong một chương trình khuyến mãi sau khi đăng nhập. "platform" cho biết rằng RP muốn có một trình xác thực nền tảng (trình xác thực được nhúng vào thiết bị nền tảng) không nhắc người dùng, ví dụ: cắm khoá bảo mật USB. Người dùng có một tuỳ chọn đơn giản hơn để tạo khoá truy cập.
  • authenticatorSelection.requireResidentKey: Đặt thành true boolean. Thông tin xác thực có thể phát hiện (khoá thường trú) lưu trữ thông tin người dùng vào khoá truy cập và cho phép người dùng chọn tài khoản sau khi xác thực.
  • authenticatorSelection.userVerification: Cho biết quy trình xác minh người dùng bằng cách sử dụng tính năng khoá màn hình thiết bị là "required", "preferred" hay "discouraged". Giá trị mặc định là "preferred", có nghĩa là trình xác thực có thể bỏ qua bước xác minh người dùng. Đặt thuộc tính này thành "preferred" hoặc bỏ qua thuộc tính này.

Bạn nên tạo đối tượng trên máy chủ, mã hoá ArrayBuffer bằng Base64URL và tìm nạp đối tượng đó từ giao diện người dùng. Bằng cách này, bạn có thể giải mã tải trọng bằng PublicKeyCredential.parseCreationOptionsFromJSON() và truyền tải trọng đó trực tiếp đến navigator.credentials.create().

Đoạn mã sau đây cho biết cách bạn có thể tìm nạp và giải mã thông tin cần thiết để tạo khoá truy cập.

// Fetch an encoded `PubicKeyCredentialCreationOptions` from the server.
const _options = await fetch('/webauthn/registerRequest');

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

Gọi API WebAuthn để tạo khoá truy cập

Gọi navigator.credentials.create() để tạo khoá truy cập mới. API trả về một lời hứa, chờ hoạt động tương tác của người dùng hiển thị hộp thoại phương thức.

Browser Support

  • Chrome: 60.
  • Edge: 18.
  • Firefox: 60.
  • Safari: 13.

Source

// Invoke WebAuthn to create a passkey.
const credential = await navigator.credentials.create({
  publicKey: options
});

Gửi thông tin xác thực khoá công khai được trả về đến phần phụ trợ

Sau khi người dùng được xác minh bằng phương thức khoá màn hình của thiết bị, một khoá truy cập sẽ được tạo và lời hứa sẽ được phân giải bằng cách trả về đối tượng PublicKeyCredential cho giao diện người dùng.

Lời hứa có thể bị từ chối vì nhiều lý do. Bạn có thể xử lý các lỗi này bằng cách kiểm tra thuộc tính name của đối tượng Error:

  • InvalidStateError: Khoá truy cập đã tồn tại trên thiết bị. Người dùng sẽ không thấy hộp thoại lỗi nào. Trang web không được coi đây là lỗi. Người dùng muốn thiết bị cục bộ được đăng ký và thiết bị đó đã được đăng ký.
  • NotAllowedError: Người dùng đã huỷ thao tác.
  • AbortError: Thao tác đã bị huỷ.
  • Ngoại lệ khác: Đã xảy ra lỗi không mong muốn. Trình duyệt hiển thị hộp thoại lỗi cho người dùng.

Đối tượng thông tin xác thực khoá công khai chứa các thuộc tính sau:

  • 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: Phiên bản ArrayBuffer của mã thông tin xác thực.
  • response.clientDataJSON: Dữ liệu ứng dụng khách được mã hoá ArrayBuffer.
  • response.attestationObject: Một đối tượng chứng thực đượ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.
  • 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ó thể sử dụng khoá truy cập.
  • type: Trường này luôn được đặt thành "public-key".

Mã hoá đối tượng bằng phương thức .toJSON(), chuyển đổi tuần tự đối tượng bằng JSON.stringify() rồi gửi đối tượng đó đến máy chủ.

...

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

// Encode and send the credential to the server for verification.  
const response = await fetch('/webauthn/registerResponse', {
  method: 'post',
  credentials: 'same-origin',
  body: result
});
...

Lưu thông tin xác thực

Sau khi nhận được thông tin xác thực khoá công khai ở phần phụ trợ, bạn nên sử dụng thư viện phía máy chủ hoặc giải pháp thay vì viết mã của riêng mình để xử lý thông tin xác thực khoá công khai.

Sau đó, bạn có thể lưu trữ thông tin được truy xuất từ thông tin xác thực vào cơ sở dữ liệu để sử dụng sau này.

Danh sách sau đây bao gồm các thuộc tính nên lưu:

  • Mã thông tin xác thực: Mã thông tin xác thực được trả về cùng với thông tin xác thực khoá công khai.
  • Tên thông tin xác thực: Tên của thông tin xác thực. Đặt tên cho khoá truy cập đó theo tên của nhà cung cấp khoá truy cập đã tạo khoá truy cập đó (có thể xác định được dựa trên AAGUID).
  • Mã nhận dạng người dùng: Mã nhận dạng người dùng dùng để tạo khoá truy cập.
  • Khoá công khai: Khoá công khai được trả về cùng với thông tin xác thực khoá công khai. Bạn cần phải thực hiện việc này để xác minh câu nhận định về khoá truy cập.
  • Ngày và giờ tạo: Ghi lại ngày và giờ tạo khoá truy cập. Điều này rất hữu ích để xác định khoá truy cập.
  • Ngày và giờ sử dụng gần đây nhất: Ghi lại ngày và giờ gần đây nhất mà người dùng sử dụng khoá truy cập để đăng nhập. Điều này hữu ích để xác định khoá truy cập mà người dùng đã sử dụng (hoặc không sử dụng).
  • AAGUID: Giá trị nhận dạng duy nhất của nhà cung cấp khoá truy cập.
  • Cờ đủ điều kiện sao lưu: đúng nếu thiết bị đủ điều kiện để đồng bộ hoá khoá truy cập. Thông tin này giúp người dùng xác định khoá truy cập có thể đồng bộ hoá và khoá truy cập liên kết với thiết bị (không thể đồng bộ hoá) trên trang quản lý khoá truy cập.

Hãy làm theo hướng dẫn chi tiết hơn tại bài viết Đăng ký khoá truy cập phía máy chủ

Báo hiệu nếu không đăng ký được

Nếu không đăng ký được khoá truy cập, người dùng có thể bị nhầm lẫn. Nếu có khoá truy cập trong trình cung cấp khoá truy cập và khoá truy cập đó có sẵn cho người dùng, nhưng khoá công khai được liên kết không được lưu trữ ở phía máy chủ, thì các lần đăng nhập bằng khoá truy cập sẽ không bao giờ thành công và khó khắc phục sự cố. Hãy nhớ thông báo cho người dùng nếu đó là trường hợp.

Để ngăn chặn tình trạng như vậy, bạn có thể gửi tín hiệu về khoá truy cập không xác định cho trình cung cấp khoá truy cập bằng Signal API. Bằng cách gọi PublicKeyCredential.signalUnknownCredential() bằng mã nhận dạng RP và mã nhận dạng thông tin xác thực, RP có thể thông báo cho nhà cung cấp khoá truy cập rằng thông tin xác thực đã chỉ định đã bị xoá hoặc không tồn tại. Nhà cung cấp khoá truy cập sẽ quyết định cách xử lý tín hiệu này, nhưng nếu được hỗ trợ, khoá truy cập được liên kết sẽ bị xoá.

// Detect authentication failure due to lack of the credential
if (response.status === 404) {
  // Feature detection
  if (PublicKeyCredential.signalUnknownCredential) {
    await PublicKeyCredential.signalUnknownCredential({
      rpId: "example.com",
      credentialId: "vI0qOggiE3OT01ZRWBYz5l4MEgU0c7PmAA" // base64url encoded credential ID
    });
  } else {
    // Encourage the user to delete the passkey from the password manager nevertheless.
    ...
  }
}

Để tìm hiểu thêm về Signal API, hãy đọc bài viết Duy trì tính nhất quán giữa khoá truy cập và thông tin xác thực trên máy chủ của bạn bằng Signal API.

Gửi thông báo cho người dùng

Việc gửi thông báo (chẳng hạn như email) khi khoá truy cập được đăng ký sẽ giúp người dùng phát hiện hành vi truy cập trái phép vào tài khoản. Nếu kẻ tấn công tạo khoá truy cập mà người dùng không biết, thì khoá truy cập đó vẫn có thể được dùng để lợi dụng trong tương lai, ngay cả sau khi mật khẩu được thay đổi. Thông báo này sẽ cảnh báo người dùng và giúp ngăn chặn điều này.

Danh sách kiểm tra

  • Xác minh người dùng (tốt nhất là sử dụng email hoặc một phương thức bảo mật) trước khi cho phép họ tạo khoá truy cập.
  • Ngăn việc tạo khoá truy cập trùng lặp cho cùng một nhà cung cấp khoá truy cập bằng cách sử dụng excludeCredentials.
  • Lưu AAGUID để xác định trình cung cấp khoá truy cập và đặt tên cho thông tin xác thực của người dùng.
  • Báo hiệu nếu không đăng ký được khoá truy cập bằng PublicKeyCredential.signalUnknownCredential().
  • Gửi thông báo cho người dùng sau khi tạo và đăng ký khoá truy cập cho tài khoản của họ.

Tài nguyên

Bước tiếp theo: Đăng nhập bằng khoá truy cập thông qua tính năng tự động điền biểu mẫu.