การตรวจสอบสิทธิ์ด้วยพาสคีย์

คู่มือนี้จะอธิบายรายละเอียดวิธีใช้การตรวจสอบสิทธิ์ผู้ใช้ที่กลับมาโดยใช้ข้อมูลเข้าสู่ระบบที่ค้นพบได้ ทั้งผ่านทริกเกอร์ปุ่มที่ชัดเจนและคำแนะนำการป้อนข้อความอัตโนมัติในเบราว์เซอร์ที่ราบรื่น (UI แบบมีเงื่อนไข)

ฝั่งเซิร์ฟเวอร์

การสร้างตัวเลือก

สร้างปลายทางที่สร้างพารามิเตอร์คำขอ WebAuthn โดยใช้ไลบรารีที่ตรวจสอบแล้วตามมาตรฐาน

  1. ใช้รหัส RP ที่กำหนดไว้ล่วงหน้า: ใช้รหัส RP ที่เหมาะสมซึ่งกำหนดไว้ล่วงหน้าเป็นสตริงคงที่
  2. สร้างคำถาม: สร้างบัฟเฟอร์แบบสุ่มที่มีเอนโทรปีสูงและปลอดภัยในการเข้ารหัส จัดเก็บไว้อย่างปลอดภัยในเซสชันของผู้ใช้ และเข้ารหัสเป็น Base64URL
  3. การแมปข้อมูลเข้าสู่ระบบที่ค้นพบได้: ระบุอาร์เรย์ที่ว่างเปล่า [] สำหรับ allowCredentials ซึ่งจะขอข้อมูลเข้าสู่ระบบที่ค้นพบได้ หมายความว่าผู้ใช้ไม่จำเป็นต้องป้อนชื่อผู้ใช้ก่อน ผู้ให้บริการพาสคีย์จะแสดงบัญชีที่พร้อมใช้งาน
  4. ระดับการยืนยันตัวตนของผู้ใช้: ตั้งค่าเป็น userVerification: "preferred" (หรือ "required" หากนโยบายการปฏิบัติตามข้อกำหนดขององค์กรกำหนดไว้อย่างชัดเจน)
    • ระดับข้อจำกัด userVerification ที่ขอต้องคงอยู่ในระเบียนเซสชันของเซิร์ฟเวอร์ที่ปลายทางตัวเลือก แทนที่จะส่งกลับจากไคลเอ็นต์ผ่านสตริงการค้นหา ซึ่งช่วยให้ปลายทางการยืนยันบังคับใช้ข้อจำกัดการจับคู่ที่เข้มงวดได้อย่างปลอดภัยโดยไม่มีความเสี่ยงที่ไคลเอ็นต์จะถูกบิดเบือนข้อมูล
// 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";

ปลายทางการยืนยัน

ยืนยันการยืนยันที่ไคลเอ็นต์แสดงผลอย่างปลอดภัยเพื่อตรวจสอบสิทธิ์ผู้ใช้ โดยทำดังนี้

  1. ตรวจสอบคำถามของเซสชัน: บังคับใช้การจับคู่คำถามที่เข้มงวดระหว่างการตอบกลับของไคลเอ็นต์กับคำถามที่คาดไว้ซึ่งจัดเก็บไว้ในเซสชัน
  2. บังคับใช้ค่ากำหนด UV:
    • อนุญาตให้ใช้เครื่องมือตรวจสอบสิทธิ์ที่ไม่มี UV (เช่น ปิดใช้การล็อกหน้าจอของเครื่องมือตรวจสอบสิทธิ์) หาก expectedUserVerification ของเซสชันขอ "preferred" โดยส่ง requireUserVerification: false ไปยังไลบรารีการยืนยันฝั่งเซิร์ฟเวอร์ หากมีการขอ "required" ให้บังคับใช้การป้อนข้อมูลไบโอเมตริก/PIN อย่างเคร่งครัด
  3. ข้อผิดพลาดเกี่ยวกับเซิร์ฟเวอร์ 404: หากไม่พบรหัสข้อมูลเข้าสู่ระบบที่ไคลเอ็นต์แสดงในฐานข้อมูล ให้แสดงข้อผิดพลาด HTTP 404 เพื่อให้ไคลเอ็นต์ทริกเกอร์ Signal API ได้

ตรรกะฝั่งไคลเอ็นต์

คำอธิบายประกอบแบบฟอร์ม HTML

ใส่คำอธิบายประกอบอินพุตชื่อผู้ใช้และรหัสผ่านเพื่อใช้ประโยชน์จาก UI แบบมีเงื่อนไขในตัว โทเค็นการเติมข้อความอัตโนมัติจะรวมพารามิเตอร์ข้อกำหนดเฉพาะของ WebAuthn และการโฟกัสอัตโนมัติจะทริกเกอร์ป๊อปอัปการป้อนข้อความอัตโนมัติของเบราว์เซอร์ทันทีเมื่อมีการโฟกัสอินพุต

<!-- 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>

ขั้นตอนการใช้ปุ่มที่ชัดเจน

