ภาพรวม
ภาพรวมระดับสูงของขั้นตอนสำคัญที่เกี่ยวข้องกับการตรวจสอบสิทธิ์ด้วยพาสคีย์มีดังนี้
- กำหนดคำถามและตัวเลือกอื่นๆ ที่จำเป็นในการตรวจสอบสิทธิ์ด้วยพาสคีย์ ส่งไปยังไคลเอ็นต์เพื่อให้ส่งต่อการโทรไปยังการตรวจสอบสิทธิ์ด้วยพาสคีย์ (
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
แล้ว ให้ส่งให้กับลูกค้า
โค้ดตัวอย่าง: สร้างตัวเลือกคำขอข้อมูลเข้าสู่ระบบ
เราใช้ไลบรารี SimpleWebAuthn ในตัวอย่างของเรา ในส่วนนี้ เราจะส่งต่อการสร้างตัวเลือกคำขอข้อมูลเข้าสู่ระบบให้กับฟังก์ชัน generateAuthenticationOptions
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} 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
response
เป็น 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
ในตารางผู้ใช้
- ตัวเลือกที่ 1: ใช้
- ค้นหาข้อมูลเข้าสู่ระบบคีย์สาธารณะที่ตรงกับการยืนยันการตรวจสอบสิทธิ์ที่คุณได้รับในฐานข้อมูล โดยมองหาข้อมูลเข้าสู่ระบบ
id
ที่ตรงกับข้อมูลเข้าสู่ระบบid
ที่มีอยู่ในออบเจ็กต์PublicKeyCredential
ในตารางข้อมูลเข้าสู่ระบบคีย์สาธารณะ
ยืนยันการยืนยันการตรวจสอบสิทธิ์ ส่งขั้นตอนการยืนยันนี้ไปยังไลบรารีฝั่งเซิร์ฟเวอร์ FIDO ซึ่งโดยปกติแล้วจะมีฟังก์ชันยูทิลิตีเพื่อวัตถุประสงค์นี้ SimpleWebAuthn มีข้อเสนอ เช่น
verifyAuthenticationResponse
ดูสิ่งที่เกิดขึ้นเบื้องหลังได้ในภาคผนวก: การยืนยันการตอบกลับการตรวจสอบสิทธิ์ลบภารกิจไม่ว่าการยืนยันจะสำเร็จหรือไม่ก็ตาม เพื่อป้องกันการโจมตีซ้ำ
ลงชื่อเข้าใช้ผู้ใช้ หากยืนยันสำเร็จ ให้อัปเดตข้อมูลเซสชันเพื่อทำเครื่องหมายผู้ใช้ว่าลงชื่อเข้าใช้แล้ว คุณอาจต้องแสดงผลออบเจ็กต์
user
ไปยังไคลเอ็นต์ด้วย เพื่อให้ฟรอนท์เอนด์ใช้ข้อมูลที่เชื่อมโยงกับผู้ใช้ที่ลงชื่อเข้าใช้ใหม่ได้
โค้ดตัวอย่าง: ยืนยันและลงชื่อเข้าใช้ผู้ใช้
เราใช้ไลบรารี SimpleWebAuthn ในตัวอย่างของเรา ในกรณีนี้ เราจะส่งต่อการยืนยันการตอบกลับการตรวจสอบสิทธิ์ให้กับฟังก์ชัน verifyAuthenticationResponse
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
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.');
}
// Base64URL decode some values
const authenticator = {
credentialPublicKey: isoBase64URL.toBuffer(cred.publicKey),
credentialID: isoBase64URL.toBuffer(cred.id),
transports: cred.transports,
};
// Verify the credential
const { verified, authenticationInfo } = await verifyAuthenticationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
authenticator,
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 หรือเจาะลึกรายการการยืนยันทั้งหมดในข้อกำหนด