Xác thực bằng 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 quy trình xác thực khoá truy cập:

Quy trình xác thực khoá truy cập

  • Xác định thử thách xác thực và các tuỳ chọn khác cần thiết để xác thực bằng khoá truy cập. Gửi các mã này đến ứng dụng để bạn có thể chuyển chúng đến lệnh gọi xác thực bằng khoá truy cập (navigator.credentials.get trên web). Sau khi người dùng xác nhận xác thực khoá truy cập, lệnh gọi xác thực khoá truy cập sẽ được phân giải và trả về thông tin xác thực (PublicKeyCredential). Thông tin xác thực có chứa câu xác nhận xác thực.
  • Xác minh khẳng định xác thực.
  • Nếu thông tin xác nhận là hợp lệ, hãy xác thực người dùng.

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

Tạo thử thách

Trong thực tế, thử thách là một mảng các byte ngẫu nhiên, được biểu thị dưới dạng đối tượng ArrayBuffer.

// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8

Để đảm bảo thử thách hoàn thành đúng mục đích, bạn phải:

  1. Đảm bảo người dùng không bao giờ sử dụng cùng một thử thách nhiều lần. Tạo thử thách mới mỗi khi bạn đăng nhập. Huỷ thử thách sau mỗi lần đăng nhập, cho dù đăng nhập thành công hay không thành công. Bạn cũng có thể huỷ thử thách sau một khoảng thời gian nhất định. Đừng bao giờ chấp nhận cùng một thử thách nhiều lần trong một câu trả lời.
  2. Đảm bảo thử thách được bảo mật theo cách mã hoá. Thử thách gần như không thể đoán được. Để tạo thử thách bảo mật bằng mật mã phía máy chủ, tốt nhất bạn nên sử dụng thư viện phía máy chủ FIDO mà bạn tin tưởng. Nếu bạn tự tạo thử thách của riêng mình, hãy sử dụng chức năng mật mã tích hợp có trong nhóm công nghệ hoặc tìm các thư viện được thiết kế cho các trường hợp sử dụng mã hoá. Ví dụ: iso-crypto trong Node.js hoặc secrets trong Python. Theo quy cách, thử thách phải dài ít nhất 16 byte để được coi là an toàn.

Sau khi bạn tạo thử thách, hãy lưu thử thách đó vào phiên của người dùng để xác minh sau.

Tạo tuỳ chọn yêu cầu thông tin xác thực

Tạo các tuỳ chọn yêu cầu thông tin xác thực dưới dạng đối tượng publicKeyCredentialRequestOptions.

Để 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 generateAuthenticationOptions.

publicKeyCredentialRequestOptions phải chứa tất cả thông tin cần thiết để xác thực khoá truy cập. Truyền thông tin này đến hàm trong thư viện phía máy chủ FIDO chịu trách nhiệm tạo đối tượng publicKeyCredentialRequestOptions.

Một số trường của publicKeyCredentialRequestOptions 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: Mã bên bị hạn chế mà bạn muốn liên kết với thông tin đăng nhập, ví dụ: example.com. Quá trình xác thực sẽ chỉ thành công nếu mã bên bị hạn chế mà bạn cung cấp ở đây khớp với mã RP được liên kết với thông tin xác thực. Để điền mã RP, hãy dùng cùng một giá trị với mã RP mà bạn đặt trong publicKeyCredentialCreationOptions trong quá trình đăng ký thông tin xác thực.
  • challenge: Một phần dữ liệu mà trình cung cấp khoá truy cập sẽ ký để chứng minh người dùng giữ khoá truy cập tại thời điểm yêu cầu xác thực. Xem thông tin chi tiết trong phần Tạo thử thách.
  • allowCredentials: Mảng gồm các thông tin xác thực được chấp nhận cho quy trình xác thực này. Truyền một mảng trống để cho phép người dùng chọn một khoá truy cập có sẵn trong danh sách do trình duyệt hiển thị. Hãy xem các bài viết Tìm nạp thử thách từ máy chủ bên bị hạn chếThông tin chuyên sâu về thông tin đăng nhập có thể phát hiện được để biết thông tin chi tiết.
  • userVerification: Cho biết việc xác minh người dùng bằng phương thức khoá màn hình thiết bị là "bắt buộc", "ưu tiên" hay "không nên chọn". Xem phần Tìm nạp thử thách qua máy chủ của bên bị hạn chế.
  • timeout: Thời gian (tính bằng mili giây) mà người dùng có thể mất để hoàn tất quy trình xác thực. Giá trị này phải hào phóng hợp lý và ngắn hơn thời gian tồn tại của challenge. Giá trị mặc định đề xuất là 5 phút, nhưng bạn có thể tăng giá trị này (tối đa 10 phút), vẫn nằm trong phạm vi đề xuất. Thời gian chờ dài sẽ hợp lý nếu bạn muốn người dùng sử dụng quy trình làm việc kết hợp, thường mất nhiều thời gian hơn một chút. Nếu thao tác hết thời gian chờ, hệ thống sẽ gửi NotAllowedError.

