ลงชื่อเข้าใช้อย่างรวดเร็วด้วยโหมด UI ทันที

การนำทางผู้ใช้ที่ไม่ได้รับการตรวจสอบสิทธิ์ไปยังหน้าลงชื่อเข้าใช้เฉพาะเมื่อคลิกลิงก์ "ลงชื่อเข้าใช้" เริ่มชำระเงิน บันทึกรายการลงในรายการโปรด หรือเข้าถึงเพย์วอลล์ของบทความจะทำให้สูญเสียบริบทและเพิ่มการออกของผู้ใช้ โหมด UI ทันที (uiMode: 'immediate' ใน navigator.credentials.get()) ช่วยให้ลงชื่อเข้าใช้ตามบริบทได้อย่างรวดเร็วโดยแสดงตัวเลือกบัญชีผู้ใช้เบราว์เซอร์แบบรวมทันทีเมื่อมีพาสคีย์ในเครื่อง (publicKey) หรือรหัสผ่านที่บันทึกไว้ (password: true) ในอุปกรณ์

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

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

กรณีการใช้งานที่เป็นไปได้

  • การสกัดกั้นลิงก์หรือปุ่มลงชื่อเข้าใช้ในส่วนหัว: สกัดกั้นการคลิกลิงก์ "ลงชื่อเข้าใช้" ทั่วไป (event.preventDefault()) เพื่อตรวจสอบสิทธิ์ผู้ใช้ที่กลับมาทันที หรือไปยังหน้าลงชื่อเข้าใช้เฉพาะใน NotAllowedError ต่อ
  • การลงชื่อเข้าใช้แบบไดนามิกก่อนชำระเงิน: ตรวจสอบสิทธิ์ผู้เลือกซื้อที่กลับมาด้วยการแตะเพียงครั้งเดียวเมื่อคลิก "ดำเนินการชำระเงิน" เพื่อให้ผู้เลือกซื้อไปยังขั้นตอนการจัดส่งและการชำระเงินโดยตรง หรือเปลี่ยนไปใช้หน้าจอการลงชื่อเข้าใช้มาตรฐานหรือการชำระเงินโดยไม่ลงชื่อเข้าใช้ในกรณีที่เกิดข้อผิดพลาด
  • การลงชื่อเข้าใช้ในบรรทัดสำหรับการดำเนินการย่อย (รายการโปรด, กดชอบ, บุ๊กมาร์ก, บันทึก): ตรวจสอบสิทธิ์ในตำแหน่งเมื่อผู้ใช้ที่ออกจากระบบคลิกปุ่มการดำเนินการย่อยและดำเนินการให้เสร็จสมบูรณ์ทันที (เช่น สลับ aria-pressed="true") หรือเปิดโมดัลการเข้าสู่ระบบสำรองหรือหน้าลงชื่อเข้าใช้ที่มีพารามิเตอร์ของ URL สำหรับการกลับมา
  • เพย์วอลล์แบบยืดหยุ่นในบทความ: ตรวจสอบสิทธิ์ผู้อ่านทันทีเมื่อคลิก "อ่านต่อด้วยบัญชีของคุณ" บนแบนเนอร์เพย์วอลล์ หรือขยายแบบฟอร์มลงชื่อเข้าใช้หรือลงทะเบียนในบรรทัดเป็นทางเลือกสำรอง

