Xác thực bằng khoá truy cập

Hướng dẫn này trình bày chi tiết cách triển khai quy trình xác thực người dùng cũ bằng thông tin xác thực có thể phát hiện, thông qua cả trình kích hoạt nút rõ ràng và đề xuất tự động điền liền mạch của trình duyệt (Giao diện người dùng có điều kiện).

Phía máy chủ

Tạo lựa chọn

Tạo một điểm cuối tạo ra các tham số yêu cầu WebAuthn bằng cách sử dụng thư viện đã được kiểm tra theo tiêu chuẩn.

  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 thử thách: 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ã, lưu trữ an toàn trong phiên của người dùng và mã hoá dưới dạng Base64URL.
  3. Ánh xạ thông tin xác thực có thể phát hiện: Chỉ định một mảng trống [] cho allowCredentials. Thao tác này yêu cầu thông tin xác thực có thể phát hiện, nghĩa là người dùng không cần nhập tên người dùng trước; nhà cung cấp khoá truy cập sẽ hiển thị các tài khoản có sẵn.
  4. Cấp độ xác minh người dùng: Đặt userVerification: "preferred" (hoặc "required" nếu chính sách tuân thủ của công ty yêu cầu rõ ràng).
    • Cấp độ ràng buộc userVerification được yêu cầu PHẢI được duy trì bên trong bản ghi phiên máy chủ tại điểm cuối tuỳ chọn, thay vì được trả về từ ứng dụng khách thông qua chuỗi truy vấn. Điều này cho phép điểm cuối xác minh thực thi các quy tắc ràng buộc nghiêm ngặt về việc so khớp một cách an toàn mà không có nguy cơ bị thao túng ứng dụng.
// Options generation example (discoverable flow)
const options = {
  challenge: serverGeneratedBase64UrlChallenge, // High-entropy random challenge stored in session
  rpId: "example.com",
  allowCredentials: [], // Request discoverable passkeys
  userVerification: "preferred",
};

// Persist expected UV level to user session
req.session.expectedUserVerification = "preferred";

Điểm cuối xác minh

Xác minh một cách an toàn câu nhận định do ứng dụng trả về để xác thực người dùng:

  1. Xác thực thử thách phiên: Thực thi việc so khớp thử thách nghiêm ngặt giữa phản hồi của ứng dụng và thử thách dự kiến được lưu trữ trong phiên.
  2. Thực thi lựa chọn ưu tiên về UV:
    • Cho phép trình xác thực không có UV (ví dụ: trình xác thực đã tắt phương thức khoá màn hình) nếu expectedUserVerification của phiên yêu cầu "preferred", bằng cách truyền requireUserVerification: false đến thư viện xác minh phía máy chủ. Nếu được yêu cầu "required", hãy thực thi nghiêm ngặt việc nhập dữ liệu sinh trắc học/mã PIN.
  3. Lỗi máy chủ sạch 404: Nếu không tìm thấy mã thông tin xác thực do ứng dụng trả về trong cơ sở dữ liệu, hãy trả về lỗi HTTP 404 rõ ràng để ứng dụng có thể kích hoạt Signal API.

Logic phía máy khách

Chú thích biểu mẫu HTML

Chú thích dữ liệu đầu vào tên người dùng và mật khẩu để tận dụng giao diện người dùng có điều kiện theo cách gốc. Mã thông báo tự động hoàn thành kết hợp các tham số thông số kỹ thuật webauthn và tính năng tự động lấy tiêu điểm sẽ kích hoạt cửa sổ bật lên tự động điền của trình duyệt ngay khi dữ liệu đầu vào được lấy tiêu điểm.

<!-- Autocomplete tokens must contain webauthn space-separated -->
<form id="signin-form">
  <input
    type="text"
    name="username"
    autocomplete="username webauthn"
    autofocus
    data-testid="username-field"
  />
  <input type="password" name="password" autocomplete="current-password" />
  <button type="submit">Sign in</button>
</form>

Quy trình nút rõ ràng

Kích hoạt quy trình xác thực bằng khoá truy cập khi người dùng nhấp vào nút "Đăng nhập bằng khoá truy cập". Huỷ mọi lệnh gọi tự động điền biểu mẫu đang diễn ra (Conditional Get) trước khi gọi lời nhắc khoá truy cập.

Quy trình dàn xếp có điều kiện (Tự động điền biểu mẫu)

