ภาพรวม
ภาพรวมระดับสูงของขั้นตอนสำคัญที่เกี่ยวข้องกับการลงทะเบียนพาสคีย์มีดังนี้
- กำหนดตัวเลือกเพื่อสร้างพาสคีย์ ส่งค่าเหล่านี้ให้ไคลเอ็นต์เพื่อให้คุณส่งค่าไปยังการเรียกใช้การสร้างพาสคีย์ได้ ซึ่งก็คือการเรียกใช้ WebAuthn API
navigator.credentials.create
บนเว็บ และcredentialManager.createCredential
ใน Android หลังจากที่ผู้ใช้ยืนยันการสร้างพาสคีย์แล้ว ระบบจะแก้ไขการเรียกการสร้างพาสคีย์และแสดงผลข้อมูลเข้าสู่ระบบPublicKeyCredential
- ยืนยันข้อมูลเข้าสู่ระบบและจัดเก็บไว้ในเซิร์ฟเวอร์
ส่วนต่อไปนี้จะเจาะลึกรายละเอียดของแต่ละขั้นตอน
สร้างตัวเลือกการสร้างข้อมูลเข้าสู่ระบบ
ขั้นตอนแรกที่คุณต้องทำในเซิร์ฟเวอร์คือการสร้างออบเจ็กต์ PublicKeyCredentialCreationOptions
โดยใช้ไลบรารีฝั่งเซิร์ฟเวอร์ FIDO โดยปกติแล้วจะเสนอฟังก์ชันยูทิลิตีที่สร้างตัวเลือกเหล่านี้ให้คุณได้ ตัวอย่างเช่น SimpleWebAuthn มี generateRegistrationOptions
PublicKeyCredentialCreationOptions
ควรมีทุกอย่างที่จำเป็นสำหรับการสร้างพาสคีย์ ได้แก่ ข้อมูลเกี่ยวกับผู้ใช้ เกี่ยวกับ RP และการกำหนดค่าสำหรับพร็อพเพอร์ตี้ของข้อมูลเข้าสู่ระบบที่คุณกำลังสร้าง เมื่อกำหนดค่าทั้งหมดนี้แล้ว ให้ส่งค่าตามที่จำเป็นไปยังฟังก์ชันในไลบรารีฝั่งเซิร์ฟเวอร์ของ FIDO ที่รับผิดชอบในการสร้างออบเจ็กต์ PublicKeyCredentialCreationOptions
ฟิลด์บางรายการของ PublicKeyCredentialCreationOptions
อาจเป็นค่าคงที่ได้ ส่วนอื่นๆ ควรมีการกำหนดแบบไดนามิกในเซิร์ฟเวอร์
rpId
: หากต้องการป้อนข้อมูลรหัส RP ในเซิร์ฟเวอร์ ให้ใช้ฟังก์ชันหรือตัวแปรฝั่งเซิร์ฟเวอร์ที่ให้ชื่อโฮสต์ของเว็บแอปพลิเคชัน เช่นexample.com
user.name
และuser.displayName
: หากต้องการป้อนข้อมูลในช่องเหล่านี้ ให้ใช้ข้อมูลเซสชันของผู้ใช้ที่ลงชื่อเข้าใช้ (หรือข้อมูลบัญชีผู้ใช้ใหม่ หากผู้ใช้สร้างพาสคีย์เมื่อลงชื่อสมัครใช้)user.name
โดยปกติจะเป็นอีเมลและไม่ซ้ำกันสำหรับ RPuser.displayName
คือชื่อที่เรียกง่าย โปรดทราบว่าบางแพลตฟอร์มอาจไม่ใช้displayName
user.id
: สตริงที่ไม่ซ้ำแบบสุ่มซึ่งสร้างขึ้นเมื่อสร้างบัญชี โดยควรเป็นชื่อที่ถาวร ซึ่งแตกต่างจากชื่อผู้ใช้ที่อาจแก้ไขได้ รหัสผู้ใช้จะระบุบัญชี แต่ไม่ควรมีข้อมูลส่วนบุคคลที่ระบุตัวบุคคลนั้นได้ (PII) คุณอาจมีรหัสผู้ใช้อยู่แล้วในระบบ แต่หากจำเป็น ให้สร้างรหัสผู้ใช้สำหรับพาสคีย์โดยเฉพาะเพื่อไม่ให้มี PIIexcludeCredentials
: รายการรหัสของข้อมูลเข้าสู่ระบบที่มีอยู่เพื่อป้องกันการทำซ้ำพาสคีย์จากผู้ให้บริการพาสคีย์ หากต้องการป้อนข้อมูลในช่องนี้ ให้ค้นหาข้อมูลเข้าสู่ระบบที่มีอยู่สำหรับผู้ใช้รายนี้ในฐานข้อมูล ดูรายละเอียดได้ที่ป้องกันการสร้างพาสคีย์ใหม่หากมีอยู่แล้วchallenge
: สำหรับการลงทะเบียนข้อมูลเข้าสู่ระบบ คำถามนี้ไม่เกี่ยวข้อง เว้นแต่คุณจะใช้การรับรอง ซึ่งเป็นเทคนิคขั้นสูงกว่าในการยืนยันตัวตนของผู้ให้บริการพาสคีย์และข้อมูลที่ผู้ให้บริการปล่อยออกมา อย่างไรก็ตาม แม้ว่าคุณจะไม่ได้ใช้การรับรอง แต่การท้าทายก็ยังคงเป็นช่องที่ต้องระบุ ดูวิธีการสร้างคำท้าที่ปลอดภัยสำหรับการตรวจสอบสิทธิ์ได้ในการตรวจสอบสิทธิ์ด้วยพาสคีย์ฝั่งเซิร์ฟเวอร์
การเข้ารหัสและถอดรหัส

