การลงทะเบียนพาสคีย์

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

ข้อกำหนดของฐานข้อมูล

ตารางข้อมูลเข้าสู่ระบบของฐานข้อมูลต้องจัดเก็บฟิลด์ต่อไปนี้เพื่อรองรับการลงทะเบียนพาสคีย์

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
}

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

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

สร้างปลายทางที่สร้างพารามิเตอร์การสร้าง WebAuthn ใช้ไลบรารีที่ผ่านการตรวจสอบตามมาตรฐานหมวดหมู่แทนการเข้ารหัสด้วยตนเอง

  1. ใช้รหัส RP ที่กำหนดไว้ล่วงหน้า: ใช้รหัส RP ที่เหมาะสมซึ่งกำหนดไว้ล่วงหน้าเป็นสตริงคงที่
  2. สร้างคำถามที่ปลอดภัย: สร้างบัฟเฟอร์แบบสุ่มที่มีเอนโทรปีสูงและปลอดภัยด้วยการเข้ารหัสบนเซิร์ฟเวอร์ จัดเก็บไว้อย่างปลอดภัยในเซสชันของผู้ใช้ และเข้ารหัสเป็น Base64URL สำหรับการส่งตัวเลือก
  3. หลีกเลี่ยงพาสคีย์ที่ซ้ำกัน: แมปรหัสข้อมูลเข้าสู่ระบบที่ลงทะเบียนไว้ล่วงหน้าที่มีอยู่ของผู้ใช้กับอาร์เรย์ตัวเลือก excludeCredentials ซึ่งจะป้องกันไม่ให้ Authenticator ลงทะเบียนข้อมูลเข้าสู่ระบบซ้ำในบัญชีผู้ให้บริการพาสคีย์เดียวกัน
  4. บังคับใช้ข้อมูลเข้าสู่ระบบที่ค้นพบได้: ตั้งค่า requireResidentKey: true และ residentKey: "required" ในตัวเลือก authenticatorSelection เพื่อขอข้อมูลเข้าสู่ระบบที่ค้นพบได้ ซึ่งจำเป็นสำหรับการลงชื่อเข้าใช้ที่ค้นพบได้
  5. กำหนดค่าการยืนยันผู้ใช้: ระบุ userVerification: "preferred" หรือ userVerification: "required" กรณีการใช้งานด้านการปฏิบัติตามข้อกำหนดหลายกรณี (เช่น การเงิน การดูแลสุขภาพ) กำหนดให้ 'required' ต้องบังคับใช้การยืนยันตัวตนของผู้ใช้เมื่อสร้าง
  6. กำหนดขอบเขตของไฟล์แนบ
    • ขั้นตอนการโปรโมต: เมื่อเสนอให้สร้างพาสคีย์ทันทีหลังจากลงชื่อเข้าใช้ด้วยรหัสผ่านมาตรฐานหรือโปรโมตหลังการลงชื่อสมัครใช้ ให้ตั้งค่า authenticatorAttachment: "platform" เพื่อบังคับใช้โปรแกรมตรวจสอบสิทธิ์ของแพลตฟอร์มและข้ามข้อความแจ้งให้ใช้คีย์ความปลอดภัยภายนอก
    • ขั้นตอนการจัดการ: เมื่อเรียกใช้จากแผงการตั้งค่าหรือแผงความปลอดภัยเฉพาะที่รองรับคีย์ความปลอดภัยภายนอกนอกเหนือจากโปรแกรมตรวจสอบสิทธิ์ของแพลตฟอร์ม ให้ละเว้นพร็อพเพอร์ตี้ authenticatorAttachment ทั้งหมด
    • เคล็ดลับ: ยอมรับแฟล็กคำขอ promotion: boolean เพื่อจัดการทั้ง 2 ขั้นตอนแบบมีเงื่อนไขด้วยปลายทางเดียว
// 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" }),
  },
};

การยืนยัน

  1. การยืนยันคำถาม: ยืนยันคำถามอย่างปลอดภัยเทียบกับคำถามที่เชื่อมโยงกับเซสชันที่คาดไว้
  2. ยืนยันการแสดงตนของผู้ใช้
    • ตรวจสอบว่าแฟล็ก User Present (UP) ที่แสดงในข้อมูลผูตรวจสอบสิทธิ์ที่แยกวิเคราะห์แล้วเป็น true เพื่อยืนยันว่าผู้ใช้แสดงตนในขณะที่สร้าง
  3. การผ่อนคลายการยืนยันสำหรับ "preferred"
    • เมื่อตัวเลือกการสร้างระบุ userVerification: "preferred" คุณต้องกำหนดค่าการเรียกใช้การยืนยันฝั่งเซิร์ฟเวอร์ด้วย requireUserVerification: false ไม่เช่นนั้น เครื่องมือตรวจสอบสิทธิ์ที่ลงทะเบียนโดยไม่มีการยืนยันตัวตนของผู้ใช้ (เช่น ปิดใช้การล็อกหน้าจอ) จะทำให้การยืนยันเซิร์ฟเวอร์ล้มเหลวโดยไม่ทราบสาเหตุ

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

  1. จำกัด UI ในการโหลดหน้าเว็บ
    • เมื่อโหลดหน้าเว็บ ให้เรียกใช้ PublicKeyCredential.getClientCapabilities() และปิดใช้ปุ่ม "สร้างพาสคีย์" หาก conditionalGet หรือ passkeyPlatformAuthenticator ไม่พร้อมใช้งาน
  2. เรียกใช้การสร้างและทำให้เป็นอนุกรม: ถอดรหัสตัวเลือกเซิร์ฟเวอร์ด้วย PublicKeyCredential.parseCreationOptionsFromJSON() และส่งการกำหนดค่าที่ได้ไปยัง navigator.credentials.create()
    • เรียกใช้ credential.toJSON() เพื่อเข้ารหัส AuthenticatorAttestationResponse เป็นออบเจ็กต์ที่แปลงเป็น JSON ได้และถูกต้องก่อนดึงข้อมูลปลายทางการยืนยัน
  3. จัดการข้อยกเว้น WebAuthn:
    • InvalidStateError: มีพาสคีย์ที่ตรงกันอยู่แล้ว (จับคู่โดย excludeCredentials)
    • NotAllowedError: ผู้ใช้ยกเลิกหรือหมดเวลาในกล่องโต้ตอบพาสคีย์การตรวจสอบสิทธิ์
    • AbortError: การดำเนินการถูกยกเลิก
    • SecurityError: ข้อผิดพลาดเกี่ยวกับต้นทางที่ปลอดภัย (HTTPS) หรือรหัส RP ไม่ตรงกัน (ปัญหาการกำหนดค่า)
  4. การแยก Try/Catch สำหรับ Signal API:
    • รวมการเรียก fetch() ของการยืนยันเซิร์ฟเวอร์ไว้ในบล็อก try/catch เรียกใช้ signalUnknownCredential() เมื่อการดึงข้อมูลการยืนยันเซิร์ฟเวอร์ไม่สำเร็จ (สถานะ response.ok === false หรือเครือข่ายแสดงข้อผิดพลาด)
// 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
    });
  }
}

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

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

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

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

Easy JSON Serialization Fallback

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

import 'webauthn-polyfills';