Đă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 lựa chọn để tạo khoá truy cập. Gửi các tham số này đến ứng dụng khách để bạn có thể truyề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 giải quyết và trả về một thông tin đăng nhập PublicKeyCredential.
  • Xác minh thông tin đăng nhập và lưu trữ thông tin đó trên máy chủ.

Các phần sau đây sẽ đi sâu vào thông tin cụ thể của từng bước.

Tạo các lựa chọn tạo thông tin đăng nhập

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.

Để làm việc này, hãy dựa vào thư viện phía máy chủ FIDO. Thường thì nó sẽ cung cấp một hàm tiện ích có thể tạo các lựa 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 đăng nhập mà bạn đang tạo. Sau khi xác định tất cả các tham số 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 tham số khác phải được xác định một cách linh động trên máy chủ:

  • rpId: Để điền RP ID 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 sẵn các trường này, hãy sử dụng thông tin 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 đang tạo khoá truy cập khi đăng ký). user.name thường là một địa chỉ email và là giá trị duy nhất cho RP. user.displayName là một 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. Mã này phải là mã 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 của mình, nhưng nếu cần, hãy tạo một mã nhận dạng người dùng dành riêng cho khoá truy cập để không chứa bất kỳ thông tin nhận dạng cá nhân nào.
  • excludeCredentials: Danh sách mã nhận dạng của thông tin đăng nhập hiện có để ngăn việc sao chép khoá truy cập từ nhà cung cấp khoá truy cập. Để điền sẵn thông tin vào trường này, hãy tra cứu thông tin đăng nhập 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 tại phần Ngăn việc tạo khoá truy cập mới nếu đã có một khoá truy cập.
  • challenge: Đối với việc đăng ký thông tin đăng nhập, thử thách này không liên quan 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 nhà cung cấp khoá truy cập và dữ liệu mà nhà cung cấp đó phát ra. Tuy nhiên, ngay cả khi bạn không sử dụng chứng thực, thì thử thách vẫn là một trường bắt buộc. Hướng dẫn cách tạo một thử thách an toàn để xác thực có trong phần 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ủ thành base64URL để có thể phân phối PublicKeyCredentialCreationOptions qua HTTPS.

PublicKeyCredentialCreationOptions bao gồm các trường là ArrayBuffer, nên 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 cách sử dụng base64URL rồi giải mã trên máy khách.

  • Trên máy chủ, việc mã hoá và giải mã thường được xử lý bằng thư viện phía máy chủ FIDO.
  • Trên ứng dụng, bạn cần tự mã hoá và giải mã dữ liệu. Trong tương lai, việc này sẽ trở nên dễ dàng hơn: bạn có thể sử dụng một phương thức để chuyển đổi các lựa chọn dưới dạng JSON thành PublicKeyCredentialCreationOptions. Kiểm tra trạng thái triển khai trong Chrome.

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

Chúng tôi đang sử dụng thư viện SimpleWebAuthn trong các ví dụ của mình. Ở đây, chúng ta sẽ chuyển việc tạo các lựa chọn thông tin đăng nhập 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 = await 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 phân giải thành công trên ứng dụng, tức là 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 tượng này đại diện cho phản hồi của nhà cung cấp khoá truy cập đối với chỉ dẫn của ứng dụng để tạo khoá truy cập. Nó chứa thông tin về thông tin đăng nhập mới mà bạn cần với tư cách là một 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 nhận được, hãy xác minh địa chỉ đó.

Chuyển bước xác minh này cho thư viện phía máy chủ FIDO. Thường thì nó sẽ cung cấp một hàm tiện ích cho mục đích này. Ví dụ: SimpleWebAuthn cung cấp verifyRegistrationResponse. Tìm hiểu những gì đ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 để sau này 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 đăng nhập đó.

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, 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 khác qua Trình quản lý mật khẩu của Google.

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

Những thông tin khác có thể hữu ích cho mục đích nâng cao trải nghiệm người dùng, bao gồm cả những thông tin như nhà cung cấp khoá truy cập, thời gian tạo thông tin đăng nhập và thời gian sử dụng gần đây nhất. Đọc thêm trong bài viết Thiết kế giao diện người dùng cho khoá truy cập.

