Đăng ký khoá truy cập

Hướng dẫn này trình bày chi tiết cách cho phép người dùng đăng ký khoá truy cập cho tài khoản của họ, cung cấp một giải pháp thay thế đăng nhập không cần mật khẩu có độ bảo mật cao, chống lừa đảo.

Yêu cầu về cơ sở dữ liệu

Để hỗ trợ việc đăng ký khoá truy cập, bảng thông tin xác thực trong cơ sở dữ liệu của bạn phải lưu trữ các trường sau:

export interface StoredPasskeyCredential {
  id: string; // Base64URL-encoded credential ID (unique lookup key)
  passkeyUserId: string; // Associated application user ID
  credentialPublicKey: string; // Base64URL-encoded public key used to verify assertion signatures
  credentialType: "public-key";
  credentialDeviceType: "singleDevice" | "multiDevice"; // Helps distinguish device-bound vs cloud-synced passkeys
  credentialBackedUp: boolean; // Boolean backup state reported by the authenticator
  aaguid: string; // Authenticator Attestation GUID
  providerIcon?: string; // Provider icon derived from the AAGUID registry (dark or light theme URLs)
  name: string; // Provider name derived from AAGUID registry
  transports: string[]; // Array of transport names (e.g. 'internal', 'hybrid') necessary for exclusion options
  lastUsedAt?: number; // Optional epoch timestamp of last sign-in
  registeredAt: number; // Registration epoch timestamp
  counter: number; // Authenticator sign-in signature counter used to prevent replay attacks
}

Phía máy chủ

Tạo lựa chọn

Tạo một điểm cuối để tạo các tham số tạo WebAuthn. Sử dụng thư viện đã được kiểm tra theo tiêu chuẩn của danh mục thay vì tự triển khai mật mã.

  1. Sử dụng mã nhận dạng bên tin cậy được xác định trước: Sử dụng mã nhận dạng bên tin cậy thích hợp được xác định trước làm chuỗi hằng số.
  2. Tạo một thử thách an toàn: Tạo một vùng đệm ngẫu nhiên có độ hỗn loạn cao, bảo mật bằng mật mã trên máy chủ, lưu trữ an toàn trong phiên của người dùng và mã hoá dưới dạng Base64URL để phân phối các tuỳ chọn.
  3. Tránh khoá truy cập trùng lặp: Liên kết mã thông tin xác thực đã đăng ký trước của người dùng với mảng tuỳ chọn excludeCredentials. Điều này ngăn trình xác thực đăng ký thông tin xác thực trùng lặp trên cùng một tài khoản nhà cung cấp khoá truy cập.
  4. Thực thi thông tin xác thực có thể phát hiện: Đặt requireResidentKey: true và residentKey: "required" trong các tuỳ chọn authenticatorSelection để yêu cầu thông tin xác thực có thể phát hiện, điều này là cần thiết cho việc đăng nhập có thể phát hiện.
  5. Định cấu hình tính năng Xác minh người dùng: Chỉ định userVerification: "preferred" hoặc userVerification: "required". Nhiều trường hợp sử dụng liên quan đến việc tuân thủ (ví dụ: tài chính, chăm sóc sức khoẻ) yêu cầu 'required' phải thực thi quy trình xác minh người dùng khi tạo.
  6. Xác định phạm vi tệp đính kèm:
    • Quy trình quảng bá: Khi đề xuất tạo khoá truy cập ngay sau khi đăng nhập bằng mật khẩu chuẩn hoặc quảng bá sau khi đăng ký, hãy đặt authenticatorAttachment: "platform" để thực thi trình xác thực nền tảng và bỏ qua lời nhắc khoá bảo mật bên ngoài.
    • Quy trình quản lý: Khi được gọi từ một bảng điều khiển bảo mật hoặc chế độ cài đặt chuyên dụng, nơi hỗ trợ khoá bảo mật bên ngoài ngoài trình xác thực nền tảng, hãy bỏ qua hoàn toàn thuộc tính authenticatorAttachment.
    • Mẹo: Chấp nhận cờ yêu cầu promotion: boolean để xử lý có điều kiện cả hai luồng bằng một điểm cuối duy nhất.
// Options generation example
const options = {
  challenge: serverGeneratedBase64UrlChallenge, // Cryptographically random challenge
  rp: { id: "example.com", name: "Secure Application" },
  user: {
    id: userBase64UrlId, // Unique base64url string identifying the account
    name: "user@example.com",
    displayName: "Jane Doe",
  },
  pubKeyCredParams: [
    {
      type: "public-key",
      alg: -7,
    },
    {
      type: "public-key",
      alg: -257,
    },
  ],
  excludeCredentials: userExistingCredentials.map((cred) => ({
    type: "public-key",
    id: cred.id,
    transports: cred.transports,
  })),
  authenticatorSelection: {
    residentKey: "required",
    requireResidentKey: true,
    userVerification: "preferred",
    ...(isPromotionFlow && { authenticatorAttachment: "platform" }),
  },
};

