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

Tổng quan

Sau đây là thông tin tổng quan về các bước chính liên quan đến việc đă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 ứng dụng khách để bạn có thể chuyển chúng đến lệnh gọi tạo khoá truy cập của mình: 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 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 xác thực và lưu trữ trên máy chủ.

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

Tạo 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 sử dụng thư viện phía máy chủ FIDO của bạn. Mã này thường cung cấp hàm hiệu dụng có thể tạo các tuỳ chọn này cho bạn. Ví dụ: SimpleWebAuthn cung cấp generateRegistrationOptions.

PublicKeyCredentialCreationOptions phải bao gồm mọi thứ cần thiết để tạo khoá truy cập: thông tin về người dùng, về 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 đối tượng này, hãy truyền chúng khi cần đến 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 số trường của PublicKeyCredentialCreationOptions 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 hoạt động 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 đang tạo khoá truy cập khi đăng ký). user.name thường là một địa chỉ email và mã riêng biệt của bên bị hạn chế. 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 ngẫu nhiên, duy nhất được tạo khi tạo tài khoản. Tên này phải là 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 một mã dành riêng cho khoá truy cập để đảm bảo mã đó không có bất kỳ PII nào.
  • excludeCredentials: Danh sách mã thông tin xác thực hiện có để ngăn chặn việc trùng lặp khoá truy cập từ nhà cung cấp khoá truy cập. Để điền sẵn thông tin cho trường này, hãy tra cứu thông tin xác thực hiện có của người dùng này trong cơ sở dữ liệu của bạn. Xem thông tin chi tiết trong phần Ngăn chặ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 phù hợp trừ phi bạn sử dụng chứng thực, 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 đó phát ra. 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 để đơn giản. Bạn có thể xem hướng dẫn về cách tạo phương thức xác thực bảo mật để xác thực trong bài viết Xác thực bằng 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ủ vào base64URL để PublicKeyCredentialCreationOptions có thể được phân phối qua HTTPS.

PublicKeyCredentialCreationOptions bao gồm các trường là ArrayBuffer, vì vậy, JSON.stringify() không hỗ trợ các trường này. Đ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á theo cách 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ủ, việc mã hoá và giải mã thường do thư viện FIDO phía máy chủ xử lý.
  • Trên máy khách, việc mã hoá và giải mã cần được thực hiện theo cách thủ công tại thời điểm này. Trong tương lai, việc này sẽ trở nên dễ dàng hơn: có sẵn một phương thức để chuyển đổi các tuỳ chọn dưới dạng JSON thành PublicKeyCredentialCreationOptions. Hãy xem trạng thái triển khai trong Chrome.

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

Chúng ta đang sử dụng thư viện SimpleWebAuthn trong các ví dụ. Ở đây, chúng ta sẽ chuyển việc tạo các tuỳ chọn 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ề đối tượng PublicKeyCredential.

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

Đố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 trong vai trò là bên bị hạn chế để 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. Thường thì mã này sẽ cung cấp chức năng hiệu dụng cho mục đích này. Ví dụ: SimpleWebAuthn cung cấp verifyRegistrationResponse. Tìm hiểu những gì đang xảy 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 xác thực trong cơ sở dữ liệu của bạn để người dùng có thể xác thực bằng khoá truy cập được liên kết với thông tin xác thực đó sau.

Sử dụng một bảng riêng cho thông tin xác thực khoá công khai được liên kết với khoá truy cập. Người dùng chỉ có thể có một mật khẩu duy nhất 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 được đồng bộ hoá qua Trình quản lý mật khẩu của Google.

Dưới đây là giản đồ mẫu mà bạn có thể sử dụng để lưu trữ thông tin xác thực:

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ã nhận dạng ngẫu nhiên, duy nhất và vĩnh viễn cho người dùng. Hãy 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 (Thông tin nhận dạng cá nhân) 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ý của bạn. Sau đó, khi người dùng cố gắng xác thực, 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ực sự trong 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ã thông tin xác thực. Sử 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 chứng chỉ danh tính.
    • passkey_user_id: 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 sẽ đượ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 sẽ hữu ích nếu bạn muốn cân nhắc việc xoá mật khẩu trong tương lai cho những người dùng lưu giữ mã xác thực backed_up. 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 sử dụng 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. Lưu trữ điều kiện sao lưu có thể giúp giải đáp 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 để cho phép người dùng cung cấp tên tuỳ chỉnh cho thông tin đăng nhập.
    • transports: Một mảng phương tiện truyền tải. Việc lưu trữ mạng 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ó sẵn mạng truyền tải, trình duyệt có thể hoạt động tương ứng và hiển thị giao diện người dùng khớp với trình truyền tải mà nhà cung cấp khoá truy cập dùng để giao tiếp với ứng dụng khách, đặc biệt là các trường hợp sử dụng xác thực lại trong đó allowCredentials không trống.

Các thông tin khác có thể hữu ích khi lưu trữ 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 được sử dụng gần đây nhất. Hãy đọc 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 sử dụng thư viện SimpleWebAuthn trong các ví dụ. Tại đây, chúng ta sẽ chuyển quy trình 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à một 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 của RP, yêu cầu xác thực và androidPackageName nếu ứng dụng là một ứng dụng Android. Với tư cách là RP, thao tá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.attestationObject chứa hai phần thông tin:
    • attestationStatement không liên quan trừ phi bạn sử dụng chứng thực.
    • authenticatorData là dữ liệu mà trình cung cấp khoá truy cập nhìn thấy. Trong vai trò là bên bị hạn chế, việc đọc authenticatorData sẽ 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à trả về tại thời điểm yêu cầu create.

authenticatorData chứa thông tin cần thiết về thông tin xác thực 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ã thông tin xác thực duy nhất cho thông tin đó.
  • Mã 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ự hiện diện hay không và người dùng đó có được xác minh thành công hay không (xem userVerification).
  • AAGUID giúp 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ủa bạn, đặc biệt khi họ có khoá truy cập đã đăng ký cho dịch vụ của bạn trên nhiều 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 để bạn 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 để tự lưu một số phân tích cú pháp và giải mã phía máy chủ.

Phụ lục: thông tin xác minh của phản hồi đăng ký

Trong trường hợp này, việc xác minh phản hồi đăng ký bao gồm các bước kiểm tra sau:

  • Đả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 gốc 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ờ authenticatorData.up về sự có mặt của người dùng là true, vì bạn luôn bắt buộc phải có sự hiện diện của người dùng để sử dụng mã xác thực.
  • 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 không quan trọng. Tuy nhiên, phương pháp hay nhất là triển khai quy trình kiểm tra này: đả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à một thuật toán mà bạn đã liệt kê (trong mỗi trường alg của publicKeyCredentialCreationOptions.pubKeyCredParams, trường này thường được xác định trong thư viện phía máy chủ và không hiển thị với bạn). Điều này đảm bảo rằng người dùng chỉ có thể đăng ký bằng các 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 của SimpleWebAuthn cho verifyRegistrationResponse hoặc xem danh sách đầy đủ các quy trình xác minh trong quy cách.

Tiếp theo

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