Đăng ký khoá truy cập phía máy chủ

Tổng quan

Dưới đây là thông tin tổng quan chung về các bước quan trọng trong quy trình đăng ký khoá truy cập:

Quy trình đăng ký khoá truy cập

  • Xác định các tuỳ chọn để tạo khoá truy cập. Gửi các mã này đến máy khách để bạn có thể chuyển chúng đến lệnh gọi tạo khoá truy cập: lệnh gọi API WebAuthn navigator.credentials.create trên web và credentialManager.createCredential trên Android. Sau khi người dùng xác nhận việc tạo khoá truy cập, lệnh gọi tạo khoá truy cập sẽ được phân giải và trả về thông tin xác thực PublicKeyCredential.
  • Xác minh thông tin đăng nhập và lưu trữ thông tin này trên máy chủ.

Các phần sau đây sẽ trình bày chi tiết cụ thể về từng bước.

Tạo các tuỳ chọn tạo thông tin xác thực

Bước đầu tiên bạn cần thực hiện trên máy chủ là tạo một đối tượng PublicKeyCredentialCreationOptions.

Để thực hiện việc này, hãy dựa vào thư viện phía máy chủ FIDO của bạn. Ứng dụng này thường sẽ cung cấp một chức năng tiện ích có thể tạo các lựa chọn này cho bạn. SimpleWebAuthn cung cấp, chẳng hạn như generateRegistrationOptions.

PublicKeyCredentialCreationOptions phải có mọi thứ cần thiết để tạo khoá truy cập: thông tin về người dùng, RP và cấu hình cho các thuộc tính của thông tin xác thực mà bạn đang tạo. Sau khi bạn xác định tất cả các thuộc tính này, hãy truyền nếu cần vào hàm trong thư viện phía máy chủ FIDO chịu trách nhiệm tạo đối tượng PublicKeyCredentialCreationOptions.

Một vài phút trong khoảng PublicKeyCredentialCreationOptions phút các trường có thể là hằng số. Các thuộc tính khác phải được xác định động trên máy chủ:

  • rpId: Để điền mã RP trên máy chủ, hãy sử dụng các hàm hoặc biến phía máy chủ cung cấp cho bạn tên máy chủ của ứng dụng web, chẳng hạn như example.com.
  • user.nameuser.displayName: Để điền các trường này, hãy sử dụng thông tin về phiên của người dùng đã đăng nhập (hoặc thông tin tài khoản người dùng mới, nếu người dùng tạo khoá truy cập khi đăng nhập). user.name thường là một địa chỉ email và dành riêng cho RP. user.displayName là tên thân thiện với người dùng. Xin lưu ý rằng không phải nền tảng nào cũng sử dụng displayName.
  • user.id: Một chuỗi riêng biệt, ngẫu nhiên được tạo khi tạo tài khoản. Đây phải là tên vĩnh viễn, không giống như tên người dùng có thể chỉnh sửa được. 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 bất kỳ thông tin nhận dạng cá nhân (PII) nào. Có thể bạn đã có mã nhận dạng người dùng trong hệ thống của mình, nhưng nếu cần, hãy tạo riêng mã nhận dạng cho khoá truy cập để không gặp bất kỳ thông tin nhận dạng cá nhân nào.
  • excludeCredentials: Danh sách các thông tin đăng nhập hiện có Mã nhận dạng để ngăn việc sao chép khoá truy cập từ trình cung cấp khoá truy cập. Để điền sẵn vào trường này, hãy tra cứu thông tin đăng nhập hiện có trong cơ sở dữ liệu của người dùng này. Xem thông tin chi tiết tại bài viết Ngăn tạo khoá truy cập mới nếu đã có khoá truy cập.
  • challenge: Đối với việc đăng ký thông tin xác thực, thử thách sẽ không liên quan trừ phi bạn sử dụng quy trình chứng thực. Đây là một kỹ thuật nâng cao hơn để xác minh danh tính của trình cung cấp khoá truy cập và dữ liệu mà trình cung cấp khoá truy cập cung cấp. Tuy nhiên, ngay cả khi bạn không sử dụng quy trình chứng thực, thử thách vẫn là một trường bắt buộc. Trong trường hợp đó, bạn có thể đặt thử thách này thành một 0 duy nhất cho đơn giản. Bạn có thể xem hướng dẫn về cách tạo yêu cầu xác thực an toàn trong bài viết Xác thực khoá truy cập phía máy chủ.

Mã hoá và giải mã

PublicKeyCredentialCreationOptions do máy chủ gửi
PublicKeyCredentialCreationOptions do máy chủ gửi. challenge, user.idexcludeCredentials.credentials phải được mã hoá phía máy chủ thành base64URL để có thể phân phối PublicKeyCredentialCreationOptions qua HTTPS.