รูปแบบที่ควรหลีกเลี่ยง:

  • อย่าใช้โหมด UI ทันทีในหน้าลงชื่อเข้าใช้เฉพาะ: อย่าแนบ uiMode: 'immediate' กับปุ่ม "ลงชื่อเข้าใช้" หรือ "ลงชื่อเข้าใช้ด้วยพาสคีย์" หลักในหน้าลงชื่อเข้าใช้ปลายทางสุดท้าย ผู้ใช้จะเข้าถึงหน้าลงชื่อเข้าใช้เฉพาะเมื่อไม่มีข้อมูลเข้าสู่ระบบในอุปกรณ์ปัจจุบัน หรือเมื่อต้องการใช้พาสคีย์ข้ามอุปกรณ์ (สแกนคิวอาร์โค้ดด้วยโทรศัพท์) หรือคีย์ความปลอดภัยแบบฮาร์ดแวร์ภายนอก เนื่องจาก uiMode: 'immediate' จะระงับตัวเลือกคิวอาร์โค้ดและคีย์ความปลอดภัยข้ามอุปกรณ์ และแสดง NotAllowedError ทันทีเมื่อไม่มีข้อมูลเข้าสู่ระบบในเครื่อง การใช้ตัวเลือกนี้ในหน้าลงชื่อเข้าใช้เฉพาะจะทำให้ผู้ใช้ติดอยู่ในทางตันที่การคลิกปุ่มไม่ส่งผลใดๆ ในหน้าลงชื่อเข้าใช้เฉพาะ ให้ใช้ UI แบบมีเงื่อนไข (mediation: 'conditional' ที่มี autocomplete="username webauthn") ในอินพุตของแบบฟอร์มที่จับคู่กับปุ่ม navigator.credentials.get({ publicKey }) โมดัลมาตรฐาน (ดูการตรวจสอบสิทธิ์พาสคีย์)
  • อย่าใช้โหมด UI ทันทีสำหรับการตรวจสอบสิทธิ์ซ้ำแบบยกระดับ: อย่าใช้ uiMode: 'immediate' เพื่อตรวจสอบสิทธิ์ซ้ำของผู้ใช้ที่ลงชื่อเข้าใช้อยู่แล้วก่อนการดำเนินการที่มีความละเอียดอ่อน (เช่น การเปลี่ยนรหัสผ่าน การอัปเดตที่อยู่สำหรับจัดส่ง การแก้ไขวิธีการชำระเงินที่บันทึกไว้ หรือการเพิ่มพาสคีย์ใหม่) การตรวจสอบสิทธิ์ซ้ำต้องจำกัดข้อความแจ้งให้แสดงเฉพาะข้อมูลเข้าสู่ระบบของผู้ใช้ที่ลงชื่อเข้าใช้ผ่านรายการ allowCredentials ที่ไม่ว่างเปล่า ซึ่งโหมด UI ทันทีจะปฏิเสธด้วย NotAllowedError และการละเว้น allowCredentials จะทำให้ผู้ใช้เลือกบัญชีอื่นที่ไม่ใช่บัญชีที่ลงชื่อเข้าใช้อยู่ในปัจจุบันโดยไม่ตั้งใจ สำหรับการตรวจสอบสิทธิ์ซ้ำแบบ Step-up ให้ทำตามการตรวจสอบสิทธิ์ซ้ำด้วยพาสคีย์

ข้อจำกัดด้านความเป็นส่วนตัวและความปลอดภัย

เบราว์เซอร์จะบังคับใช้กฎความเป็นส่วนตัวและความปลอดภัยที่เข้มงวดเมื่อมีการขอ uiMode: 'immediate' ดังนี้

  • ต้องมีการเปิดใช้งานผู้ใช้ชั่วคราว: คุณต้องเรียกใช้ navigator.credentials.get({ uiMode: 'immediate' }) ภายในเครื่องจัดการเหตุการณ์ที่ผู้ใช้เริ่ม (เช่น Listener click ในองค์ประกอบ <button> หรือ <a>) เพื่อป้องกันการตรวจสอบข้อมูลเข้าสู่ระบบแบบไม่แสดงข้อความแจ้ง อย่าเรียกใช้โดยอัตโนมัติเมื่อโหลดหน้าเว็บ (DOMContentLoaded) การเรียก API จะไม่ใช้การเปิดใช้งานชั่วคราว
  • อย่าส่งรายการ allowCredentials ที่ไม่ว่างเปล่า: อย่าป้อนข้อมูล publicKey.allowCredentials เมื่อระบุ uiMode: 'immediate' เบราว์เซอร์จะปฏิเสธคำขอโหมด UI แบบทันทีที่มีรายการ allowCredentials ที่ไม่ว่างเปล่าทันทีโดยแสดง NotAllowedError เพื่อไม่ให้ฝ่ายที่เกี่ยวข้องตรวจสอบรหัสข้อมูลเข้าสู่ระบบที่เฉพาะเจาะจงเพื่ออนุมานการเข้าชมก่อนหน้าหรือติดตามผู้ใช้ในเซสชันต่างๆ
  • อย่าส่ง AbortSignal (signal): อย่าตั้งค่าพร็อพเพอร์ตี้ signal ใน navigator.credentials.get() เมื่อระบุ uiMode: 'immediate' เนื่องจากเบราว์เซอร์ไม่อนุญาตให้ปิดกล่องโต้ตอบการเข้าสู่ระบบทันทีแบบเป็นโปรแกรม หากหน้าเว็บของคุณเรียกใช้คำขอ UI แบบมีเงื่อนไข (mediation: 'conditional') ในเบื้องหลังด้วย ให้เรียกใช้ abort() ใน UI แบบมีเงื่อนไข AbortController ก่อนเรียกใช้โหมด UI แบบทันที และละเว้น signal จากคำขอแบบทันที
  • เซสชันการท่องเว็บแบบไม่ระบุตัวตนและแบบส่วนตัว: คำขอที่ทำในหน้าต่างการท่องเว็บแบบไม่ระบุตัวตนหรือแบบส่วนตัวจะปฏิเสธด้วย NotAllowedError เสมอ เพื่อให้เซสชันส่วนตัวยังคงแยกไม่ออกจากเซสชันที่ไม่มีข้อมูลเข้าสู่ระบบในเครื่อง

