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

ภาพรวม

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

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

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

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

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