PublicKeyCredentialCreationOptions bao gồm các trường là ArrayBuffer, vì vậy, JSON.stringify() không hỗ trợ. Điều này có nghĩa là hiện tại, để phân phối PublicKeyCredentialCreationOptions qua HTTPS, một số trường phải được mã hoá thủ công trên máy chủ bằng base64URL, sau đó được giải mã trên máy khách.

  • Trên máy chủ, thư viện phía máy chủ FIDO của bạn thường đảm nhận việc mã hoá và giải mã.
  • Trên máy khách, hiện tại bạn cần phải mã hoá và giải mã theo cách thủ công. Trong tương lai, bạn sẽ dễ dàng thực hiện được việc này: bạn sẽ có thể sử dụng phương thức để chuyển đổi các lựa chọn dưới dạng JSON thành PublicKeyCredentialCreationOptions. Xem trạng thái triển khai trong Chrome.

Mã ví dụ: tạo các tuỳ chọn tạo thông tin xác thực

Chúng ta đang dùng thư viện SimpleWebAuthn trong các ví dụ. Ở đây, chúng ta sẽ chuyển việc tạo thông tin xác thực khoá công khai cho hàm generateRegistrationOptions.

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';

router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
  const { user } = res.locals;
  // Ensure you nest verification function calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // `excludeCredentials` prevents users from re-registering existing
    // credentials for a given passkey provider
    const excludeCredentials = [];
    const credentials = Credentials.findByUserId(user.id);
    if (credentials.length > 0) {
      for (const cred of credentials) {
        excludeCredentials.push({
          id: isoBase64URL.toBuffer(cred.id),
          type: 'public-key',
          transports: cred.transports,
        });
      }
    }

    // Generate registration options for WebAuthn create
    const options = generateRegistrationOptions({
      rpName: process.env.RP_NAME,
      rpID: process.env.HOSTNAME,
      userID: user.id,
      userName: user.username,
      userDisplayName: user.displayName || '',
      attestationType: 'none',
      excludeCredentials,
      authenticatorSelection: {
        authenticatorAttachment: 'platform',
        requireResidentKey: true
      },
    });

    // Keep the challenge in the session
    req.session.challenge = options.challenge;

    return res.json(options);
  } catch (e) {
    console.error(e);
    return res.status(400).send({ error: e.message });
  }
});

Lưu trữ khoá công khai

PublicKeyCredentialCreationOptions do máy chủ gửi
navigator.credentials.create trả về một đối tượng PublicKeyCredential.

Khi navigator.credentials.create được phân giải thành công trên ứng dụng, điều đó có nghĩa là khoá truy cập đã được tạo thành công. Trả về đối tượng PublicKeyCredential.

Đối tượng PublicKeyCredential chứa một đối tượng AuthenticatorAttestationResponse đại diện cho phản hồi của trình cung cấp khoá truy cập đối với hướng dẫn của ứng dụng về việc tạo khoá truy cập. Tệp này chứa thông tin về thông tin đăng nhập mới mà bạn cần dưới dạng một bên bị hạn chế (RP) để xác thực người dùng sau này. Tìm hiểu thêm về AuthenticatorAttestationResponse trong Phụ lục: AuthenticatorAttestationResponse.

Gửi đối tượng PublicKeyCredential đến máy chủ. Sau khi bạn nhận được, hãy xác minh.

Hãy chuyển bước xác minh này cho thư viện phía máy chủ FIDO của bạn. Ứng dụng này thường sẽ cung cấp một chức năng tiện ích cho mục đích này. SimpleWebAuthn cung cấp, chẳng hạn như verifyRegistrationResponse. Tìm hiểu sâu hơn về những việc đang diễn ra trong Phụ lục: xác minh phản hồi đăng ký.

Sau khi xác minh thành công, hãy lưu trữ thông tin đăng nhập trong cơ sở dữ liệu của bạn để sau này, người dùng có thể xác thực bằng khoá truy cập liên kết với thông tin đăng nhập đó.

Sử dụng một bảng riêng cho thông tin xác thực khoá công khai liên kết với khoá truy cập. Một người dùng chỉ được có một mật khẩu nhưng có thể có nhiều khoá truy cập. Ví dụ: một khoá truy cập được đồng bộ hoá qua Chuỗi khoá iCloud của Apple và một khoá truy cập qua Trình quản lý mật khẩu của Google.

Dưới đây là một giản đồ mẫu mà bạn có thể sử dụng để lưu trữ thông tin đăng nhập:

