ภาพรวม
ภาพรวมระดับสูงของขั้นตอนสำคัญที่เกี่ยวข้องกับการตรวจสอบสิทธิ์ด้วยพาสคีย์มีดังนี้

- กำหนดคำถามและตัวเลือกอื่นๆ ที่จำเป็นสำหรับการตรวจสอบสิทธิ์ด้วยพาสคีย์ ส่งข้อมูลเข้าสู่ระบบไปยังไคลเอ็นต์เพื่อให้คุณส่งต่อไปยังการเรียกใช้การตรวจสอบสิทธิ์พาสคีย์ (
navigator.credentials.getบนเว็บ) ได้ หลังจากผู้ใช้ยืนยันการตรวจสอบสิทธิ์ด้วยพาสคีย์ ระบบจะแก้ไขการเรียกใช้การตรวจสอบสิทธิ์ด้วยพาสคีย์และแสดงผลข้อมูลเข้าสู่ระบบ (PublicKeyCredential) ซึ่งข้อมูลเข้าสู่ระบบจะมีการยืนยันการตรวจสอบสิทธิ์
- ยืนยันการยืนยันการตรวจสอบสิทธิ์
- หากการยืนยันการตรวจสอบสิทธิ์ถูกต้อง ให้ตรวจสอบสิทธิ์ผู้ใช้
ส่วนต่อไปนี้จะเจาะลึกรายละเอียดของแต่ละขั้นตอน
สร้างชาเลนจ์
ในทางปฏิบัติ ชาเลนจ์คืออาร์เรย์ของไบต์แบบสุ่ม ซึ่งแสดงเป็นออบเจ็กต์ ArrayBuffer
// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8
คุณต้องทำสิ่งต่อไปนี้เพื่อให้การแข่งขันบรรลุวัตถุประสงค์
- ตรวจสอบว่าไม่มีการใช้คำถามยืนยันตัวตนเดียวกันมากกว่า 1 ครั้ง สร้างคำถามใหม่ทุกครั้งที่พยายามลงชื่อเข้าใช้ ทิ้งคำถามหลังจากการพยายามลงชื่อเข้าใช้ทุกครั้ง ไม่ว่าจะสำเร็จหรือไม่ก็ตาม และทิ้งการทดสอบหลังจากผ่านไประยะเวลาหนึ่งด้วย อย่ายอมรับคำถามเดียวกันในการตอบกลับมากกว่า 1 ครั้ง
- ตรวจสอบว่าคำถามมีความปลอดภัยในการเข้ารหัส การถามคำถามควรเป็นสิ่งที่เดาได้ยากมาก หากต้องการสร้างคำถามฝั่งเซิร์ฟเวอร์ที่ปลอดภัยด้วยการเข้ารหัสลับ คุณควรใช้ไลบรารีฝั่งเซิร์ฟเวอร์ FIDO ที่เชื่อถือได้ หากคุณสร้างคำถามยืนยันตัวตนของคุณเองแทน ให้ใช้ฟังก์ชันการเข้ารหัสในตัวที่มีอยู่ในชุดซอฟต์แวร์โครงสร้างพื้นฐานของคุณ หรือมองหาไลบรารีที่ออกแบบมาสำหรับกรณีการใช้งานการเข้ารหัส ตัวอย่างเช่น iso-crypto ใน Node.js หรือ secrets ใน Python ตามข้อกำหนด ชาเลนจ์ต้องมีความยาวอย่างน้อย 16 ไบต์จึงจะถือว่าปลอดภัย
เมื่อสร้างคำถามแล้ว ให้บันทึกไว้ในเซสชันของผู้ใช้เพื่อยืนยันในภายหลัง
สร้างตัวเลือกคำขอข้อมูลเข้าสู่ระบบ
สร้างตัวเลือกคำขอข้อมูลเข้าสู่ระบบเป็นออบเจ็กต์ publicKeyCredentialRequestOptions
โดยใช้ไลบรารีฝั่งเซิร์ฟเวอร์ FIDO โดยปกติแล้ว ไลบรารีจะเสนอฟังก์ชันยูทิลิตีที่สร้างตัวเลือกเหล่านี้ให้คุณได้ เช่น SimpleWebAuthn มี generateAuthenticationOptions
publicKeyCredentialRequestOptions ควรมีข้อมูลทั้งหมดที่จำเป็นสำหรับการตรวจสอบสิทธิ์ด้วยพาสคีย์ ส่งข้อมูลนี้ไปยังฟังก์ชันในไลบรารีฝั่งเซิร์ฟเวอร์ FIDO ที่มีหน้าที่สร้างออบเจ็กต์ publicKeyCredentialRequestOptions
ฟิลด์บางฟิลด์ของ publicKeyCredentialRequestOptions อาจเป็นค่าคงที่ ส่วนอื่นๆ ควรกำหนดแบบไดนามิกบนเซิร์ฟเวอร์ ดังนี้
rpId: รหัส RP ที่คุณคาดหวังให้เชื่อมโยงกับข้อมูลเข้าสู่ระบบ เช่นexample.comการตรวจสอบสิทธิ์จะสำเร็จก็ต่อเมื่อรหัส RP ที่คุณระบุที่นี่ตรงกับรหัส RP ที่เชื่อมโยงกับข้อมูลเข้าสู่ระบบ หากต้องการป้อนข้อมูลรหัส RP ให้ใช้ค่าเดียวกับรหัส RP ที่คุณตั้งค่าไว้ในpublicKeyCredentialCreationOptionsระหว่างการลงทะเบียนข้อมูลเข้าสู่ระบบchallenge: ข้อมูลที่ผู้ให้บริการพาสคีย์จะลงชื่อเพื่อพิสูจน์ว่าผู้ใช้มีพาสคีย์ ณ เวลาที่ส่งคำขอตรวจสอบสิทธิ์ ตรวจสอบรายละเอียดในหัวข้อสร้างชาเลนจ์allowCredentials: อาร์เรย์ของข้อมูลเข้าสู่ระบบที่ยอมรับได้สำหรับการตรวจสอบสิทธิ์นี้ ส่งอาร์เรย์ว่างเพื่อให้ผู้ใช้เลือกพาสคีย์ที่ใช้ได้จากรายการที่เบราว์เซอร์แสดง ดูรายละเอียดได้ที่ดึงคำถามจากเซิร์ฟเวอร์ RP และข้อมูลเจาะลึกเกี่ยวกับข้อมูลเข้าสู่ระบบที่ค้นพบได้userVerification: ระบุว่าการยืนยันตัวตนผู้ใช้โดยใช้การล็อกหน้าจออุปกรณ์เป็นแบบ "ต้องใช้" "แนะนำ" หรือ "ไม่แนะนำ" ดูหัวข้อดึงคำถามจากเซิร์ฟเวอร์ RPtimeout: ระยะเวลา (เป็นมิลลิวินาที) ที่ผู้ใช้ใช้ในการตรวจสอบสิทธิ์จนเสร็จสมบูรณ์ โดยควรมีระยะเวลาที่เหมาะสมและสั้นกว่าอายุการใช้งานของchallengeค่าเริ่มต้นที่แนะนำคือ 5 นาที แต่คุณสามารถเพิ่มได้สูงสุด 10 นาที ซึ่งยังคงอยู่ในช่วงที่แนะนำ ระยะหมดเวลาที่นานจะเหมาะสมหากคุณคาดหวังให้ผู้ใช้ใช้เวิร์กโฟลว์แบบผสม ซึ่งโดยปกติจะใช้เวลานานกว่าเล็กน้อย หากการดำเนินการหมดเวลา ระบบจะแสดงข้อผิดพลาดNotAllowedError
เมื่อสร้าง publicKeyCredentialRequestOptions แล้ว ให้ส่งให้ลูกค้า
challenge การถอดรหัสจะเกิดขึ้นฝั่งไคลเอ็นต์โค้ดตัวอย่าง: สร้างตัวเลือกคำขอข้อมูลเข้าสู่ระบบ
เราใช้ไลบรารี SimpleWebAuthn ในตัวอย่าง ในส่วนนี้ เราจะส่งต่อการสร้างตัวเลือกคำขอข้อมูลเข้าสู่ระบบไปยังฟังก์ชัน generateAuthenticationOptions
import {
generateAuthenticationOptions,
} from '@simplewebauthn/server';
router.post('/signinRequest', csrfCheck, async (req, res) => {
// Ensure you nest 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 {
// Use the generateAuthenticationOptions function from SimpleWebAuthn
const options = await generateAuthenticationOptions({
rpID: process.env.HOSTNAME,
allowCredentials: [],
});
// Save the challenge in the user session
req.session.challenge = options.challenge;
return res.json(options);
} catch (e) {
console.error(e);
return res.status(400).json({ error: e.message });
}
});
ยืนยันและลงชื่อเข้าใช้ให้ผู้ใช้
เมื่อ navigator.credentials.get แก้ไขปัญหาในฝั่งไคลเอ็นต์สำเร็จ ระบบจะแสดงผลออบเจ็กต์ PublicKeyCredential
navigator.credentials.get จะแสดงผลเป็น PublicKeyCredentialresponse เป็น AuthenticatorAssertionResponse ซึ่งแสดงถึงการตอบสนองของผู้ให้บริการพาสคีย์ต่อคำสั่งของไคลเอ็นต์ในการสร้างสิ่งที่จำเป็นในการลองและตรวจสอบสิทธิ์ด้วยพาสคีย์ใน RP ซึ่งประกอบด้วย
response.authenticatorDataและresponse.clientDataJSONเช่น ในขั้นตอนการลงทะเบียนพาสคีย์response.signatureซึ่งมีลายเซ็นเหนือค่าเหล่านี้
ส่งออบเจ็กต์ PublicKeyCredential ไปยังเซิร์ฟเวอร์
ในเซิร์ฟเวอร์ ให้ทำดังนี้
- รวบรวมข้อมูลที่จำเป็นในการยืนยันการยืนยันสิทธิ์และตรวจสอบสิทธิ์ผู้ใช้
- รับคำถามที่คาดไว้ซึ่งคุณจัดเก็บไว้ในเซสชันเมื่อสร้างตัวเลือกการตรวจสอบสิทธิ์
- รับ origin และรหัส RP ที่คาดไว้
- ค้นหาผู้ใช้ในฐานข้อมูล ในกรณีของข้อมูลเข้าสู่ระบบที่ค้นพบได้ คุณจะไม่ทราบว่าผู้ใช้รายใดส่งคำขอตรวจสอบสิทธิ์ คุณมี 2 ตัวเลือกในการตรวจสอบ ดังนี้
- ตัวเลือกที่ 1: ใช้
response.userHandleในออบเจ็กต์PublicKeyCredentialในตารางผู้ใช้ ให้มองหาpasskey_user_idที่ตรงกับuserHandle - ตัวเลือกที่ 2: ใช้ข้อมูลเข้าสู่ระบบ
idที่อยู่ในออบเจ็กต์PublicKeyCredentialในตารางข้อมูลเข้าสู่ระบบคีย์สาธารณะ ให้มองหาข้อมูลเข้าสู่ระบบidที่ตรงกับข้อมูลเข้าสู่ระบบidที่อยู่ในออบเจ็กต์PublicKeyCredentialจากนั้นค้นหาผู้ใช้ที่เกี่ยวข้องโดยใช้คีย์นอกpasskey_user_idไปยังตาราง Users
- ตัวเลือกที่ 1: ใช้
- ค้นหาข้อมูลเข้าสู่ระบบคีย์สาธารณะในฐานข้อมูลที่ตรงกับการยืนยันการตรวจสอบสิทธิ์ที่คุณได้รับ โดยให้ค้นหาข้อมูลเข้าสู่ระบบ
idที่ตรงกับข้อมูลเข้าสู่ระบบidที่มีอยู่ในออบเจ็กต์PublicKeyCredentialในตารางข้อมูลเข้าสู่ระบบคีย์สาธารณะ
ยืนยันการยืนยันการตรวจสอบสิทธิ์ ส่งต่อขั้นตอนการยืนยันนี้ไปยังไลบรารีฝั่งเซิร์ฟเวอร์ FIDO ซึ่งโดยปกติแล้วจะมีฟังก์ชันยูทิลิตีสำหรับวัตถุประสงค์นี้ เช่น SimpleWebAuthn มี
verifyAuthenticationResponseดูสิ่งที่เกิดขึ้นเบื้องหลังได้ในภาคผนวก: การยืนยันการตอบกลับการตรวจสอบสิทธิ์ลบคำถามยืนยันไม่ว่าการยืนยันจะสำเร็จหรือไม่ เพื่อป้องกันการโจมตีแบบ Replay Attack
ลงชื่อเข้าใช้ให้ผู้ใช้ หากการยืนยันสำเร็จ ให้อัปเดตข้อมูลเซสชันเพื่อทำเครื่องหมายผู้ใช้ว่าลงชื่อเข้าใช้แล้ว คุณอาจต้องแสดงผลออบเจ็กต์
userไปยังไคลเอ็นต์ด้วย เพื่อให้ฟรอนท์เอนด์ใช้ข้อมูลที่เชื่อมโยงกับผู้ใช้ที่เพิ่งลงชื่อเข้าใช้ได้
โค้ดตัวอย่าง: ยืนยันและลงชื่อเข้าใช้ให้ผู้ใช้
เราใช้ไลบรารี SimpleWebAuthn ในตัวอย่าง ในขั้นตอนนี้ เราจะส่งต่อการยืนยันการตอบกลับการตรวจสอบสิทธิ์ไปยังฟังก์ชัน verifyAuthenticationResponse
import {
verifyAuthenticationResponse,
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/signinResponse', csrfCheck, async (req, res) => {
const response = req.body;
const expectedChallenge = req.session.challenge;
const expectedOrigin = getOrigin(req.get('User-Agent'));
const expectedRPID = process.env.HOSTNAME;
// 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 {
// Find the credential stored to the database by the credential ID
const cred = Credentials.findById(response.id);
if (!cred) {
throw new Error('Credential not found.');
}
// Find the user - Here alternatively we could look up the user directly
// in the Users table via userHandle
const user = Users.findByPasskeyUserId(cred.passkey_user_id);
if (!user) {
throw new Error('User not found.');
}
// Prepare credential information for verification
const credential = {
id: cred.id,
publicKey: isoBase64URL.toBuffer(cred.publicKey),
transports: cred.transports,
};
// Verify the credential
const { verified, authenticationInfo } = await verifyAuthenticationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
credential,
requireUserVerification: false,
});
if (!verified) {
throw new Error('User verification failed.');
}
// Kill the challenge for this session.
delete req.session.challenge;
req.session.username = user.username;
req.session['signed-in'] = 'yes';
return res.json(user);
} catch (e) {
delete req.session.challenge;
console.error(e);
return res.status(400).json({ error: e.message });
}
});
ภาคผนวก: การยืนยันการตอบกลับการตรวจสอบสิทธิ์
การยืนยันการตอบกลับการตรวจสอบสิทธิ์ประกอบด้วยการตรวจสอบต่อไปนี้
- ตรวจสอบว่ารหัส RP ตรงกับเว็บไซต์ของคุณ
- ตรวจสอบว่าต้นทางของคำขอตรงกับต้นทางการลงชื่อเข้าใช้ของเว็บไซต์ สำหรับแอป Android โปรดอ่านหัวข้อยืนยันต้นทาง
- ตรวจสอบว่าอุปกรณ์สามารถให้คำถามที่คุณระบุได้
- ยืนยันว่าในระหว่างการตรวจสอบสิทธิ์ ผู้ใช้ได้ปฏิบัติตามข้อกำหนดที่คุณกำหนดในฐานะ RP หากคุณกำหนดให้มีการยืนยันผู้ใช้ ให้ตรวจสอบว่าแฟล็ก
uv(ผู้ใช้ที่ยืนยันแล้ว) ในauthenticatorDataเป็นtrueตรวจสอบว่าแฟล็กup(ผู้ใช้อยู่) ในauthenticatorDataเป็นtrueเนื่องจากพาสคีย์ต้องมีผู้ใช้อยู่เสมอ - ยืนยันลายเซ็น หากต้องการยืนยันลายเซ็น คุณต้องมีสิ่งต่อไปนี้
- ลายเซ็นซึ่งเป็นคำถามที่ลงชื่อแล้ว:
response.signature - คีย์สาธารณะเพื่อยืนยันลายเซ็น
- ข้อมูลที่ลงนามต้นฉบับ นี่คือข้อมูลที่จะต้องยืนยันลายเซ็น
- อัลกอริทึมการเข้ารหัสที่ใช้สร้างลายเซ็น
- ลายเซ็นซึ่งเป็นคำถามที่ลงชื่อแล้ว:
ดูข้อมูลเพิ่มเติมเกี่ยวกับขั้นตอนเหล่านี้ได้ที่ซอร์สโค้ดสำหรับ verifyAuthenticationResponse ของ SimpleWebAuthn หรือเจาะลึกรายการการยืนยันทั้งหมดในข้อกำหนดเฉพาะ