Mã ví dụ: lưu trữ thông tin đăng nhập

Chúng tôi đang sử dụng thư viện SimpleWebAuthn trong các ví dụ của mình. 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 {
      aaguid,
      credentialPublicKey,
      credentialID,
      credentialBackedUp
    } = registrationInfo;

    // Name the credential based on AAGUID
    const name =
      aaguid === undefined ||
      aaguid === '000000-0000-0000-0000-00000000' ?
        req.useragent?.platform : aaguids[aaguid].name;

    const base64CredentialID = isoBase64URL.fromBuffer(credentialID);
    const base64PublicKey = isoBase64URL.fromBuffer(credentialPublicKey);

    // Existing, signed-in user
    const { user } = res.locals;

    // Save the credential
    await Credentials.update({
      id: base64CredentialID,
      passkey_user_id: user.passkey_user_id,
      publicKey: base64PublicKey,
      name,
      aaguid,
      transports: response.response.transports,
      backed_up: credentialBackedUp,
      registered_at: new Date().getTime()
    });

    // 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. Trên web, đây là dữ liệu mà trình duyệt nhìn thấy. Nội dung này chứa nguồn gốc RP, thử thách và androidPackageName nếu ứng dụng là ứng dụng Android. Khi là một RP, việc đọc clientDataJSON cho phép bạn truy cập vào thông tin mà trình duyệt 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 chứng thực.
    • authenticatorData là dữ liệu mà nhà cung cấp khoá truy cập nhìn thấy. Là một RP, việc đọc authenticatorData cho phép bạn truy cập vào dữ liệu mà nhà cung cấp khoá truy cập nhìn thấy và trả về tại thời điểm yêu cầu create.

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

  • Bản thân thông tin đăng nhập khoá công khai và mã nhận dạng thông tin đăng nhập riêng biệt cho thông tin đăng nhập đó.
  • Mã nhận dạng RP được liên kết với thông tin đăng nhập.
  • Cá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à liệu người dùng có được xác minh thành công hay không (xem phần tìm hiểu sâu về userVerification).
  • AAGUID là giá trị nhận dạng cho nhà cung cấp khoá truy cập, chẳng hạn như Trình quản lý mật khẩu của Google. Dựa vào AAGUID, bạn có thể xác định trình cung cấp khoá truy cập và hiển thị tên trong trang quản lý khoá truy cập. (xem phần Xác định nhà cung cấp khoá truy cập bằng AAGUID)

Mặc dù authenticatorData được lồng trong attestationObject, nhưng thông tin mà khoá này chứa là cần thiết cho việc triển khai khoá truy cập của bạn, cho dù bạn có sử dụng chứng thực hay không. authenticatorData được mã hoá và chứa các trường được mã hoá ở định dạng nhị phân. Thư viện phía máy chủ của bạn thường sẽ xử lý việc 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 việc tận dụng phía máy khách getAuthenticatorData() để tiết kiệm một số công việc 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, 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ã nhận dạng 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.
  • Cờ trạng thái hoạt động của người dùng authenticatorData.up thường được dự kiến là true, nhưng nếu thông tin đăng nhập được tạo có điều kiện, thì thông tin đăng nhập đó dự kiến sẽ là false.
  • Kiểm tra để đảm bảo rằng ứng dụng có thể cung cấp thử thách mà bạn đã đưa ra. Nếu bạn không sử dụng chứng thực, thì quy trình kiểm tra này 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: quy trình này đảm bảo mã của bạn đã sẵn sàng nếu bạn quyết định sử dụng chứng thực trong tương lai.
  • Đảm bảo rằng mã nhận dạng thông tin đăng nhập 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à nhà cung cấp khoá truy cập dùng để tạo thông tin đăng nhập là thuật toán mà 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 thấy được). Đ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 xem mã nguồn của SimpleWebAuthn cho verifyRegistrationResponse hoặc tìm hiểu danh sách đầy đủ các quy trình xác minh trong quy cách.

Tiếp theo

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