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

ภาพรวม

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

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

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

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

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

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

ในเซิร์ฟเวอร์ ให้ทำดังนี้

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