การตั้งค่าฝั่งเซิร์ฟเวอร์

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

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

  1. สร้างคำถามที่เชื่อมโยงกับเซสชัน: สร้างบัฟเฟอร์คำถามแบบสุ่มที่เข้ารหัส จัดเก็บไว้ในเซสชันเซิร์ฟเวอร์ และเข้ารหัสเป็น Base64URL
  2. คงค่ากำหนดการยืนยันผู้ใช้: ตั้งค่า userVerification: 'preferred' และจัดเก็บ expectedUserVerification: 'preferred' ในเซสชัน
  3. ละเว้น allowCredentials: อย่าใส่รายการ allowCredentials ที่ไม่ว่างเปล่าในตัวเลือกสำหรับโหมด UI ทันที เนื่องจากเบราว์เซอร์จะปฏิเสธรายการที่อนุญาตที่ไม่ว่างเปล่าซึ่งมี NotAllowedError
// Server-side options generation example (/api/login/options)
router.post('/api/login/options', (req, res) => {
  const options = {
    challenge: serverGeneratedBase64UrlChallenge, // High-entropy random challenge stored in session
    rpId: 'example.com',
    userVerification: 'preferred',
  };

  req.session.challenge = options.challenge;
  req.session.expectedUserVerification = 'preferred';
  return res.json(options);
});

ปลายทางการยืนยันข้อมูลเข้าสู่ระบบแบบคู่

เนื่องจาก navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' }) สามารถแก้ไขได้ด้วยพาสคีย์ (PublicKeyCredential) หรือรหัสผ่านที่บันทึกไว้ (PasswordCredential) ให้ระบุเครื่องมือจัดการการยืนยันสำหรับข้อมูลเข้าสู่ระบบทั้ง 2 ประเภท ดังนี้

  1. การยืนยันการยืนยันตัวตน WebAuthn (/api/login/verify): ยืนยันการยืนยันตัวตนเทียบกับคำถามเซสชันและคีย์สาธารณะที่จัดเก็บไว้ หากไม่พบรหัสข้อมูลเข้าสู่ระบบในฐานข้อมูล ให้แสดงสถานะ HTTP 404 เพื่อให้ไคลเอ็นต์แจ้งเครื่องมือจัดการรหัสผ่านผ่าน PublicKeyCredential.signalUnknownCredential() ได้
  2. การยืนยันรหัสผ่าน (/api/login/password): ยืนยัน username (cred.id) และ password (cred.password) กับฐานข้อมูลผู้ใช้และสร้างเซสชันที่ตรวจสอบสิทธิ์แล้ว

การติดตั้งใช้งานฝั่งไคลเอ็นต์