Sau khi bạn tạo publicKeyCredentialRequestOptions, hãy gửi nó cho ứng dụng.

PublicKeyCredentialCreationOptions do máy chủ gửi
Các tuỳ chọn do máy chủ gửi. Quá trình giải mã challenge diễn ra ở phía máy khách.

Mã ví dụ: tạo tuỳ chọn yêu cầu 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 cho yêu cầu thông tin xác thực sang hàm generateAuthenticationOptions.

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

router.post('/signinRequest', csrfCheck, async (req, res) => {

  // Ensure you nest 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 {
    // Use the generateAuthenticationOptions function from SimpleWebAuthn
    const options = await generateAuthenticationOptions({
      rpID: process.env.HOSTNAME,
      allowCredentials: [],
    });
    // Save the challenge in the user session
    req.session.challenge = options.challenge;

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

Xác minh và đăng nhập người dùng

Khi phân giải thành công trên máy khách, navigator.credentials.get sẽ trả về đối tượng PublicKeyCredential.

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

response là một AuthenticatorAssertionResponse. Mã này cho biết 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 khách nhằm tạo những việc cần thiết để thử và xác thực bằng khoá truy cập trên RP. Bảng này gồm có:

  • response.authenticatorDataresponse.clientDataJSON, chẳng hạn như ở bước đăng ký khoá truy cập.
  • response.signature chứa chữ ký trên các giá trị này.

Gửi đối tượng PublicKeyCredential đến máy chủ.

Trên máy chủ, hãy làm như sau:

Giản đồ cơ sở dữ liệu
Giản đồ cơ sở dữ liệu được đề xuất. Tìm hiểu thêm về thiết kế này trong phần Đăng ký khoá truy cập phía máy chủ.
  • Thu thập thông tin bạn cần để xác minh câu nhận định và xác thực người dùng:
    • Nhận thử thách dự kiến mà bạn đã lưu trữ trong phiên khi bạn tạo các tuỳ chọn xác thực.
    • Lấy nguồn gốc và mã RP dự kiến.
    • Tìm người dùng trong cơ sở dữ liệu của bạn. Trong trường hợp thông tin đăng nhập có thể phát hiện được, bạn sẽ không biết ai là người dùng đưa ra yêu cầu xác thực. Bạn có hai lựa chọn để tìm hiểu xem:
      • Cách 1: Sử dụng response.userHandle trong đối tượng PublicKeyCredential. Trong bảng Người dùng, hãy tìm passkey_user_id khớp với userHandle.
      • Lựa chọn 2: Sử dụng thông tin xác thực id có trong đối tượng PublicKeyCredential. Trong bảng Thông tin xác thực của khoá công khai, hãy tìm thông tin xác thực id khớp với thông tin xác thực id có trong đối tượng PublicKeyCredential. Sau đó, hãy tìm người dùng tương ứng bằng cách sử dụng khoá ngoại passkey_user_id cho bảng Người dùng.
    • Tìm trong cơ sở dữ liệu của bạn thông tin về thông tin xác thực khoá công khai khớp với thông tin xác nhận mà bạn đã nhận được. Để thực hiện việc này, trong bảng Thông tin xác thực khoá công khai, hãy tìm thông tin đăng nhập id khớp với thông tin xác thực idcó trong đối tượng PublicKeyCredential.
  • Xác minh câu nhận định xác thực. 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ư viện này thường sẽ cung cấp hàm hiệu dụng cho mục đích này. Ví dụ: SimpleWebAuthn cung cấp verifyAuthenticationResponse. Tìm hiểu những gì đang xảy ra trong Phụ lục: xác minh phản hồi xác thực.

  • Xoá thử thách xác minh có thành công hay không để ngăn chặn các cuộc tấn công phát lại.

  • Đăng nhập người dùng. Nếu quá trình xác minh thành công, hãy cập nhật thông tin về phiên để đánh dấu người dùng là đã đăng nhập. Bạn cũng có thể muốn trả về một đối tượng user cho ứng dụng khách để giao diện người dùng có thể sử dụng thông tin liên kết với người dùng mới đăng nhập.

Mã ví dụ: xác minh và đăng nhập người dùng

Chúng ta đang sử dụng thư viện SimpleWebAuthn trong các ví dụ. Ở đây, chúng ta sẽ chuyển thông tin xác minh của phản hồi xác thực cho hàm verifyAuthenticationResponse.

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

router.post('/signinResponse', csrfCheck, async (req, res) => {
  const response = req.body;
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;

  // 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 {
    // Find the credential stored to the database by the credential ID
    const cred = Credentials.findById(response.id);
    if (!cred) {
      throw new Error('Credential not found.');
    }
    // Find the user - Here alternatively we could look up the user directly
    // in the Users table via userHandle
    const user = Users.findByPasskeyUserId(cred.passkey_user_id);
    if (!user) {
      throw new Error('User not found.');
    }
    // Base64URL decode some values
    const authenticator = {
      credentialPublicKey: isoBase64URL.toBuffer(cred.publicKey),
      credentialID: isoBase64URL.toBuffer(cred.id),
      transports: cred.transports,
    };

    // Verify the credential
    const { verified, authenticationInfo } = await verifyAuthenticationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      authenticator,
      requireUserVerification: false,
    });

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

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

    req.session.username = user.username;
    req.session['signed-in'] = 'yes';

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

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

Phụ lục: xác minh phản hồi xác thực

Quy trình xác minh phản hồi xác thực 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 khớp với nguồn gốc đăng nhập của trang web. Đối với ứng dụng Android, hãy xem bài viết Xác minh nguồn gốc.
  • Kiểm tra để đảm bảo thiết bị có thể thực hiện thử thách mà bạn đã đưa ra.
  • Xác minh rằng trong quá trình xác thực, người dùng đã tuân thủ các yêu cầu mà bạn uỷ thác với tư cách là bên bị hạn chế (RP). Nếu bạn yêu cầu xác minh người dùng, hãy đảm bảo rằng cờ uv (được người dùng xác minh) trong authenticatorDatatrue. Kiểm tra để đảm bảo cờ up (có người dùng) trong authenticatorDatatrue, vì khoá truy cập luôn bắt buộc phải có người dùng.
  • Xác minh chữ ký. Để xác minh chữ ký này, bạn cần:
    • Chữ ký chính là yêu cầu xác thực có chữ ký: response.signature
    • Khoá công khai để xác minh chữ ký.
    • Dữ liệu đã ký ban đầu. Đây là dữ liệu có chữ ký cần được xác minh.
    • Thuật toán mật mã được dùng để tạo chữ ký.

Để tìm hiểu thêm về các bước này, hãy kiểm tra mã nguồn của SimpleWebAuthn cho verifyAuthenticationResponse hoặc tìm hiểu danh sách đầy đủ các quy trình xác minh trong quy cách.