Giản đồ cơ sở dữ liệu cho khoá truy cập

  • Bảng Người dùng:
    • user_id: Mã nhận dạng người dùng chính. Một mã nhận dạng cố định, ngẫu nhiên và duy nhất cho người dùng. Sử dụng khoá này làm khoá chính cho bảng Người dùng.
    • username. Tên người dùng do người dùng xác định, có thể chỉnh sửa được.
    • passkey_user_id: Mã nhận dạng người dùng không có PII dành riêng cho khoá truy cập, được biểu thị bằng user.id trong các tuỳ chọn đăng ký. Khi người dùng cố gắng xác thực sau này, trình xác thực sẽ cung cấp passkey_user_id này trong phản hồi xác thực trong userHandle. Bạn không nên đặt passkey_user_id làm khoá chính. Khoá chính có xu hướng trở thành PII (Thông tin nhận dạng cá nhân) trên các hệ thống vì được sử dụng rộng rãi.
  • Bảng Thông tin xác thực khoá công khai:
    • id: Mã chứng chỉ. Dùng khoá này làm khoá chính cho bảng Thông tin xác thực khoá công khai.
    • public_key: Khoá công khai của thông tin đăng nhập.
    • passkey_user_id: Sử dụng khoá này làm khoá ngoại để thiết lập mối liên kết với bảng Người dùng.
    • backed_up: Khoá truy cập được sao lưu nếu được trình cung cấp khoá truy cập đồng bộ hoá. Việc lưu trữ trạng thái sao lưu rất hữu ích nếu bạn muốn cân nhắc xoá mật khẩu trong tương lai đối với những người dùng lưu giữ backed_up khoá truy cập. Bạn có thể kiểm tra xem khoá truy cập đã được sao lưu hay chưa bằng cách kiểm tra cờ trong authenticatorData, hoặc dùng một tính năng thư viện phía máy chủ FIDO thường có sẵn để giúp bạn dễ dàng truy cập vào thông tin này. Việc lưu trữ điều kiện sao lưu có thể giúp giải quyết các thắc mắc tiềm năng của người dùng.
    • name: Tên hiển thị cho thông tin đăng nhập (không bắt buộc) để cho phép người dùng đặt tên tuỳ chỉnh cho thông tin xác thực.
    • transports: Một mảng phương tiện vận chuyển. Việc lưu trữ phương thức truyền tải rất hữu ích cho trải nghiệm xác thực của người dùng. Khi có phương thức truyền tải, trình duyệt có thể hoạt động phù hợp và hiển thị một giao diện người dùng khớp với phương thức truyền tải mà trình cung cấp khoá truy cập dùng để giao tiếp với ứng dụng – cụ thể là trong các trường hợp sử dụng xác thực lại khi allowCredentials không trống.

Có thể lưu trữ một số thông tin hữu ích khác để phục vụ cho mục đích trải nghiệm người dùng, bao gồm các mục như trình cung cấp khoá truy cập, thời gian tạo thông tin xác thực và thời gian sử dụng gần đây nhất. Tìm hiểu thêm trong bài viết Thiết kế giao diện người dùng của khoá truy cập.

Mã ví dụ: lưu trữ thông tin xác thực

Chúng ta đang dùng thư viện SimpleWebAuthn trong các ví dụ. Ở đây, chúng ta sẽ chuyển hoạt động xác minh phản hồi đăng ký cho hàm verifyRegistrationResponse.

import { isoBase64URL } from '@simplewebauthn/server/helpers';


router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;
  const response = req.body;
  // This sample code is for registering a passkey for an existing,
  // signed-in user

  // Ensure you nest verification function calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // Verify the credential
    const { verified, registrationInfo } = await verifyRegistrationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      requireUserVerification: false,
    });

    if (!verified) {
      throw new Error('Verification failed.');
    }

    const { credentialPublicKey, credentialID } = registrationInfo;

    // Existing, signed-in user
    const { user } = res.locals;
    
    // Save the credential
    await Credentials.update({
      id: base64CredentialID,
      publicKey: base64PublicKey,
      // Optional: set the platform as a default name for the credential
      // (example: "Pixel 7")
      name: req.useragent.platform, 
      transports: response.response.transports,
      passkey_user_id: user.passkey_user_id,
      backed_up: registrationInfo.credentialBackedUp
    });

    // Kill the challenge for this session
    delete req.session.challenge;

    return res.json(user);
  } catch (e) {
    delete req.session.challenge;

    console.error(e);
    return res.status(400).send({ error: e.message });
  }
});

Phụ lục: AuthenticatorAttestationResponse

