การนำทางผู้ใช้ที่ไม่ได้รับการตรวจสอบสิทธิ์ไปยังหน้าลงชื่อเข้าใช้เฉพาะเมื่อคลิกลิงก์ "ลงชื่อเข้าใช้" เริ่มชำระเงิน บันทึกรายการลงในรายการโปรด หรือเข้าถึงเพย์วอลล์ของบทความจะทำให้สูญเสียบริบทและเพิ่มการออกของผู้ใช้ โหมด UI ทันที (uiMode: 'immediate' ใน navigator.credentials.get()) ช่วยให้ลงชื่อเข้าใช้ตามบริบทได้อย่างรวดเร็วโดยแสดงตัวเลือกบัญชีผู้ใช้เบราว์เซอร์แบบรวมทันทีเมื่อมีพาสคีย์ในเครื่อง (publicKey) หรือรหัสผ่านที่บันทึกไว้ (password: true) ในอุปกรณ์
โหมด UI แบบทันทีจะดำเนินการไม่สำเร็จอย่างรวดเร็วพร้อมข้อผิดพลาด NotAllowedError หากไม่มีข้อมูลเข้าสู่ระบบที่มีสิทธิ์ในอุปกรณ์ ซึ่งต่างจากข้อความแจ้ง WebAuthn แบบโมดัลมาตรฐานที่แสดง UI ของเบราว์เซอร์เสมอและเปลี่ยนไปใช้คิวอาร์โค้ดข้ามอุปกรณ์หรือคีย์ความปลอดภัยภายนอก ซึ่งช่วยให้แอปพลิเคชันตรวจสอบสิทธิ์ผู้ใช้ที่กลับมาได้ทันทีด้วยการแตะเพียงครั้งเดียว พร้อมทั้งเปลี่ยนเส้นทางผู้ใช้ที่ไม่มีข้อมูลเข้าสู่ระบบในเครื่องไปยังหน้าลงชื่อเข้าใช้มาตรฐาน การชำระเงินโดยไม่ลงชื่อเข้าใช้ หรือแบบฟอร์มการเข้าสู่ระบบในหน้าได้อย่างราบรื่น
รูปแบบ UX ที่แนะนำและรูปแบบที่ควรหลีกเลี่ยง
ทริกเกอร์โหมด 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' })ภายในเครื่องจัดการเหตุการณ์ที่ผู้ใช้เริ่ม (เช่น Listenerclickในองค์ประกอบ<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 ที่ค้นพบได้สำหรับผู้เข้าชมที่ไม่ได้รับการตรวจสอบสิทธิ์ โดยทำดังนี้
- สร้างคำถามที่เชื่อมโยงกับเซสชัน: สร้างบัฟเฟอร์คำถามแบบสุ่มที่เข้ารหัส จัดเก็บไว้ในเซสชันเซิร์ฟเวอร์ และเข้ารหัสเป็น Base64URL
- คงค่ากำหนดการยืนยันผู้ใช้: ตั้งค่า
userVerification: 'preferred'และจัดเก็บexpectedUserVerification: 'preferred'ในเซสชัน - ละเว้น
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 ประเภท ดังนี้
- การยืนยันการยืนยันตัวตน WebAuthn (
/api/login/verify): ยืนยันการยืนยันตัวตนเทียบกับคำถามเซสชันและคีย์สาธารณะที่จัดเก็บไว้ หากไม่พบรหัสข้อมูลเข้าสู่ระบบในฐานข้อมูล ให้แสดงสถานะ HTTP404เพื่อให้ไคลเอ็นต์แจ้งเครื่องมือจัดการรหัสผ่านผ่านPublicKeyCredential.signalUnknownCredential()ได้ - การยืนยันรหัสผ่าน (
/api/login/password): ยืนยันusername(cred.id) และpassword(cred.password) กับฐานข้อมูลผู้ใช้และสร้างเซสชันที่ตรวจสอบสิทธิ์แล้ว
การติดตั้งใช้งานฝั่งไคลเอ็นต์
คำขอข้อมูลเข้าสู่ระบบแบบรวม
- ขอทั้งพาสคีย์และรหัสผ่าน: ถอดรหัสตัวเลือกเซิร์ฟเวอร์โดยใช้
PublicKeyCredential.parseRequestOptionsFromJSON()และเรียกใช้navigator.credentials.get()ภายในตัวแฮนเดิลการคลิกที่ผู้ใช้เริ่มด้วยpassword: true,publicKeyและuiMode: 'immediate'(โดยไม่ส่งsignal) - การยืนยันสาขาโดย
cred.type- หาก
cred.type === 'public-key'ให้เรียงลำดับการยืนยันด้วยcred.toJSON()และส่ง POST ไปยังปลายทางการยืนยัน WebAuthn เมื่อเซิร์ฟเวอร์ตอบกลับด้วย HTTP404ให้เรียกใช้PublicKeyCredential.signalUnknownCredential({ rpId: publicKey.rpId, credentialId: encoded.id }) - หาก
cred.type === 'password'ให้ส่ง POST{ username: cred.id, password: cred.password }ไปยังปลายทางการยืนยันรหัสผ่าน
- หาก
- จัดการ
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)เมื่อไม่รองรับ ให้ข้ามการเรียกใช้สัญญาณอย่างราบรื่นและแสดงข้อความที่ชัดเจนเพื่อแนะนำให้ผู้ใช้ลงชื่อเข้าใช้ด้วยวิธีอื่น