คู่มือนี้จะอธิบายวิธีอนุญาตให้ผู้ใช้ดู เปลี่ยนชื่อ และลบพาสคีย์ที่ลงทะเบียนไว้ ขณะเดียวกันก็ซิงค์ข้อมูลเข้าสู่ระบบที่บันทึกไว้และรายละเอียดโปรไฟล์ผู้ใช้ระหว่างเซิร์ฟเวอร์กับเครื่องมือจัดการรหัสผ่านของผู้ใช้โดยใช้ Signal API
การดำเนินการฝั่งเซิร์ฟเวอร์
เลเยอร์ฐานข้อมูลแบ็กเอนด์และปลายทางต้องรองรับการดำเนินการ CRUD ทั่วไปสำหรับข้อมูลเข้าสู่ระบบที่ลงทะเบียนไว้ รวมถึงการอัปเดตรายละเอียดโปรไฟล์ผู้ใช้ เซิร์ฟเวอร์จะแยกออกจากไลบรารีเฉพาะเฟรมเวิร์ก และแสดงปลายทางเพื่อดำเนินการต่อไปนี้
- แสดงรายการข้อมูลเข้าสู่ระบบทั้งหมดของผู้ใช้: ดึงข้อมูล
StoredPasskeyCredentialทั้งหมดที่ตรงกับรหัสของผู้ใช้ที่ลงชื่อเข้าใช้ - อัปเดตชื่อข้อมูลเข้าสู่ระบบ: ยอมรับชื่อเล่นที่กำหนดเองใหม่สำหรับรหัสข้อมูลเข้าสู่ระบบที่เฉพาะเจาะจง และคงการอัปเดตไว้ในเซิร์ฟเวอร์
- ลบข้อมูลเข้าสู่ระบบ: นำรหัสข้อมูลเข้าสู่ระบบที่เฉพาะเจาะจงออกจากฐานข้อมูล
- อัปเดตรายละเอียดบัญชีผู้ใช้: ยอมรับค่า
name(ชื่อผู้ใช้) และdisplayNameที่อัปเดตแล้วสำหรับโปรไฟล์ของผู้ใช้ที่ลงชื่อเข้าใช้ เพื่อให้ไคลเอ็นต์ซิงค์ค่าดังกล่าวกับผู้ให้บริการพาสคีย์ได้
// Node.js routing example for credential CRUD and user profile rename
router.get('/api/credentials', checkUserAuthenticated, async (req, res) => {
const list = await db.findCredentialsByUserId(req.user.id);
return res.json(list);
});
router.put('/api/credential/:id', checkUserAuthenticated, async (req, res) => {
const { id } = req.params;
const { name } = req.body;
const cred = await db.findCredentialById(id);
if (!cred || cred.passkeyUserId !== req.user.id) {
return res.status(404).json({ error: 'Credential not found.' });
}
cred.name = name;
await db.saveCredential(cred);
return res.json(cred);
});
router.delete('/api/credential/:id', checkUserAuthenticated, async (req, res) => {
const { id } = req.params;
const cred = await db.findCredentialById(id);
if (!cred || cred.passkeyUserId !== req.user.id) {
return res.status(404).json({ error: 'Credential not found.' });
}
await db.deleteCredential(id);
return res.json({ success: true });
});
router.put('/api/user', checkUserAuthenticated, async (req, res) => {
const { name, displayName } = req.body;
const updatedUser = await db.updateUser(req.user.id, { name, displayName });
return res.json(updatedUser);
});
UI การจัดการฝั่งไคลเอ็นต์
แสดงแผงการตั้งค่าเฉพาะที่ช่วยให้ผู้ใช้ตรวจสอบและจัดการตัวเลือกการตรวจสอบสิทธิ์ที่ลงทะเบียนและรายละเอียดโปรไฟล์บัญชีได้อย่างง่ายดาย ดังนี้
- แสดงรายการที่บันทึกไว้: ดึงข้อมูลรายการจากปลายทางและแสดงผลแถวข้อมูลเข้าสู่ระบบแต่ละแถว หากการตอบกลับว่างเปล่า ให้แสดงข้อความสถานะว่างเปล่าที่เป็นประโยชน์ (เช่น "ไม่พบพาสคีย์")
- แมปข้อมูลเมตา AAGUID: สำหรับพาสคีย์แต่ละรายการ ให้ค้นหาพร็อพเพอร์ตี้
aaguidกับรีจิสทรีในเครื่องเพื่อแสดงรายละเอียดผู้ให้บริการ ดูรายละเอียดเพิ่มเติมได้ที่ส่วนระบุผู้ให้บริการพาสคีย์จาก AAGUID - ข้อกำหนดด้าน UI สำหรับแต่ละรายการ: ทุกแถวภายในคอนเทนเนอร์รายการต้องแสดงผลดังนี้
- ไอคอนผู้ให้บริการ: รูปภาพที่ได้มาจาก AAGUID หรือ URI ข้อมูล
- ชื่อผู้ให้บริการ/ชื่อที่กำหนดเอง: ชื่อที่ได้มาจาก AAGUID หรือสตริงที่ผู้ใช้เปลี่ยนชื่อ
- วันที่ลงทะเบียน: การประทับเวลา Epoch ดิบ
registeredAtที่คงอยู่ในฐานข้อมูลซึ่งจัดรูปแบบเป็นวันที่ที่มนุษย์อ่านได้สำหรับการแสดงผลของไคลเอ็นต์ - วันที่ใช้ล่าสุด: การประทับเวลา Epoch ดิบที่คงอยู่ในฐานข้อมูล
lastUsedAtซึ่งจัดรูปแบบเป็นวันที่ที่มนุษย์อ่านได้ (หากมี) สำหรับการแสดงผลของไคลเอ็นต์ - ปุ่มเปลี่ยนชื่อ: ทริกเกอร์การดำเนินการเปลี่ยนชื่อเพื่ออัปเดตชื่อเล่นที่กำหนดเองของข้อมูลเข้าสู่ระบบในเซิร์ฟเวอร์ (
PUT /api/credential/:id) หมายเหตุ: การเปลี่ยนชื่อเล่นของพาสคีย์แต่ละรายการเป็นการเปลี่ยนแปลงป้ายกำกับฝั่งเซิร์ฟเวอร์ และจะไม่เรียกใช้signalCurrentUserDetails() - ปุ่มลบ: ทริกเกอร์การลบข้อมูลเข้าสู่ระบบ (
DELETE /api/credential/:id) และซิงค์รหัสข้อมูลเข้าสู่ระบบที่เหลือผ่านsignalAllAcceptedCredentials()
- UI การเปลี่ยนชื่อบัญชีผู้ใช้
- จัดเตรียมการควบคุมที่อนุญาตให้ผู้ใช้อัปเดตชื่อผู้ใช้บัญชี (
name) หรือdisplayName(PUT /api/user) และซิงค์รายละเอียดผู้ใช้ที่อัปเดตกับเครื่องมือจัดการรหัสผ่านผ่านPublicKeyCredential.signalCurrentUserDetails()ทันที
- จัดเตรียมการควบคุมที่อนุญาตให้ผู้ใช้อัปเดตชื่อผู้ใช้บัญชี (
- ปุ่ม "สร้างพาสคีย์" แบบมีเงื่อนไข
- แสดงปุ่มทริกเกอร์การลงทะเบียน "สร้างพาสคีย์" ที่โดดเด่นในหน้าการจัดการ ก่อนแสดงผลองค์ประกอบ UI นี้ หน้าเว็บต้องตรวจหาความสามารถของฟีเจอร์โดยใช้
PublicKeyCredential.getClientCapabilities()เพื่อยืนยันว่าระบบรองรับโปรแกรมตรวจสอบสิทธิ์ของแพลตฟอร์ม หากระบบไม่รองรับพาสคีย์ ให้ซ่อนปุ่มนี้และกระตุ้นให้ผู้ใช้ลงทะเบียน MFA มาตรฐานแทนอย่างเหมาะสม - อนุญาตให้ลงทะเบียนคีย์ความปลอดภัยโดยละเว้น
authenticatorSelection.authenticatorAttachmentในการเรียกnavigator.credentials.create()
- แสดงปุ่มทริกเกอร์การลงทะเบียน "สร้างพาสคีย์" ที่โดดเด่นในหน้าการจัดการ ก่อนแสดงผลองค์ประกอบ UI นี้ หน้าเว็บต้องตรวจหาความสามารถของฟีเจอร์โดยใช้
การซิงค์ Signal API
Signal API ช่วยให้แอปพลิเคชันสื่อสารสถานะของข้อมูลเข้าสู่ระบบกับเครื่องมือจัดการรหัสผ่านได้ ซึ่งจะช่วยให้ห้องนิรภัยที่ซิงค์ของผู้ใช้และฐานข้อมูลแบ็กเอนด์ของคุณสอดคล้องกัน
- กฎการตรวจหาฟีเจอร์และการเข้ารหัสพารามิเตอร์
- ตรวจหาฟีเจอร์ของเมธอด Signal API ก่อนเรียกใช้เสมอ (
if (PublicKeyCredential.signalAllAcceptedCredentials)และif (PublicKeyCredential.signalCurrentUserDetails)) - พารามิเตอร์
userIdและรหัสข้อมูลเข้าสู่ระบบทั้งหมดที่ส่งไปยังเมธอด Signal API (signalAllAcceptedCredentials,signalCurrentUserDetails) ต้องเป็นสตริงที่เข้ารหัส Base64URL
- ตรวจหาฟีเจอร์ของเมธอด Signal API ก่อนเรียกใช้เสมอ (
- การเริ่มซิงค์การโหลดหน้าเว็บ
- แอปพลิเคชันต้องเรียกใช้
signalAllAcceptedCredentials()โดยอัตโนมัติเมื่อหน้าการจัดการโหลด (เช่น ภายใน Listener เหตุการณ์DOMContentLoaded, การเริ่มต้นโมดูล หรือฮุกการต่อเชื่อมคอมโพเนนต์เฟรมเวิร์ก เช่นuseEffect)
- แอปพลิเคชันต้องเรียกใช้
- การซิงค์ข้อมูลอัปเดตการจัดการ:
- แอปพลิเคชันต้องเรียกใช้
signalAllAcceptedCredentials()ทันทีภายในตัวแฮนเดิลการคลิกลบข้อมูลเข้าสู่ระบบหลังการดึงข้อมูล - แอปพลิเคชันต้องเรียกใช้
signalCurrentUserDetails()ทันทีภายในตัวแฮนเดิลการคลิกเปลี่ยนชื่อบัญชีผู้ใช้ (nameหรือdisplayName) หลังการดึงข้อมูล
- แอปพลิเคชันต้องเรียกใช้
// Client-side management synchronization ES module
import { listFetch, renameCredentialFetch, updateUserFetch, deleteFetch } from './api.js';
// Base64URL-encoded User ID string (illustration only)
const base64UrlUserId = "M2YPl-KGnA8";
async function syncAcceptedCredentials(currentCredentialsList) {
if (!window.PublicKeyCredential || !PublicKeyCredential.signalAllAcceptedCredentials) return;
try {
const credentialIds = currentCredentialsList.map(c => c.id); // Array of Base64URL credential ID strings
await PublicKeyCredential.signalAllAcceptedCredentials({
rpId, // RP ID must match the one defined on the server
userId: base64UrlUserId, // User ID Base64URL-encoded string
allAcceptedCredentialIds: credentialIds
});
} catch (e) {
console.error('SignalAllAcceptedCredentials sync failure:', e);
}
}
async function loadManagementPanel() {
const response = await listFetch();
const list = await response.json();
renderUI(list);
// Sync accepted credentials on initial load
await syncAcceptedCredentials(list);
}
async function performDelete(credentialId) {
const response = await deleteFetch(credentialId);
if (response.ok) {
const updatedResponse = await listFetch();
const updatedList = await updatedResponse.json();
renderUI(updatedList);
// Sync remaining accepted credentials after deletion
await syncAcceptedCredentials(updatedList);
}
}
// Renaming a passkey's nickname updates the server record only
async function performCredentialRename(credentialId, newCredentialName) {
const response = await renameCredentialFetch(credentialId, { name: newCredentialName });
if (response.ok) {
const updatedResponse = await listFetch();
renderUI(await updatedResponse.json());
}
}
// Renaming the user's account username or displayName triggers signalCurrentUserDetails
async function performUserRename(rpId, userId, updatedName, updatedDisplayName) {
const response = await updateUserFetch({ name: updatedName, displayName: updatedDisplayName });
if (response.ok && window.PublicKeyCredential && PublicKeyCredential.signalCurrentUserDetails) {
try {
await PublicKeyCredential.signalCurrentUserDetails({
rpId, // RP ID must match the one defined on the server
userId, // Base64URL-encoded user ID
name: updatedName, // Updated username
displayName: updatedDisplayName // Updated display name
});
} catch (e) {
console.error('SignalCurrentUserDetails sync failure:', e);
}
}
}
window.addEventListener('DOMContentLoaded', loadManagementPanel);
ระบุผู้ให้บริการพาสคีย์จาก AAGUID
AAGUID (Authenticator Attestation Globally Unique Identifier) คือตัวระบุที่ไม่ซ้ำกันขนาด 128 บิตที่แสดงถึงรุ่นของ Authenticator ไม่ใช่อินสแตนซ์ที่เฉพาะเจาะจง โดยจะรวมอยู่ในข้อมูล Authenticator ระหว่างการลงทะเบียนพาสคีย์ และสามารถใช้เพื่อระบุผู้ให้บริการพาสคีย์ (เช่น เครื่องมือจัดการรหัสผ่านบน Google, พวงกุญแจ iCloud, 1Password) ที่สร้างข้อมูลเข้าสู่ระบบ
ควรใช้ AAGUID เพื่อช่วยผู้ใช้ในการจัดการพาสคีย์เท่านั้น ซึ่งสามารถแก้ไขได้ เว้นแต่จะมีการรับรองด้วยการเข้ารหัสลับ ซึ่งปัจจุบันพาสคีย์ของแพลตฟอร์มยังไม่รองรับ
1. รีจิสทรี AAGUID
การแมป JSON ที่ชุมชนดูแลรักษาของ AAGUID กับชื่อและไอคอนของผู้ให้บริการมีอยู่ที่:
https://raw.githubusercontent.com/passkeydeveloper/passkey-authenticator-aaguids/refs/heads/main/combined_aaguid.json
แต่ละรายการมีสคีมาต่อไปนี้
{
"<aaguid-uuid>": {
"name": "Provider Name",
"icon_light": "data:image/png;base64,...",
"icon_dark": "data:image/png;base64,..."
}
}
2. การใช้ AAGUID หลังจากการลงทะเบียน
หลังจากยืนยันการตอบกลับการลงทะเบียนแล้ว ให้อ่าน aaguid จากผลการลงทะเบียนและค้นหาเทียบกับรีจิสทรีเพื่อป้อนข้อมูล name และ providerIcon ของข้อมูลเข้าสู่ระบบ
ก่อนค้นหา AAGUID ในรีจิสทรี ให้ตรวจสอบว่ามีค่าเท่ากับ '00000000-0000-0000-0000-000000000000' หรือไม่ หากเป็นเช่นนั้น ให้ข้ามการค้นหาในรีจิสทรีและตั้งค่า name เป็นค่าสำรอง (เช่น ชื่ออุปกรณ์จาก User Agent หรือ "ผู้ให้บริการพาสคีย์ที่ไม่รู้จัก") และตั้งค่า providerIcon เป็น undefined ค้นหารีจิสทรีสำหรับ AAGUID ที่ไม่ใช่ศูนย์เท่านั้น
import aaguids from './aaguids.json' with { type: 'json' };
const { aaguid } = registrationInfo;
if (aaguid === '00000000-0000-0000-0000-000000000000') {
// use the device name as the passkey provider based on
// the information derived from the user agent string,
// or just say "Unknown passkey provider"
} else {
const provider = aaguids[aaguid];
const credential = {
// ...other fields
aaguid,
name: provider?.name || 'Unknown passkey provider',
providerIcon: provider?.icon_light,
};
}
กลยุทธ์สำรอง
ฟีเจอร์สำรองสำหรับการตรวจหาฟีเจอร์พาสคีย์
ติดตั้ง "webauthn-polyfills" และนำเข้าในบริบทเสมอ
พิจารณาว่าตราบใดที่ระบบรองรับ PublicKeyCredential ระบบก็จะรองรับ PublicKeyCredential.getClientCapabilities ด้วย
import 'webauthn-polyfills';
การซิงค์ API ของ Signal สำรอง
หากเบราว์เซอร์ไม่รองรับ PublicKeyCredential.parseRequestOptionsFromJSON ให้ใช้ "webauthn-polyfills" ดังนี้
<script type="module">
if (!PublicKeyCredential.parseRequestOptionsFromJSON) {
await import('https://unpkg.com/webauthn-polyfills');
}
</script>
ซึ่งจะเพิ่มการรองรับ PublicKeyCredential.prototype.toJSON ด้วย