PublicKeyCredentialCreationOptions
ที่เซิร์ฟเวอร์ส่ง challenge
user.id
และ excludeCredentials.credentials
ต้องได้รับการเข้ารหัสฝั่งเซิร์ฟเวอร์เป็น base64URL
เพื่อให้ส่ง PublicKeyCredentialCreationOptions
ผ่าน HTTPS ได้PublicKeyCredentialCreationOptions
มีฟิลด์ที่เป็น ArrayBuffer
จึงไม่รองรับใน JSON.stringify()
ซึ่งหมายความว่าในขณะนี้ หากต้องการส่ง PublicKeyCredentialCreationOptions
ผ่าน HTTPS คุณจะต้องเข้ารหัสบางช่องด้วยตนเองในเซิร์ฟเวอร์โดยใช้ base64URL
แล้วถอดรหัสในไคลเอ็นต์
- ในเซิร์ฟเวอร์ โดยปกติแล้วไลบรารีฝั่งเซิร์ฟเวอร์ FIDO จะจัดการการเข้ารหัสและถอดรหัส
- ในไคลเอ็นต์ การเข้ารหัสและถอดรหัสต้องดำเนินการด้วยตนเองในขณะนี้ ในอนาคตจะง่ายขึ้น โดยจะมีเมธอดในการแปลงตัวเลือกเป็น JSON ใน
PublicKeyCredentialCreationOptions
ดูสถานะการใช้งานใน Chrome
ตัวอย่างโค้ด: สร้างตัวเลือกการสร้างข้อมูลเข้าสู่ระบบ
เราใช้ไลบรารี SimpleWebAuthn ในตัวอย่าง ในที่นี้ เราจะส่งต่อการสร้างตัวเลือกข้อมูลเข้าสู่ระบบคีย์สาธารณะไปยังฟังก์ชัน generateRegistrationOptions
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
const { user } = res.locals;
// Ensure you nest verification function calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// `excludeCredentials` prevents users from re-registering existing
// credentials for a given passkey provider
const excludeCredentials = [];
const credentials = Credentials.findByUserId(user.id);
if (credentials.length > 0) {
for (const cred of credentials) {
excludeCredentials.push({
id: isoBase64URL.toBuffer(cred.id),
type: 'public-key',
transports: cred.transports,
});
}
}
// Generate registration options for WebAuthn create
const options = await generateRegistrationOptions({
rpName: process.env.RP_NAME,
rpID: process.env.HOSTNAME,
userID: user.id,
userName: user.username,
userDisplayName: user.displayName || '',
attestationType: 'none',
excludeCredentials,
authenticatorSelection: {
authenticatorAttachment: 'platform',
requireResidentKey: true
},
});
// Keep the challenge in the session
req.session.challenge = options.challenge;
return res.json(options);
} catch (e) {
console.error(e);
return res.status(400).send({ error: e.message });
}
});
จัดเก็บคีย์สาธารณะ

