การตรวจสอบสิทธิ์พาสคีย์ฝั่งเซิร์ฟเวอร์

ภาพรวม

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

ขั้นตอนการตรวจสอบสิทธิ์พาสคีย์

  • กำหนดคำถามและตัวเลือกอื่นๆ ที่จำเป็นในการตรวจสอบสิทธิ์ด้วยพาสคีย์ ส่งไปยังไคลเอ็นต์เพื่อให้คุณส่งผ่านการตรวจสอบสิทธิ์ด้วยพาสคีย์ (navigator.credentials.get บนเว็บ) หลังจากผู้ใช้ยืนยันการตรวจสอบสิทธิ์ของพาสคีย์แล้ว การเรียกการตรวจสอบสิทธิ์ด้วยพาสคีย์จะได้รับการแก้ไขและแสดงผลข้อมูลเข้าสู่ระบบ (PublicKeyCredential) ข้อมูลเข้าสู่ระบบมีการยืนยันการตรวจสอบสิทธิ์
  • ยืนยันการตรวจสอบสิทธิ์
  • ถ้าการยืนยันการตรวจสอบสิทธิ์ถูกต้อง ให้ตรวจสอบสิทธิ์ผู้ใช้

ส่วนต่อไปนี้จะเจาะลึกรายละเอียดของแต่ละขั้นตอน

สร้างภารกิจ

ในทางปฏิบัติ ภารกิจคืออาร์เรย์ของไบต์แบบสุ่ม ซึ่งแสดงเป็นออบเจ็กต์ ArrayBuffer

// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8

เพื่อให้ภารกิจแข่งขันบรรลุวัตถุประสงค์ คุณต้องมีคุณสมบัติดังนี้

  1. ตรวจสอบว่าไม่ได้ใช้คำท้าเดียวกันมากกว่า 1 ครั้ง สร้างภารกิจใหม่ทุกครั้งที่พยายามลงชื่อเข้าใช้ ทิ้งภารกิจหลังจากการพยายามลงชื่อเข้าใช้ทุกครั้ง ไม่ว่าจะลงชื่อเข้าใช้สำเร็จหรือไม่สำเร็จ ทิ้งคำท้าหลังจากระยะเวลาหนึ่งด้วย ไม่รับคำถามเดียวกันในคำตอบมากกว่า 1 ครั้ง
  2. ตรวจสอบว่าภารกิจดังกล่าวมีการเข้ารหัสลับอย่างปลอดภัย ความท้าทายคงเป็นสิ่งที่คาดไม่ถึงจริงๆ หากต้องการสร้างการยืนยันที่ปลอดภัยในการเข้ารหัสฝั่งเซิร์ฟเวอร์ วิธีที่ดีที่สุดคือการใช้ไลบรารีฝั่งเซิร์ฟเวอร์ของ 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: ระบุว่า "ต้องระบุ" การยืนยันผู้ใช้โดยใช้การล็อกหน้าจออุปกรณ์เป็น "แนะนำ" หรือไม่ หรือ "ไม่แนะนำ" ตรวจสอบการดึงข้อมูลชาเลนจ์จากเซิร์ฟเวอร์ RP
  • timeout: ระยะเวลา (เป็นมิลลิวินาที) ที่ผู้ใช้จะตรวจสอบสิทธิ์ได้ ควรมีขอบเขตการใช้งานที่มากพอและสั้นกว่าอายุการใช้งานของ challenge ค่าเริ่มต้นที่แนะนำคือ 5 นาที แต่คุณสามารถเพิ่มค่านี้ได้สูงสุด 10 นาที ซึ่งยังคงอยู่ในช่วงที่แนะนำ ระยะหมดเวลาที่นานจะเหมาะสมหากคุณคาดว่าผู้ใช้จะใช้เวิร์กโฟลว์แบบผสม ซึ่งโดยทั่วไปจะใช้เวลานานกว่าเล็กน้อย หากการดำเนินการหมดเวลา ระบบจะส่งข้อความ NotAllowedError

เมื่อสร้าง publicKeyCredentialRequestOptions แล้ว ส่งให้ลูกค้า

วันที่ PublicKeyCredentialCreationOptions ที่เซิร์ฟเวอร์ส่ง
ตัวเลือกที่เซิร์ฟเวอร์ส่ง การถอดรหัส challenge จะเกิดขึ้นฝั่งไคลเอ็นต์

โค้ดตัวอย่าง: สร้างตัวเลือกคำขอข้อมูลเข้าสู่ระบบ

เราใช้ไลบรารี 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 กลับมา

วันที่ ออบเจ็กต์ PublicKeyCredential ที่ส่งโดยเซิร์ฟเวอร์
navigator.credentials.get แสดงผล PublicKeyCredential

response เป็น AuthenticatorAssertionResponse โดยจะแสดงการตอบสนองของผู้ให้บริการพาสคีย์ต่อวิธีการของไคลเอ็นต์เพื่อสร้างสิ่งที่ต้องใช้ในการพยายามตรวจสอบสิทธิ์ด้วยพาสคีย์ใน RP ซึ่งประกอบด้วย

ส่งออบเจ็กต์ PublicKeyCredential ไปยังเซิร์ฟเวอร์

ดำเนินการต่อไปนี้ในเซิร์ฟเวอร์

วันที่ สคีมาฐานข้อมูล
สคีมาฐานข้อมูลที่แนะนำ ดูข้อมูลเพิ่มเติมเกี่ยวกับการออกแบบนี้ได้ในการลงทะเบียนพาสคีย์ฝั่งเซิร์ฟเวอร์
  • รวบรวมข้อมูลที่จำเป็นต่อการยืนยันการยืนยันและตรวจสอบสิทธิ์ผู้ใช้
    • รับคำถามที่คาดไว้ซึ่งคุณจัดเก็บไว้ในเซสชันเมื่อสร้างตัวเลือกการตรวจสอบสิทธิ์
    • รับต้นทางและรหัส RP ที่คาดไว้
    • ค้นหาว่าผู้ใช้รายนั้นคือใครในฐานข้อมูล ในกรณีที่ข้อมูลเข้าสู่ระบบที่ค้นพบได้ คุณจะไม่ทราบว่าใครคือผู้ใช้ที่ส่งคำขอการตรวจสอบสิทธิ์ คุณมี 2 ตัวเลือกดังนี้
      • ตัวเลือกที่ 1: ใช้ response.userHandle ในออบเจ็กต์ PublicKeyCredential ในตารางผู้ใช้ ให้มองหา passkey_user_id ที่ตรงกับ userHandle
      • ตัวเลือกที่ 2: ใช้ข้อมูลเข้าสู่ระบบ id ที่มีอยู่ในออบเจ็กต์ PublicKeyCredential ในตารางข้อมูลเข้าสู่ระบบคีย์สาธารณะ ให้ค้นหาข้อมูลเข้าสู่ระบบ id ที่ตรงกับข้อมูลเข้าสู่ระบบ id ที่มีอยู่ในออบเจ็กต์ PublicKeyCredential จากนั้นค้นหาผู้ใช้ที่ตรงกันโดยใช้คีย์นอก passkey_user_id ในตารางผู้ใช้ของคุณ
    • ค้นหาข้อมูลเข้าสู่ระบบคีย์สาธารณะที่ตรงกับการยืนยันการตรวจสอบสิทธิ์ที่คุณได้รับในฐานข้อมูล วิธีการคือ ในตารางข้อมูลเข้าสู่ระบบคีย์สาธารณะ ให้มองหาข้อมูลเข้าสู่ระบบ 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 หรือเจาะลึกรายการการยืนยันทั้งหมดในข้อกำหนด