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 thay vì mật khẩu là một cách tuyệt vời để các trang web giúp tài khoản người dùng của họ an toàn hơn, đơn giản hơn, dễ sử dụng hơn và không cần mật khẩu. 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 chỉ bằng cách sử dụng 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áy chủ của bạn trước khi người dùng có thể dùng để đăng nhập.

Cách hoạt động

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

  • Khi người dùng đăng nhập bằng mật khẩu.
  • Khi người dùng đăng nhập bằng khoá truy cập trên một thiết bị khác (tức là authenticatorAttachmentcross-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ợ: Máy chủ phụ trợ lưu trữ cơ sở dữ liệu tài khoản lưu trữ khoá công khai và siêu dữ liệu khác về khoá truy cập.
  • Giao diện người dùng: Giao diện người dùng giao tiếp với trình duyệt và gửi các yêu cầu tìm nạp đến phần phụ trợ.
  • Trình duyệt: Trình duyệt của người dùng đang chạy JavaScript.
  • Authenticator: Trình xác thực của người dùng tạo và lưu trữ khoá truy cập. Điều này có thể bao gồm trình quản lý mật khẩu trên cùng một thiết bị với trình duyệt (ví dụ: khi sử dụng Windows Hello) hoặc trên một thiết bị khác, chẳng hạn như điện thoại.
Sơ đồ đăng ký khoá truy cập

Quy trình thêm khoá truy cập mới vào tài khoản người dùng hiện có như sau:

  1. Người dùng đăng nhập vào trang web.
  2. Sau khi đăng nhập, người dùng sẽ yêu cầu tạo khoá truy cập trên giao diện người dùng, chẳng hạn như bằng cách nhấn vào nút "Tạo khoá truy cập".
  3. Giao diện người dùng yêu cầu thông tin từ phần phụ trợ để tạo khoá truy cập, chẳng hạn như thông tin người dùng, một thử thách và mã thông tin xác thực cần loại trừ.
  4. Giao diện người dùng gọi navigator.credentials.create() để tạo khoá truy cập. Lệnh gọi này trả về một lời hứa.
  5. Khoá truy cập được tạo sau khi người dùng đồng ý sử dụng phương thức khoá màn hình của thiết bị. Lời hứa được giải quyết và thông tin xác thực khoá công khai được trả về cho giao diện người dùng.
  6. Phần giao diện người dùng sẽ gửi thông tin xác thực khoá công khai đến phần phụ trợ và lưu trữ mã thông tin xác thực và khoá công khai liên kết với tài khoản người dùng để xác thực trong tương lai.

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

Hầu hết các trình duyệt đều hỗ trợ WebAuthn, nhưng vẫn có một số điểm khác biệt nhỏ. Hãy tham khảo nội dung Hỗ trợ thiết bị – passkeys.dev để tìm hiểu những tổ hợp trình duyệt và hệ điều hành hỗ trợ việc tạo khoá truy cập.

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

Sau đây là cách hoạt động của giao diện người dùng khi có yêu cầu tạo khoá truy cập mới.

Phát hiện tính năng

Trước khi hiển thị nút "Tạo khoá truy cập mới", hãy kiểm tra xem:

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

Hỗ trợ trình duyệt

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

Nguồn

  • 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().

Hỗ trợ trình duyệt

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

Nguồn

Hỗ trợ trình duyệt

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

Nguồn

// 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  
    }  
  });  
}  

Cho đến khi tất cả các điều kiện được đáp ứng, khoá truy cập sẽ không được hỗ trợ trên trình duyệt này. Nút "Tạo khoá truy cập mới" sẽ không xuất hiện cho đến lúc đó.

Tìm nạp thông tin quan trọng 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 quan trọng để gọi navigator.credentials.create() từ phần phụ trợ:

  • challenge: Một thử thách do máy chủ tạo trong ArrayBuffer cho lượt đăng ký này. Chuỗi này là 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 ở đâ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 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 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. (Nếu 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à tên tài khoản bắt buộc và thân thiện với người dùng hơ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.
  • excludeCredentials: Ngăn việc đăng ký cùng một thiết bị bằng cách cung cấp danh sách mã thông tin đã đă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.

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ờ người dùng tương tác để hiển thị hộp thoại phương thức.

Hỗ trợ trình duyệt

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

Nguồn

const publicKeyCredentialCreationOptions = {
  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,
  }
};

const credential = await navigator.credentials.create({
  publicKey: publicKeyCredentialCreationOptions
});

// Encode and send the credential to the server for verification.  

Các tham số không được giải thích ở trên là:

  • rp.id: Mã nhận dạng RP là một miền và trang web có thể chỉ định miền của nó hoặc một 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.

  • rp.name: Tên của RP.

  • pubKeyCredParams: Trường này chỉ định các thuật toán khoá công khai được hỗ trợ của RP. 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 đủ.

  • authenticatorSelection.authenticatorAttachment: Đặt giá trị này thành "platform" nếu việc tạo khoá truy cập này là nâng cấp từ mật khẩu, ví dụ: 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) sẽ không nhắc người dùng chèn khoá bảo mật (ví dụ: 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 boolean "true". Thông tin xác thực có thể khám phá (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 khi xác thực. Tìm hiểu thêm về thông tin xác thực có thể tìm thấy trong bài viết Tìm hiểu chuyên sâu về thông tin xác thực có thể tìm thấy

  • authenticatorSelection.userVerification: Cho biết liệu quy trình xác minh người dùng bằng phương thức khoá màn hình thiết bị có phải là "required", "preferred" hay "discouraged" hay không. Giá trị mặc định là "preferred", 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.

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 đồng ý sử dụ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 giải quyết 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ị. Không có hộp thoại lỗi nào sẽ hiển thị cho người dùng và trang web không nên 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.
  • 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. Bạn cần lưu trữ giá trị này trong cơ sở dữ liệu ở phần phụ trợ.
  • rawId: Phiên bản ArrayBuffer của mã nhận dạng 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á 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".

Nếu sử dụng thư viện để xử lý đối tượng thông tin xác thực khoá công khai trên phần phụ trợ, bạn nên gửi toàn bộ đối tượng đó đến phần phụ trợ sau khi mã hoá một phần đối tượng bằng base64url.

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 trên phần phụ trợ, hãy chuyển thông tin đó đến thư viện FIDO để xử lý đối tượng.

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 một số thuộc tính thông thường cần lưu:

  • Mã thông tin xác thực (Khoá chính)
  • User ID
  • Khoá công khai

Thông tin xác thực khoá công khai cũng bao gồm những thông tin sau đây mà bạn có thể muốn lưu trong cơ sở dữ liệu:

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

Để xác thực người dùng, hãy đọc bài viết Đăng nhập bằng khoá truy cập thông qua tính năng tự động điền biểu mẫu.

Tài nguyên