navigator.credentials.create
จะแสดงผลออบเจ็กต์ PublicKeyCredential
เมื่อ navigator.credentials.create
แก้ไขสำเร็จในไคลเอ็นต์ หมายความว่าสร้างพาสคีย์สำเร็จแล้ว ระบบจะแสดงผลออบเจ็กต์ PublicKeyCredential
ออบเจ็กต์ PublicKeyCredential
มีออบเจ็กต์ AuthenticatorAttestationResponse
ซึ่งแสดงถึงการตอบกลับของผู้ให้บริการพาสคีย์ต่อคำสั่งของไคลเอ็นต์ในการสร้างพาสคีย์ โดยจะมีข้อมูลเกี่ยวกับข้อมูลเข้าสู่ระบบใหม่ที่คุณต้องใช้ในฐานะ RP เพื่อตรวจสอบสิทธิ์ผู้ใช้ในภายหลัง ดูข้อมูลเพิ่มเติมเกี่ยวกับ AuthenticatorAttestationResponse
ในภาคผนวก: AuthenticatorAttestationResponse
ส่งออบเจ็กต์ PublicKeyCredential
ไปยังเซิร์ฟเวอร์ เมื่อได้รับแล้ว ให้ยืนยัน
ส่งต่อขั้นตอนการยืนยันนี้ไปยังไลบรารีฝั่งเซิร์ฟเวอร์ของ FIDO โดยปกติแล้วฟังก์ชันนี้จะมีฟังก์ชันยูทิลิตีสำหรับวัตถุประสงค์นี้ ตัวอย่างเช่น SimpleWebAuthn มี verifyRegistrationResponse
ดูสิ่งที่เกิดขึ้นเบื้องหลังได้ในภาคผนวก: การยืนยันการตอบกลับการจดทะเบียน
เมื่อยืนยันสำเร็จแล้ว ให้จัดเก็บข้อมูลเข้าสู่ระบบในฐานข้อมูลเพื่อให้ผู้ใช้สามารถตรวจสอบสิทธิ์ด้วยพาสคีย์ที่เชื่อมโยงกับข้อมูลเข้าสู่ระบบนั้นได้ในภายหลัง
ใช้ตารางเฉพาะสำหรับข้อมูลเข้าสู่ระบบคีย์สาธารณะที่เชื่อมโยงกับพาสคีย์ ผู้ใช้จะมีรหัสผ่านได้เพียงรหัสเดียว แต่มีพาสคีย์ได้หลายรายการ เช่น พาสคีย์ที่ซิงค์ผ่าน Apple iCloud Keychain และพาสคีย์ที่ซิงค์ผ่านเครื่องมือจัดการรหัสผ่านบน Google
ต่อไปนี้คือตัวอย่างสคีมาที่คุณใช้จัดเก็บข้อมูลเข้าสู่ระบบได้
- ตารางผู้ใช้
user_id
: รหัสผู้ใช้หลัก รหัสแบบสุ่มที่ไม่ซ้ำกันและถาวรสำหรับผู้ใช้ ใช้เป็นคีย์หลักสำหรับตารางผู้ใช้username
ชื่อผู้ใช้ที่กำหนดโดยผู้ใช้ ซึ่งอาจแก้ไขได้passkey_user_id
: รหัสผู้ใช้ที่ไม่มี PII เฉพาะพาสคีย์ ซึ่งแสดงโดยuser.id
ในตัวเลือกการลงทะเบียน เมื่อผู้ใช้พยายามตรวจสอบสิทธิ์ในภายหลัง เครื่องมือตรวจสอบสิทธิ์จะทำให้passkey_user_id
นี้พร้อมใช้งานในการตอบกลับการตรวจสอบสิทธิ์ในuserHandle
เราขอแนะนำว่าอย่าตั้งค่าpasskey_user_id
เป็นคีย์หลัก คีย์หลักมักจะกลายเป็น PII โดยพฤตินัยในระบบเนื่องจากมีการใช้งานอย่างกว้างขวาง
- ตารางข้อมูลเข้าสู่ระบบคีย์สาธารณะ
id
: รหัสข้อมูลประจำตัว ใช้รหัสนี้เป็นคีย์หลักสำหรับตารางข้อมูลเข้าสู่ระบบคีย์สาธารณะpublic_key
: คีย์สาธารณะของข้อมูลเข้าสู่ระบบpasskey_user_id
: ใช้เป็นคีย์นอกเพื่อสร้างลิงก์กับตารางผู้ใช้backed_up
: พาสคีย์จะสำรองข้อมูลหากผู้ให้บริการพาสคีย์ซิงค์พาสคีย์ การจัดเก็บสถานะการสำรองข้อมูลมีประโยชน์หากคุณต้องการพิจารณาเลิกใช้รหัสผ่านในอนาคตสำหรับผู้ใช้ที่มีพาสคีย์backed_up
คุณสามารถตรวจสอบว่ามีการสำรองข้อมูลพาสคีย์หรือไม่โดยดูที่แฟล็ก BE ในauthenticatorData
หรือใช้ฟีเจอร์ไลบรารีฝั่งเซิร์ฟเวอร์ของ FIDO ซึ่งโดยปกติแล้วจะพร้อมใช้งานเพื่อให้คุณเข้าถึงข้อมูลนี้ได้ง่าย การจัดเก็บการมีสิทธิ์สำรองข้อมูลอาจเป็นประโยชน์ในการตอบคำถามที่อาจเกิดขึ้นจากผู้ใช้name
: ชื่อที่แสดงสำหรับข้อมูลเข้าสู่ระบบ (ไม่บังคับ) เพื่อให้ผู้ใช้ตั้งชื่อที่กำหนดเองสำหรับข้อมูลเข้าสู่ระบบได้transports
: อาร์เรย์ของ transports การจัดเก็บการรับส่งมีประโยชน์ต่อประสบการณ์ของผู้ใช้ในการตรวจสอบสิทธิ์ เมื่อมีการขนส่ง เบราว์เซอร์จะทำงานตามนั้นและแสดง UI ที่ตรงกับการขนส่งที่ผู้ให้บริการพาสคีย์ใช้ในการสื่อสารกับไคลเอ็นต์ โดยเฉพาะอย่างยิ่งสำหรับกรณีการใช้งานการตรวจสอบสิทธิ์ซ้ำที่allowCredentials
ไม่ว่างเปล่า
ข้อมูลอื่นๆ อาจมีประโยชน์ในการจัดเก็บเพื่อวัตถุประสงค์ด้านประสบการณ์ของผู้ใช้ ซึ่งรวมถึงรายการต่างๆ เช่น ผู้ให้บริการพาสคีย์ เวลาที่สร้างข้อมูลเข้าสู่ระบบ และเวลาที่ใช้ล่าสุด อ่านเพิ่มเติมได้ในการออกแบบอินเทอร์เฟซผู้ใช้ของพาสคีย์
ตัวอย่างโค้ด: จัดเก็บข้อมูลเข้าสู่ระบบ
เราใช้ไลบรารี SimpleWebAuthn ในตัวอย่าง
ในที่นี้ เราจะส่งต่อการยืนยันการตอบกลับการลงทะเบียนไปยังฟังก์ชัน verifyRegistrationResponse
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
const expectedChallenge = req.session.challenge;
const expectedOrigin = getOrigin(req.get('User-Agent'));
const expectedRPID = process.env.HOSTNAME;
const response = req.body;
// This sample code is for registering a passkey for an existing,
// signed-in user
// Ensure you nest verification function calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// Verify the credential
const { verified, registrationInfo } = await verifyRegistrationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
requireUserVerification: false,
});
if (!verified) {
throw new Error('Verification failed.');
}
const {
aaguid,
credentialPublicKey,
credentialID,
credentialBackedUp
} = registrationInfo;
// Name the credential based on AAGUID
const name =
aaguid === undefined ||
aaguid === '000000-0000-0000-0000-00000000' ?
req.useragent?.platform : aaguids[aaguid].name;
const base64CredentialID = isoBase64URL.fromBuffer(credentialID);
const base64PublicKey = isoBase64URL.fromBuffer(credentialPublicKey);
// Existing, signed-in user
const { user } = res.locals;
// Save the credential
await Credentials.update({
id: base64CredentialID,
passkey_user_id: user.passkey_user_id,
publicKey: base64PublicKey,
name,
aaguid,
transports: response.response.transports,
backed_up: credentialBackedUp,
registered_at: new Date().getTime()
});
// Kill the challenge for this session
delete req.session.challenge;
return res.json(user);
} catch (e) {
delete req.session.challenge;
console.error(e);
return res.status(400).send({ error: e.message });
}
});
ภาคผนวก: AuthenticatorAttestationResponse
AuthenticatorAttestationResponse
มีออบเจ็กต์สำคัญ 2 รายการ ได้แก่
response.clientDataJSON
คือข้อมูลไคลเอ็นต์ เวอร์ชัน JSON ซึ่งในเว็บคือข้อมูลที่เบราว์เซอร์เห็น โดยมีต้นทาง RP, ชาเลนจ์ และandroidPackageName
หากไคลเอ็นต์เป็นแอป Android ในฐานะ RP การอ่านclientDataJSON
จะให้สิทธิ์เข้าถึงข้อมูลที่เบราว์เซอร์เห็นในขณะที่ส่งคำขอcreate
response.attestationObject
ประกอบด้วยข้อมูล 2 ส่วน ดังนี้attestationStatement
ซึ่งไม่เกี่ยวข้องเว้นแต่คุณจะใช้การรับรองauthenticatorData
คือข้อมูลที่ผู้ให้บริการพาสคีย์เห็น ในฐานะ RP การอ่านauthenticatorData
จะให้สิทธิ์เข้าถึงข้อมูลที่ผู้ให้บริการพาสคีย์เห็นและส่งกลับมาในเวลาที่create
ขอ
authenticatorData
มีข้อมูลสำคัญเกี่ยวกับข้อมูลเข้าสู่ระบบคีย์สาธารณะที่เชื่อมโยงกับพาสคีย์ที่สร้างขึ้นใหม่
- ข้อมูลเข้าสู่ระบบคีย์สาธารณะเองและรหัสข้อมูลเข้าสู่ระบบที่ไม่ซ้ำกันสำหรับข้อมูลเข้าสู่ระบบนั้น
- รหัส RP ที่เชื่อมโยงกับข้อมูลเข้าสู่ระบบ
- Flag ที่อธิบายสถานะผู้ใช้เมื่อสร้างพาสคีย์ ได้แก่ ผู้ใช้มีตัวตนจริงหรือไม่ และยืนยันตัวตนของผู้ใช้สำเร็จหรือไม่ (ดูเจาะลึกการยืนยันตัวตนของผู้ใช้)
- AAGUID คือตัวระบุสำหรับผู้ให้บริการพาสคีย์ เช่น เครื่องมือจัดการรหัสผ่านบน Google คุณสามารถระบุผู้ให้บริการพาสคีย์และแสดงชื่อในหน้าการจัดการพาสคีย์ได้โดยอิงตาม AAGUID (ดูระบุผู้ให้บริการพาสคีย์ด้วย AAGUID)
แม้ว่า authenticatorData
จะซ้อนอยู่ภายใน attestationObject
แต่ข้อมูลที่อยู่ในนั้นจำเป็นต่อการติดตั้งใช้งานพาสคีย์ ไม่ว่าคุณจะใช้การรับรองหรือไม่ก็ตาม authenticatorData
ได้รับการเข้ารหัส และมีฟิลด์ที่เข้ารหัสในรูปแบบไบนารี โดยปกติแล้ว ไลบรารีฝั่งเซิร์ฟเวอร์จะจัดการการแยกวิเคราะห์และการถอดรหัส หากไม่ได้ใช้ไลบรารีฝั่งเซิร์ฟเวอร์ ให้พิจารณาใช้getAuthenticatorData()
ฝั่งไคลเอ็นต์เพื่อประหยัดงานการแยกวิเคราะห์และการถอดรหัสฝั่งเซิร์ฟเวอร์
ภาคผนวก: การยืนยันการตอบกลับการลงทะเบียน
การยืนยันการตอบกลับการลงทะเบียนประกอบด้วยการตรวจสอบต่อไปนี้
- ตรวจสอบว่ารหัส RP ตรงกับเว็บไซต์ของคุณ
- ตรวจสอบว่าต้นทางของคำขอเป็นต้นทางที่คาดไว้สำหรับเว็บไซต์ (URL ของเว็บไซต์หลัก, แอป Android)
- หากกำหนดให้มีการยืนยันผู้ใช้ ให้ตรวจสอบว่าค่าสถานะการยืนยันผู้ใช้
authenticatorData.uv
เป็นtrue
- โดยปกติแล้วคาดว่าแฟล็กสถานะการใช้งานของผู้ใช้
authenticatorData.up
จะเป็นtrue
แต่หากมีการสร้างข้อมูลเข้าสู่ระบบแบบมีเงื่อนไข คาดว่าแฟล็กจะเป็นfalse
- ตรวจสอบว่าไคลเอ็นต์สามารถให้คำท้าที่คุณมอบให้ได้ หากไม่ได้ใช้การรับรอง การตรวจสอบนี้ก็ไม่สำคัญ อย่างไรก็ตาม การตรวจสอบนี้เป็นแนวทางปฏิบัติแนะนำ เนื่องจากจะช่วยให้โค้ดพร้อมใช้งานหากคุณตัดสินใจใช้การรับรองในอนาคต
- ตรวจสอบว่ายังไม่มีการลงทะเบียนรหัสข้อมูลเข้าสู่ระบบสำหรับผู้ใช้รายใด
- ตรวจสอบว่าอัลกอริทึมที่ผู้ให้บริการพาสคีย์ใช้เพื่อสร้างข้อมูลเข้าสู่ระบบเป็นอัลกอริทึมที่คุณระบุไว้ (ในช่อง
alg
แต่ละช่องของpublicKeyCredentialCreationOptions.pubKeyCredParams
ซึ่งโดยปกติจะกำหนดไว้ในไลบรารีฝั่งเซิร์ฟเวอร์และคุณมองไม่เห็น) ซึ่งวิธีนี้ช่วยให้มั่นใจได้ว่าผู้ใช้จะลงทะเบียนได้เฉพาะกับอัลกอริทึมที่คุณเลือกอนุญาตเท่านั้น
ดูข้อมูลเพิ่มเติมได้ที่ซอร์สโค้ดของ verifyRegistrationResponse
ใน SimpleWebAuthn หรือดูรายการการยืนยันทั้งหมดในข้อกำหนด