ภาพรวม
ต่อไปนี้เป็นภาพรวมระดับสูงของขั้นตอนสำคัญที่เกี่ยวข้องกับการตรวจสอบสิทธิ์พาสคีย์
- กำหนดคำถามและตัวเลือกอื่นๆ ที่จำเป็นในการตรวจสอบสิทธิ์ด้วยพาสคีย์ ส่งไปยังไคลเอ็นต์เพื่อให้คุณส่งผ่านการตรวจสอบสิทธิ์ด้วยพาสคีย์ (
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
ไปยังเซิร์ฟเวอร์
ดำเนินการต่อไปนี้ในเซิร์ฟเวอร์
- รวบรวมข้อมูลที่จำเป็นต่อการยืนยันการยืนยันและตรวจสอบสิทธิ์ผู้ใช้
- รับคำถามที่คาดไว้ซึ่งคุณจัดเก็บไว้ในเซสชันเมื่อสร้างตัวเลือกการตรวจสอบสิทธิ์
- รับต้นทางและรหัส 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 หรือเจาะลึกรายการการยืนยันทั้งหมดในข้อกำหนด