คำขอข้อมูลเข้าสู่ระบบแบบรวม

  1. ขอทั้งพาสคีย์และรหัสผ่าน: ถอดรหัสตัวเลือกเซิร์ฟเวอร์โดยใช้ PublicKeyCredential.parseRequestOptionsFromJSON() และเรียกใช้ navigator.credentials.get() ภายในตัวแฮนเดิลการคลิกที่ผู้ใช้เริ่มด้วย password: true, publicKey และ uiMode: 'immediate' (โดยไม่ส่ง signal)
  2. การยืนยันสาขาโดย cred.type
    • หาก cred.type === 'public-key' ให้เรียงลำดับการยืนยันด้วย cred.toJSON() และส่ง POST ไปยังปลายทางการยืนยัน WebAuthn เมื่อเซิร์ฟเวอร์ตอบกลับด้วย HTTP 404 ให้เรียกใช้ PublicKeyCredential.signalUnknownCredential({ rpId: publicKey.rpId, credentialId: encoded.id })
    • หาก cred.type === 'password' ให้ส่ง POST { username: cred.id, password: cred.password } ไปยังปลายทางการยืนยันรหัสผ่าน
  3. จัดการ NotAllowedError เมื่อไม่มีข้อมูลเข้าสู่ระบบในเครื่อง: จับ NotAllowedError (แสดงทันทีเมื่อไม่มีพาสคีย์ในเครื่องหรือรหัสผ่านที่บันทึกไว้ในอุปกรณ์ หรือเมื่อผู้ใช้ปิดตัวเลือก) และเปลี่ยนไปใช้ขั้นตอนการลงชื่อเข้าใช้มาตรฐาน (เช่น การเปิดกล่องโต้ตอบการลงชื่อเข้าใช้ในบรรทัดหรือไปยัง /signin ด้วย URL ที่ส่งกลับ)
let isAuthenticating = false;

async function authenticateWithImmediateUi() {
  const optionsResponse = await fetch('/api/login/options', { method: 'POST' });
  if (!optionsResponse.ok) {
    throw new Error('Failed to fetch authentication options');
  }
  const optionsJSON = await optionsResponse.json();
  const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJSON);

  // MANDATORY: Request both password and publicKey with uiMode: 'immediate' and NO signal parameter
  const cred = await navigator.credentials.get({
    password: true,
    publicKey,
    uiMode: 'immediate',
  });

  if (!cred) {
    throw new DOMException('No credential returned', 'NotAllowedError');
  }

  if (cred.type === 'public-key') {
    const encoded = cred.toJSON();
    const verifyResponse = await fetch('/api/login/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(encoded),
    });

    if (verifyResponse.status === 404) {
      await PublicKeyCredential.signalUnknownCredential({
        rpId: publicKey.rpId || window.location.hostname,
        credentialId: encoded.id, // Base64URL-encoded credential ID string
      });
      throw new Error('Passkey credential is no longer recognized by the server');
    }

    if (!verifyResponse.ok) {
      throw new Error('Passkey verification failed');
    }
    return await verifyResponse.json();
  }

  if (cred.type === 'password') {
    const verifyResponse = await fetch('/api/login/password', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        username: cred.id,
        password: cred.password,
      }),
    });

    if (!verifyResponse.ok) {
      throw new Error('Password verification failed');
    }
    return await verifyResponse.json();
  }

  throw new Error(`Unsupported credential type: ${cred.type}`);
}

// Example: Intercepting an inline micro-action (Favorite button) or Header Sign-in click
const favoriteButton = document.getElementById('favorite-btn');

favoriteButton.addEventListener('click', async (event) => {
  event.preventDefault();
  if (isAuthenticating) return;

  // If already signed in, toggle the micro-action state immediately
  if (document.body.dataset.signedIn === 'true') {
    const nextPressed = favoriteButton.getAttribute('aria-pressed') !== 'true';
    favoriteButton.setAttribute('aria-pressed', String(nextPressed));
    return;
  }

  isAuthenticating = true;
  try {
    const user = await authenticateWithImmediateUi();
    // Complete the user's intended action immediately
    document.body.dataset.signedIn = 'true';
    favoriteButton.setAttribute('aria-pressed', 'true');
    updateHeaderUserState(user);
  } catch (err) {
    if (err.name === 'NotAllowedError') {
      // No local credentials available or user dismissed dialog:
      // transition smoothly to fallback sign-in UI or redirect with return URL
      showFallbackSignInUi({ returnAction: 'favorite' });
      return;
    }
    console.error('Authentication error:', err);
  } finally {
    isAuthenticating = false;
  }
});

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