Xác minh

  1. Xác minh thử thách: Xác minh thử thách một cách an toàn dựa trên thử thách dự kiến liên kết với phiên.
  2. Xác minh sự hiện diện của người dùng:
    • Đảm bảo rằng cờ Người dùng hiện diện (UP) được trả về trong dữ liệu trình xác thực đã phân tích cú pháp là true để xác nhận sự hiện diện thực tế của người dùng tại thời điểm tạo.
  3. Nới lỏng quy trình xác minh cho "preferred":
    • Khi các tuỳ chọn tạo được chỉ định userVerification: "preferred", lệnh gọi xác minh phía máy chủ PHẢI được định cấu hình bằng requireUserVerification: false. Nếu không, trình xác thực đăng ký mà không có quy trình xác minh người dùng (ví dụ: tắt phương thức khoá màn hình) sẽ kích hoạt lỗi xác minh máy chủ giả mạo.

Logic phía máy khách

  1. Kiểm soát giao diện người dùng khi tải trang:
    • Khi tải trang, hãy gọi PublicKeyCredential.getClientCapabilities() và tắt nút "Tạo khoá truy cập" nếu không có conditionalGet hoặc passkeyPlatformAuthenticator.
  2. Gọi lệnh tạo và chuyển đổi tuần tự: Giải mã các tuỳ chọn máy chủ bằng PublicKeyCredential.parseCreationOptionsFromJSON() và truyền cấu hình thu được đến navigator.credentials.create().
    • Gọi credential.toJSON() để mã hoá AuthenticatorAttestationResponse thành một đối tượng hợp lệ, có thể chuyển đổi tuần tự JSON trước khi tìm nạp điểm cuối xác minh.
  3. Xử lý các trường hợp ngoại lệ của WebAuthn:
    • InvalidStateError: Đã có khoá truy cập trùng khớp (do excludeCredentials so khớp).
    • NotAllowedError: Người dùng đã huỷ hoặc hết thời gian chờ hộp thoại xác thực khoá truy cập.
    • AbortError: Thao tác đã bị huỷ.
    • SecurityError: Lỗi nguồn gốc bảo mật (HTTPS) hoặc lỗi không khớp mã nhận dạng bên tin cậy (vấn đề về cấu hình).
  4. Phân tách Try/Catch cho Signal API:
    • Gói lệnh gọi xác minh máy chủ fetch() trong một khối try/catch. Gọi signalUnknownCredential() khi không tìm nạp được thông tin xác minh máy chủ (bất kỳ trạng thái response.ok === false hoặc lỗi mạng nào).
// optionsFetch and registerVerifyFetch are app-defined HTTP methods
import { optionsFetch, registerVerifyFetch } from "./api.js";

async function registerPasskey(isPromotion = false) {
  // Verify passkey capability and conditional UI are available
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (
    !capabilities.passkeyPlatformAuthenticator ||
    !capabilities.conditionalGet
  ) {
    // Hide "Create passkey" buttons and fall back to password flows instead
    showStandardPasswordFallbackUI();
    return;
  }

  const creationOptionsJSON = await optionsFetch({ promotion: isPromotion });
  const publicKey =
    PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);

  let credential;
  try {
    // passkey prompt execution
    credential = await navigator.credentials.create({ publicKey });
  } catch (err) {
    if (err.name === "InvalidStateError") {
      console.log("A passkey already exists for this account.");
      alert("A passkey already exists for this account.");
    } else if (err.name === "SecurityError") {
      console.error("Configuration RP ID or Secure Context error.");
      alert("Configuration RP ID or Secure Context error.");
    } else if (err.name === "NotAllowedError") {
      console.log("User cancelled the passkey dialog.");
    } else if (err.name === "AbortError") {
      console.log("The creation operation has been aborted.");
    }
    return; // Safe API exit, do not signal unknown for standard WebAuthn cancels
  }

  // Server Verification phase (Segregated Try/Catch)
  let encodedResponse = credential.toJSON();
  try {
    const response = await registerVerifyFetch(encodedResponse);
    if (!response.ok) {
      // Server verification failed to verify/authenticate the credential (orphaned)
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encodedResponse.id, // Base64URL-encoded credential ID
      });
    }
  } catch (serverErr) {
    console.error("Server verification network failure:", serverErr);
    await publickeycredential.signalunknowncredential({
      rpId, // RP ID must match the one defined on the server
      credentialid: encodedresponse.id, // base64url-encoded credential id
    });
  }
}

Chiến lược dự phòng

Dự phòng đồng bộ hoá API tín hiệu

WebAuthn Signal API (webauthn-signals) là một tính năng tối ưu hoá tăng dần dùng để đồng bộ hoá trình quản lý mật khẩu với trạng thái thông tin xác thực của máy chủ.

  • Trải nghiệm dự phòng: Nếu trình duyệt không hỗ trợ PublicKeyCredential.signalUnknownCredential, thì lệnh gọi PHẢI được bỏ qua một cách an toàn thông qua cổng phát hiện tính năng (if (PublicKeyCredential.signalUnknownCredential)) và quy trình xác minh phía máy chủ chỉ ghi lại lỗi mà không kích hoạt bản cập nhật của trình quản lý.

Dự phòng chuyển đổi tuần tự JSON dễ dàng

Luôn cài đặt "webauthn-polyfills" và nhập vào ngữ cảnh. Hãy xem xét miễn là PublicKeyCredential được hỗ trợ, PublicKeyCredential.parseCreationOptionsFromJSON và PublicKeyCredential.prototype.toJSON cũng được hỗ trợ.

import 'webauthn-polyfills';