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

Tổng quan

Dưới đây là thông tin tổng quan cấp cao về các bước quan trọng trong quy trình xác thực bằng khoá truy cập:

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

  • Xác định yêu cầu 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 máy khách để bạn có thể chuyển chúng đến lệnh gọi xác thực 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 chứa xác nhận xác thực.
  • Xác minh câu nhận định xác thực.
  • Nếu câu nhận định xác thực 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 cụ thể 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 mục đích, bạn phải:

  1. Đảm bảo thử thách giống nhau không bao giờ được sử dụng nhiều lần. Tạo thử thách mới mỗi lần đăng nhập. Loại bỏ thử thách sau mỗi lần đăng nhập, cho dù thử thách đó thành công hay không thành công. Bạn cũng có thể loại bỏ thử thách sau một khoảng thời gian nhất định. Không được 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 bằng mật mã. Trên thực tế, thử thách khó đoán ra được. Để tạo một thư viện phía máy chủ FIDO mà bạn tin tưởng, để tạo một thử thách được bảo mật bằng mật mã phía máy chủ, tốt nhất là bạn nên sử dụng một thư viện phía máy chủ FIDO. 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 sẵn 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 secret trong Python. Theo thông số kỹ thuật, thử thách phải dài ít nhất 16 byte để được coi là an toàn.

Sau khi bạn tạo một 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 các 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 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ư 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 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 publicKeyCredentialRequestOptions.

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

  • 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 đề xuất. Tìm hiểu thêm về thiết kế này trong bài viết Đă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 tùy chọn xác thực.
    • Lấy nguồn gốc và mã nhận dạng bên bị hạn chế dự kiến.
    • Tìm trong cơ sở dữ liệu của bạn về người dùng. Trong trường hợp thông tin xác thực có thể tìm được, bạn không biết người dùng gửi yêu cầu xác thực là ai. Để tìm hiểu, bạn có 2 lựa chọn:
      • 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 đăng nhập id có trong đối tượng PublicKeyCredential. Trong bảng Thông tin xác thực 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 thông tin đăng nhập khoá công khai khớp với câu nhận định xác thực 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 xác thực id khớp với thông tin đăng nhập 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 một chức năng tiện ích cho mục đích này. SimpleWebAuthn cung cấp, chẳng hạn như verifyAuthenticationResponse. 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 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 vào tài khoản người dùng. Nếu xác minh thành công, hãy cập nhật thông tin phiên để đánh dấu người dùng là đã đăng nhập. Bạn cũng nên trả về đối tượng user cho máy 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 dùng thư viện SimpleWebAuthn trong các ví dụ. Ở đây, chúng ta sẽ chuyển giao kết quả xác minh 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:

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