AuthenticatorAttestationResponse chứa hai đối tượng quan trọng:

  • response.clientDataJSON là phiên bản JSON của dữ liệu ứng dụng, mà trên web là dữ liệu mà trình duyệt nhìn thấy. Tệp này chứa nguồn gốc RP, thông tin xác thực và androidPackageName nếu ứng dụng là một ứng dụng Android. Ở vai trò bên bị hạn chế (RP), việc đọc clientDataJSONcho phép bạn truy cập vào thông tin mà trình duyệt nhìn thấy tại thời điểm yêu cầu create.
  • response.attestationObjectchứa 2 thông tin:
    • attestationStatement không liên quan, trừ phi bạn sử dụng quy trình chứng thực.
    • authenticatorData là dữ liệu mà trình cung cấp khoá truy cập nhìn thấy. Ở vai trò RP, việc đọc authenticatorData cho phép bạn truy cập vào dữ liệu mà trình cung cấp khoá truy cập xem và được trả về tại thời điểm yêu cầu create.

authenticatorDatachứa thông tin thiết yếu về thông tin đăng nhập khoá công khai liên kết với khoá truy cập mới tạo:

  • Chính thông tin xác thực khoá công khai và một mã nhận dạng thông tin xác thực duy nhất cho khoá đó.
  • Mã bên bị hạn chế (RP) được liên kết với thông tin xác thực.
  • Cờ mô tả trạng thái người dùng khi khoá truy cập được tạo: liệu người dùng có thực sự có mặt hay không và liệu người dùng có được xác minh thành công hay không (xem userVerification).
  • AAGUID để xác định trình cung cấp khoá truy cập. Việc hiển thị trình cung cấp khoá truy cập có thể hữu ích cho người dùng, đặc biệt nếu họ đã đăng ký khoá truy cập cho dịch vụ của bạn trên nhiều trình cung cấp khoá truy cập.

Mặc dù authenticatorData được lồng trong attestationObject, nhưng thông tin trong đó vẫn cần thiết cho việc triển khai khoá truy cập cho dù bạn có sử dụng quy trình chứng thực hay không. authenticatorData được mã hoá và chứa các trường được mã hoá theo định dạng nhị phân. Thư viện phía máy chủ của bạn thường sẽ xử lý phân tích cú pháp và giải mã. Nếu bạn không sử dụng thư viện phía máy chủ, hãy cân nhắc tận dụng getAuthenticatorData() phía máy khách để tiết kiệm thời gian cho quá trình phân tích cú pháp và giải mã phía máy chủ.

Phụ lục: xác minh phản hồi đăng ký

Về cơ bản, quy trình xác minh phản hồi đăng ký bao gồm các bước kiểm tra sau:

  • Hãy đảm bảo rằng mã RP khớp với trang web của bạn.
  • Đảm bảo rằng nguồn gốc của yêu cầu là nguồn dự kiến cho trang web của bạn (URL trang web chính, ứng dụng Android).
  • Nếu bạn yêu cầu xác minh người dùng, hãy đảm bảo rằng cờ xác minh người dùng authenticatorData.uvtrue. Kiểm tra để đảm bảo cờ hiện diện của người dùng authenticatorData.uptrue, vì sự hiện diện của người dùng luôn bắt buộc đối với khoá truy cập.
  • Kiểm tra để đảm bảo rằng khách hàng có thể đưa ra thử thách mà bạn đã đưa ra. Nếu bạn không sử dụng quy trình chứng thực, thì bước kiểm tra này là không quan trọng. Tuy nhiên, việc triển khai quy trình kiểm tra này là một phương pháp hay nhất: đảm bảo mã của bạn đã sẵn sàng nếu bạn quyết định sử dụng quy trình chứng thực trong tương lai.
  • Đảm bảo rằng mã thông tin xác thực chưa được đăng ký cho bất kỳ người dùng nào.
  • Xác minh rằng thuật toán mà trình cung cấp khoá truy cập sử dụng để tạo thông tin xác thực là thuật toán bạn đã liệt kê (trong mỗi trường alg của publicKeyCredentialCreationOptions.pubKeyCredParams, thường được xác định trong thư viện phía máy chủ và bạn không nhìn thấy được). Điều này giúp đảm bảo rằng người dùng chỉ có thể đăng ký bằng thuật toán mà bạn đã chọn cho phép.

Để tìm hiểu thêm, hãy kiểm tra mã nguồn cho verifyRegistrationResponse của SimpleWebAuthn hoặc tìm hiểu chi tiết danh sách xác minh đầy đủ trong thông số kỹ thuật.

Tiếp theo

Xác thực khoá truy cập phía máy chủ