ทริกเกอร์การตรวจสอบสิทธิ์ด้วยพาสคีย์เมื่อผู้ใช้คลิกปุ่ม "ลงชื่อเข้าใช้ด้วยพาสคีย์" ยกเลิกการเรียกใช้การป้อนข้อความอัตโนมัติในแบบฟอร์ม (Conditional Get) ที่ดำเนินอยู่ก่อนที่จะเรียกใช้ข้อความแจ้งพาสคีย์

ขั้นตอนการสื่อกลางแบบมีเงื่อนไข (การป้อนข้อความอัตโนมัติในแบบฟอร์ม)

เปิดใช้งานคำแนะนำการป้อนข้อความอัตโนมัติในแบบฟอร์มเมื่อโหลดหน้าเว็บเพื่อเสนอการตรวจสอบสิทธิ์ด้วยพาสคีย์โดยตรงเมื่อผู้ใช้โฟกัสที่ช่องลงชื่อเข้าใช้

  1. ตรวจหาฟีเจอร์: เรียกใช้ PublicKeyCredential.getClientCapabilities() เมื่อโหลดหน้าเว็บ และข้ามการลงชื่อเข้าใช้ด้วยพาสคีย์หาก conditionalGet ไม่พร้อมใช้งาน
  2. ตัวเลือกการถอดรหัส: ถอดรหัสออบเจ็กต์ JSON ของข้อมูลเข้าสู่ระบบที่ดึงมาด้วย PublicKeyCredential.parseRequestOptionsFromJSON()
  3. เรียกใช้ GET แบบมีเงื่อนไข: เรียก navigator.credentials.get() ด้วย mediation: "conditional" และส่งสัญญาณ AbortController ซึ่งจะลงทะเบียนการป้อนข้อความอัตโนมัติโดยไม่มีเสียงและไม่แสดงกล่องโต้ตอบพาสคีย์
  4. การแยกข้อยกเว้น Try/Catch: รวมการเรียก navigator.credentials.get ไว้ในบล็อก Try/Catch:
    • NotAllowedError: ผู้ใช้ยกเลิกหรือหมดเวลาในข้อความแจ้งให้เข้าสู่ระบบด้วยพาสคีย์
    • AbortError: คำขอตรวจสอบสิทธิ์ถูกยกเลิกโดยใช้โปรแกรม
  5. Call Signal API: รวมการเรียก fetch() การยืนยันเซิร์ฟเวอร์ไว้ในบล็อก try/catch ดังนี้
    • แสดงข้อความแสดงข้อผิดพลาดเพื่อให้ผู้ใช้เข้าใจว่าเกิดข้อผิดพลาดอะไรขึ้น
    • เรียกใช้ signalUnknownCredential() เฉพาะเมื่อเซิร์ฟเวอร์ตอบกลับอย่างชัดเจนด้วยสถานะ HTTP 404 (ไม่พบข้อมูลเข้าสู่ระบบ) และผู้ใช้ไม่ได้รับการตรวจสอบสิทธิ์
    • พารามิเตอร์ credentialId ที่ส่งไปยัง signalUnknownCredential() ต้องเป็นสตริงรหัสข้อมูลเข้าสู่ระบบที่เข้ารหัส Base64URL อย่างเคร่งครัด (เช่น encoded.id) ไม่ใช่ออบเจ็กต์ ArrayBuffer ดิบ credential.rawId
  6. เข้ารหัสการตอบกลับ: เข้ารหัสข้อมูลเข้าสู่ระบบ AuthenticatorAssertionResponse ด้วย .toJSON() ก่อนส่งไปยังเซิร์ฟเวอร์เพื่อยืนยัน
// 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);

กลยุทธ์สำรอง

ฟีเจอร์สำรองสำหรับการตรวจหาฟีเจอร์พาสคีย์

ติดตั้ง "webauthn-polyfills" และนำเข้าในบริบทเสมอ พิจารณาว่าตราบใดที่ระบบรองรับ PublicKeyCredential ระบบก็จะรองรับ PublicKeyCredential.getClientCapabilities ด้วย

import 'webauthn-polyfills';

การซิงค์ API ของ Signal สำรอง

WebAuthn Signal API (webauthn-signals) เป็นการเพิ่มประสิทธิภาพแบบต่อเนื่องที่ใช้เพื่อซิงค์เครื่องมือจัดการรหัสผ่านกับสถานะข้อมูลเข้าสู่ระบบของเซิร์ฟเวอร์

  • ประสบการณ์สำรอง: จำกัดการเข้าถึงผ่าน if (PublicKeyCredential.signalUnknownCredential) หากไม่รองรับ ระบบจะข้ามการซิงค์การยืนยันในเบื้องหลังอย่างราบรื่นโดยไม่แสดงข้อยกเว้นของเบราว์เซอร์

Easy JSON Serialization Fallback

ติดตั้ง "webauthn-polyfills" และนำเข้าในบริบทเสมอ โปรดทราบว่าตราบใดที่ระบบรองรับ PublicKeyCredential ระบบก็จะรองรับ PublicKeyCredential.parseRequestOptionsFromJSON และ PublicKeyCredential.prototype.toJSON ด้วย

import 'webauthn-polyfills';