การสร้างพาสคีย์แบบมีเงื่อนไข (การโปรโมตหลังการเข้าสู่ระบบ)

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

ช่วงเวลาที่เหมาะสมในการกระตุ้น

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

  • อย่าพยายามสร้างแบบมีเงื่อนไขสำหรับขั้นตอนแบบไม่ใช้รหัสผ่าน (เช่น Magic Link, SMS OTP หรือการรวมข้อมูลประจำตัว)
  • หากจำเป็นต้องใช้การตรวจสอบสิทธิ์แบบหลายปัจจัย คุณต้องรอจนกว่าปัจจัยทั้งหมดจะสำเร็จก่อนจึงจะเริ่มสร้างแบบมีเงื่อนไขได้
  • ตรวจสอบว่าเซสชันผู้ใช้ที่ผ่านการตรวจสอบสิทธิ์และถูกต้องทำงานอยู่ก่อนที่จะส่งคำขอไปยังปลายทางการสร้าง

ขั้นตอนการติดตั้งใช้งาน

1. ยกเลิกการดำเนินการป้อนข้อความอัตโนมัติก่อนหน้า

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

  • เรียกใช้ abortController.abort() ใน AbortController ที่แนบมากับคำขอป้อนข้อความอัตโนมัติ navigator.credentials.get() ที่รอดำเนินการก่อนเรียกใช้ navigator.credentials.create()

2. การตรวจจับฟีเจอร์

ตรวจสอบว่าการสร้างแบบมีเงื่อนไขพร้อมใช้งานหรือไม่โดยตรวจสอบ conditionalCreate ด้วย PublicKeyCredential.getClientCapabilities()

const capabilities = await PublicKeyCredential.getClientCapabilities();
if (capabilities.conditionalCreate) {
  // Conditional create is available
}

3. สร้างพาสคีย์ด้วยการสร้างแบบมีเงื่อนไข

  • ส่ง mediation: 'conditional' ภายในตัวเลือก navigator.credentials.create() ซึ่งจะส่งสัญญาณให้เบราว์เซอร์จัดการขั้นตอนการสร้างพาสคีย์อย่างเงียบๆ ในเบื้องหลังหรือตามบริบทโดยไม่แสดงกล่องโต้ตอบโมดัลที่รบกวน
  • ป้อน excludeCredentials ด้วยรหัสข้อมูลเข้าสู่ระบบพาสคีย์ที่มีอยู่ของผู้ใช้เพื่อหลีกเลี่ยงการลงทะเบียนคีย์ที่ซ้ำกัน

4. การจัดการข้อผิดพลาดแบบเงียบ

  • รวมข้อความแจ้งการสร้างพาสคีย์ (navigator.credentials.create) ไว้ในบล็อก try/catch คุณต้องตรวจจับและละเว้นข้อยกเว้นที่แสดงต่อผู้ใช้โดยทั่วไป (InvalidStateError, NotAllowedError, AbortError) โดยไม่แสดง UI ข้อผิดพลาดต่อผู้ใช้

5. การยืนยันการแสดงตนของผู้ใช้ฝั่งเซิร์ฟเวอร์

  • ปลายทางการยืนยันฝั่งเซิร์ฟเวอร์ต้องผ่อนปรนข้อกำหนดการแสดงตนของผู้ใช้ (UP) (requireUserPresence: false) เท่านั้นเมื่อยืนยันข้อมูลเข้าสู่ระบบที่สร้างโดยทริกเกอร์การสร้างแบบมีเงื่อนไข การยืนยันการแสดงตนอย่างเข้มงวดต้องยังคงทำงานอยู่สำหรับการสร้างที่ชัดเจนมาตรฐาน

6. จัดการการยืนยันเซิร์ฟเวอร์ที่ไม่สำเร็จอย่างเหมาะสม

  • หาก navigator.credentials.create() สำเร็จ แต่การดึงข้อมูลการยืนยันเซิร์ฟเวอร์แสดงผลการตอบกลับที่ไม่ถูกต้อง (เช่น การยืนยันลายเซ็นไม่สำเร็จ) ให้เรียกใช้ PublicKeyCredential.signalUnknownCredential() เพื่อป้องกันไม่ให้ข้อมูลเข้าสู่ระบบที่ไม่มีเจ้าของค้างอยู่ในผู้ให้บริการพาสคีย์

ตัวอย่างโค้ด

// optionsFetch and registerVerifyFetch are app-defined server endpoint requests
import { optionsFetch, registerVerifyFetch } from './api.js';

async function triggerConditionalCreate(loginAbortController) {
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (capabilities.conditionalCreate !== true) {
    return; // Platform does not support conditional creation
  }

  // 1. Abort any active autofill conditional-get controllers to clear the WebAuthn pipeline
  loginAbortController.abort();

  // 2. Fetch creation options signaling the backend that this is a conditional request
  const creationOptionsJSON = await optionsFetch({ conditional: true });
  const publicKey = PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);

  let credential;
  try {
    // 3. Invoke silent credentials creation prompt
    credential = await navigator.credentials.create({ 
      publicKey,
      mediation: 'conditional' // Silent background creation mediation
    });
  } catch (e) {
    // 4. Silently swallow common WebAuthn browser exceptions
    if (['InvalidStateError', 'NotAllowedError', 'AbortError'].includes(e.name)) {
      return; 
    }
    console.error('Unexpected conditional create error:', e);
    return;
  }

  // 5. Server verification step using dedicated Try/Catch block
  let encodedResponse = credential.toJSON();
  try {
    const response = await registerVerifyFetch(encodedResponse);
    if (!response.ok) {
      // If the server verification fails, clean up using Signal API
      if (PublicKeyCredential.signalUnknownCredential) {
        await PublicKeyCredential.signalUnknownCredential({
          rpId, // RP ID must match the one defined on the server
          credentialId: encodedResponse.id
        });
      }
    }
  } catch (serverErr) {
    console.error('Verification network failure:', serverErr);
    if (PublicKeyCredential.signalUnknownCredential) {
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encodedResponse.id
      });
    }
  }
}

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

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

ติดตั้ง "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.parseCreationOptionsFromJSON และ PublicKeyCredential.prototype.toJSON ด้วย

import 'webauthn-polyfills';