โหมด UI สำรองแบบทันที

  • ประสบการณ์การเพิ่มประสิทธิภาพแบบต่อเนื่อง: ถือว่า uiMode: 'immediate' เป็นการเพิ่มประสิทธิภาพแบบต่อเนื่อง อย่าอนุมานการรองรับจากชื่อหรือเวอร์ชันของเบราว์เซอร์ สัญญาณการรองรับที่เชื่อถือได้เพียงอย่างเดียวคือการตรวจสอบรันไทม์ capabilities.immediateGet ผ่าน PublicKeyCredential.getClientCapabilities() บังคับ: ตรวจสอบ capabilities.immediateGet ก่อนเรียกใช้ navigator.credentials.get({ uiMode: 'immediate' }) เสมอ เนื่องจากเบราว์เซอร์ที่ไม่รู้จัก uiMode: 'immediate' จะละเว้นคีย์พจนานุกรมที่ไม่รู้จัก และเปิดกล่องโต้ตอบพาสคีย์และคิวอาร์โค้ดแบบโมดัลที่บล็อก เมื่อ capabilities.immediateGet เป็นเท็จ หรือเมื่อ navigator.credentials.get() ปฏิเสธด้วย NotAllowedError ให้กลับไปใช้ประสบการณ์การลงชื่อเข้าใช้มาตรฐานของแอปพลิเคชัน (เช่น การเปิดโมดัลการลงชื่อเข้าใช้ในบรรทัดหรือการไปยังหน้าลงชื่อเข้าใช้เฉพาะที่มีพารามิเตอร์ของ URL แสดงผล) เพื่อให้ผู้ใช้ในเบราว์เซอร์ที่ไม่รองรับเห็นขั้นตอนการลงชื่อเข้าใช้ที่มีอยู่โดยไม่มีการเปลี่ยนแปลง ดังนี้
async function isImmediateUiSupported() {
  if (!window.PublicKeyCredential || !PublicKeyCredential.getClientCapabilities) {
    return false;
  }
  try {
    const capabilities = await PublicKeyCredential.getClientCapabilities();
    return Boolean(capabilities?.immediateGet);
  } catch {
    return false;
  }
}

async function authenticateWithImmediateUi() {
  // MANDATORY: Check immediateGet support so unsupported browsers do not open a modal QR dialog
  const supported = await isImmediateUiSupported();
  if (!supported) {
    // Throw NotAllowedError so the caller's single catch path treats "unsupported" like "no local credentials"
    throw new DOMException('Immediate UI mode is not supported', 'NotAllowedError');
  }
  // Proceed with fetching options and calling navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' })
}

ทางเลือกสำรองสำหรับการตรวจหาความสามารถของไคลเอ็นต์

ติดตั้ง 'webauthn-polyfills' และนำเข้าในโมดูลไคลเอ็นต์เสมอเพื่อให้ PublicKeyCredential.getClientCapabilities() พร้อมใช้งานเมื่อใดก็ตามที่รองรับ PublicKeyCredential

import 'webauthn-polyfills';

การใช้ข้อมูลเข้าสู่ระบบด้วยรหัสผ่านเป็นทางเลือกสำรอง

การส่ง password: true ควบคู่ไปกับ publicKey ใน navigator.credentials.get() ช่วยให้เบราว์เซอร์ที่รองรับสามารถรวมรหัสผ่านที่บันทึกไว้จากเครื่องมือจัดการรหัสผ่านของเบราว์เซอร์ไว้ในตัวเลือกบัญชีผู้ใช้แบบรวมที่แสดงทันที

  • ประสบการณ์สำรอง: เบราว์เซอร์ที่ไม่รองรับ PasswordCredential จะละเว้นตัวเลือก password: true หรือสำรองผ่านการตรวจสอบฟีเจอร์ capabilities.immediateGet ไปยังแบบฟอร์มการลงชื่อเข้าใช้มาตรฐาน ซึ่ง <input autocomplete="username webauthn"> และ <input type="password" autocomplete="current-password"> มีการป้อนรหัสผ่านอัตโนมัติในตัว

การสำรองการแปลงรูปแบบ JSON ที่ง่ายดาย

ติดตั้ง 'webauthn-polyfills' และนำเข้าในแพ็กเกจไคลเอ็นต์เสมอเพื่อให้ PublicKeyCredential.parseRequestOptionsFromJSON() และ PublicKeyCredential.prototype.toJSON() ทำงานอย่างสอดคล้องกันในเบราว์เซอร์ที่รองรับ PublicKeyCredential

import 'webauthn-polyfills';

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

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

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