คู่มือนี้จะอธิบายวิธีอนุญาตให้ผู้ใช้ลงทะเบียนพาสคีย์สำหรับบัญชีของตน ซึ่งเป็นทางเลือกในการลงชื่อเข้าใช้แบบไร้รหัสผ่านที่มีความปลอดภัยสูงและป้องกันฟิชชิงได้
ข้อกำหนดของฐานข้อมูล
ตารางข้อมูลเข้าสู่ระบบของฐานข้อมูลต้องจัดเก็บฟิลด์ต่อไปนี้เพื่อรองรับการลงทะเบียนพาสคีย์
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 ใช้ไลบรารีที่ผ่านการตรวจสอบตามมาตรฐานหมวดหมู่แทนการเข้ารหัสด้วยตนเอง
- ใช้รหัส RP ที่กำหนดไว้ล่วงหน้า: ใช้รหัส RP ที่เหมาะสมซึ่งกำหนดไว้ล่วงหน้าเป็นสตริงคงที่
- สร้างคำถามที่ปลอดภัย: สร้างบัฟเฟอร์แบบสุ่มที่มีเอนโทรปีสูงและปลอดภัยด้วยการเข้ารหัสบนเซิร์ฟเวอร์ จัดเก็บไว้อย่างปลอดภัยในเซสชันของผู้ใช้ และเข้ารหัสเป็น Base64URL สำหรับการส่งตัวเลือก
- หลีกเลี่ยงพาสคีย์ที่ซ้ำกัน: แมปรหัสข้อมูลเข้าสู่ระบบที่ลงทะเบียนไว้ล่วงหน้าที่มีอยู่ของผู้ใช้กับอาร์เรย์ตัวเลือก
excludeCredentialsซึ่งจะป้องกันไม่ให้ Authenticator ลงทะเบียนข้อมูลเข้าสู่ระบบซ้ำในบัญชีผู้ให้บริการพาสคีย์เดียวกัน - บังคับใช้ข้อมูลเข้าสู่ระบบที่ค้นพบได้: ตั้งค่า
requireResidentKey: trueและresidentKey: "required"ในตัวเลือกauthenticatorSelectionเพื่อขอข้อมูลเข้าสู่ระบบที่ค้นพบได้ ซึ่งจำเป็นสำหรับการลงชื่อเข้าใช้ที่ค้นพบได้ - กำหนดค่าการยืนยันผู้ใช้: ระบุ
userVerification: "preferred"หรือuserVerification: "required"กรณีการใช้งานด้านการปฏิบัติตามข้อกำหนดหลายกรณี (เช่น การเงิน การดูแลสุขภาพ) กำหนดให้'required'ต้องบังคับใช้การยืนยันตัวตนของผู้ใช้เมื่อสร้าง - กำหนดขอบเขตของไฟล์แนบ
- ขั้นตอนการโปรโมต: เมื่อเสนอให้สร้างพาสคีย์ทันทีหลังจากลงชื่อเข้าใช้ด้วยรหัสผ่านมาตรฐานหรือโปรโมตหลังการลงชื่อสมัครใช้ ให้ตั้งค่า
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" }),
},
};
การยืนยัน
- การยืนยันคำถาม: ยืนยันคำถามอย่างปลอดภัยเทียบกับคำถามที่เชื่อมโยงกับเซสชันที่คาดไว้
- ยืนยันการแสดงตนของผู้ใช้
- ตรวจสอบว่าแฟล็ก User Present (UP) ที่แสดงในข้อมูลผูตรวจสอบสิทธิ์ที่แยกวิเคราะห์แล้วเป็น
trueเพื่อยืนยันว่าผู้ใช้แสดงตนในขณะที่สร้าง
- ตรวจสอบว่าแฟล็ก User Present (UP) ที่แสดงในข้อมูลผูตรวจสอบสิทธิ์ที่แยกวิเคราะห์แล้วเป็น
- การผ่อนคลายการยืนยันสำหรับ "preferred"
- เมื่อตัวเลือกการสร้างระบุ
userVerification: "preferred"คุณต้องกำหนดค่าการเรียกใช้การยืนยันฝั่งเซิร์ฟเวอร์ด้วยrequireUserVerification: falseไม่เช่นนั้น เครื่องมือตรวจสอบสิทธิ์ที่ลงทะเบียนโดยไม่มีการยืนยันตัวตนของผู้ใช้ (เช่น ปิดใช้การล็อกหน้าจอ) จะทำให้การยืนยันเซิร์ฟเวอร์ล้มเหลวโดยไม่ทราบสาเหตุ
- เมื่อตัวเลือกการสร้างระบุ
ตรรกะฝั่งไคลเอ็นต์
- จำกัด UI ในการโหลดหน้าเว็บ
- เมื่อโหลดหน้าเว็บ ให้เรียกใช้
PublicKeyCredential.getClientCapabilities()และปิดใช้ปุ่ม "สร้างพาสคีย์" หากconditionalGetหรือpasskeyPlatformAuthenticatorไม่พร้อมใช้งาน
- เมื่อโหลดหน้าเว็บ ให้เรียกใช้
- เรียกใช้การสร้างและทำให้เป็นอนุกรม: ถอดรหัสตัวเลือกเซิร์ฟเวอร์ด้วย
PublicKeyCredential.parseCreationOptionsFromJSON()และส่งการกำหนดค่าที่ได้ไปยังnavigator.credentials.create()- เรียกใช้
credential.toJSON()เพื่อเข้ารหัสAuthenticatorAttestationResponseเป็นออบเจ็กต์ที่แปลงเป็น JSON ได้และถูกต้องก่อนดึงข้อมูลปลายทางการยืนยัน
- เรียกใช้
- จัดการข้อยกเว้น WebAuthn:
InvalidStateError: มีพาสคีย์ที่ตรงกันอยู่แล้ว (จับคู่โดยexcludeCredentials)NotAllowedError: ผู้ใช้ยกเลิกหรือหมดเวลาในกล่องโต้ตอบพาสคีย์การตรวจสอบสิทธิ์AbortError: การดำเนินการถูกยกเลิกSecurityError: ข้อผิดพลาดเกี่ยวกับต้นทางที่ปลอดภัย (HTTPS) หรือรหัส RP ไม่ตรงกัน (ปัญหาการกำหนดค่า)
- การแยก 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';