Kích hoạt đề xuất tự động điền biểu mẫu khi tải trang để cung cấp tính năng xác thực bằng khoá truy cập ngay khi người dùng nhấp vào các trường đăng nhập:

  1. Phát hiện tính năng: Gọi PublicKeyCredential.getClientCapabilities() khi tải trang và bỏ qua bước đăng nhập bằng khoá truy cập nếu không có conditionalGet.
  2. Tuỳ chọn giải mã: Giải mã đối tượng JSON thông tin xác thực đã tìm nạp bằng PublicKeyCredential.parseRequestOptionsFromJSON().
  3. Gọi hàm Get có điều kiện: Gọi navigator.credentials.get() bằng mediation: "conditional" và truyền tín hiệu AbortController. Thao tác này sẽ đăng ký tính năng tự động điền một cách thầm lặng mà không hiển thị hộp thoại khoá truy cập.
  4. Phân tách ngoại lệ Try/Catch: Gói lệnh gọi navigator.credentials.get trong khối try/catch:
    • NotAllowedError: Người dùng đã huỷ hoặc hết thời gian chờ lời nhắc đăng nhập bằng khoá truy cập.
    • AbortError: Yêu cầu xác thực đã bị huỷ theo chương trình.
  5. Gọi Signal API: Gói lệnh gọi xác minh máy chủ fetch() trong một khối try/catch:
    • Hiện thông báo lỗi để người dùng hiểu được vấn đề đã xảy ra.
    • CHỈ gọi signalUnknownCredential() khi máy chủ phản hồi rõ ràng bằng trạng thái HTTP 404 (Không tìm thấy thông tin xác thực) và người dùng chưa được xác thực.
    • Tham số credentialId được truyền đến signalUnknownCredential() PHẢI là chuỗi mã thông tin xác thực được mã hoá Base64URL (ví dụ: encoded.id), KHÔNG PHẢI là đối tượng ArrayBuffer thô credential.rawId.
  6. Mã hoá phản hồi: Mã hoá thông tin xác thực AuthenticatorAssertionResponse bằng .toJSON() trước khi gửi thông tin đó đến máy chủ để xác minh.
// optionsFetch and loginVerifyFetch are app-defined HTTP methods
import { optionsFetch, loginVerifyFetch } from "./api.js";

let autofillAbortController = new AbortController();

async function initializeConditionalAutofill() {
  // Feature detect Conditional Get autofill support
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (capabilities.conditionalGet === true) {
    const loginOptionsJSON = await optionsFetch();
    const publicKey =
      PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

    try {
      // Initiate Conditional UI form autofill suggestions
      const credential = await navigator.credentials.get({
        publicKey,
        signal: autofillAbortController.signal,
        mediation: "conditional",
      });

      // Segregated verification fetch
      const encoded = credential.toJSON();
      const response = await loginVerifyFetch(encoded);
      if (!response.ok && response.status === 404) {
        // Note: this code path runs pre-authentication, satisfying the unauth precondition
        if (PublicKeyCredential.signalUnknownCredential) {
          await PublicKeyCredential.signalUnknownCredential({
            rpId, // RP ID must match the one defined on the server
            credentialId: encoded.id,
          });
        }
      }
    } catch (err) {
      // Silently swallow expected client WebAuthn exceptions
      if (["NotAllowedError", "AbortError"].includes(err.name)) {
        return;
      }
      console.error("Unexpected conditional get error:", err);
    }
  }
}

async function triggerButtonAuthentication() {
  // Abort any pending Conditional Get call to prevent passkey prompt collisions
  autofillAbortController.abort();
  autofillAbortController = new AbortController(); // Reset controller for next triggers

  const loginOptionsJSON = await optionsFetch();
  const publicKey =
    PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

  let credential;
  try {
    // Passkey explicit prompt trigger
    credential = await navigator.credentials.get({
      publicKey,
      signal: autofillAbortController.signal,
    });
  } catch (err) {
    if (err.name === "NotAllowedError") {
      console.log("User cancelled passkey login.");
    } else if (err.name === "AbortError") {
      console.log("The authentication operation was aborted.");
    }
    // Re-arm Conditional autofill Suggestions after cancelled explicit button prompts
    initializeConditionalAutofill();
    return; // Safe exit
  }

  // Segregated verification try/catch (HTTP 404 trigger)
  const encoded = credential.toJSON();
  try {
    const response = await loginVerifyFetch(encoded);
    if (!response.ok && response.status === 404) {
      // Note: this code path runs pre-authentication, satisfying the unauth precondition
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encoded.id, // Base64URL-encoded credential ID
      });
    }
  } catch (serverErr) {
    console.error("Verification request error:", serverErr);
  }
}

// Trigger Conditional Get on load
window.addEventListener("DOMContentLoaded", initializeConditionalAutofill);

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

Dự phòng phát hiện tính năng khoá truy cập

Luôn cài đặt "webauthn-polyfills" và nhập vào ngữ cảnh. Hãy cân nhắc rằng miễn là PublicKeyCredential được hỗ trợ, thì PublicKeyCredential.getClientCapabilities cũng được hỗ trợ.

import 'webauthn-polyfills';

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: Được kiểm soát thông qua if (PublicKeyCredential.signalUnknownCredential). Nếu không được hỗ trợ, quá trình đồng bộ hoá xác minh trong nền sẽ được bỏ qua một cách linh hoạt mà không gửi ngoại lệ của trình duyệt.

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.parseRequestOptionsFromJSON và PublicKeyCredential.prototype.toJSON cũng được hỗ trợ.

import 'webauthn-polyfills';