การลงทะเบียนพาสคีย์ฝั่งเซิร์ฟเวอร์

ภาพรวม

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

ขั้นตอนการลงทะเบียนพาสคีย์

  • กำหนดตัวเลือกเพื่อสร้างพาสคีย์ ส่งไปยังไคลเอ็นต์เพื่อให้ส่งผ่านไปยังการเรียกใช้การสร้างพาสคีย์ได้ ซึ่งได้แก่ การเรียก WebAuthn API navigator.credentials.create บนเว็บ และ credentialManager.createCredential ใน Android หลังจากผู้ใช้ยืนยันการสร้างพาสคีย์แล้ว การโทรเพื่อสร้างพาสคีย์จะได้รับการแก้ไขและส่งคืนข้อมูลเข้าสู่ระบบ PublicKeyCredential
  • ยืนยันข้อมูลเข้าสู่ระบบและจัดเก็บไว้ในเซิร์ฟเวอร์

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

สร้างตัวเลือกการสร้างข้อมูลเข้าสู่ระบบ

ขั้นตอนแรกที่ต้องทำในเซิร์ฟเวอร์คือการสร้างออบเจ็กต์ PublicKeyCredentialCreationOptions

ซึ่งทำได้โดยใช้ไลบรารีฝั่งเซิร์ฟเวอร์ FIDO โดยทั่วไปจะมีฟังก์ชันยูทิลิตีที่สร้างตัวเลือกเหล่านี้ให้คุณ SimpleWebAuthn มีข้อเสนอ เช่น generateRegistrationOptions

PublicKeyCredentialCreationOptions ควรรวมทุกอย่างที่จำเป็นสำหรับการสร้างพาสคีย์ ได้แก่ ข้อมูลเกี่ยวกับผู้ใช้, เกี่ยวกับ RP และการกำหนดค่าสำหรับพร็อพเพอร์ตี้ของข้อมูลเข้าสู่ระบบที่คุณกำลังสร้าง เมื่อกำหนดค่าทั้งหมดนี้แล้ว ให้ส่งค่าเหล่านี้ไปยังฟังก์ชันในไลบรารีฝั่งเซิร์ฟเวอร์ FIDO ที่มีหน้าที่สร้างออบเจ็กต์ PublicKeyCredentialCreationOptions ตามความจำเป็น

ช่องของ PublicKeyCredentialCreationOptions บางช่องเป็นค่าคงที่ได้ ส่วนประเภทอื่นๆ ควรกำหนดไว้แบบไดนามิกในเซิร์ฟเวอร์

  • rpId: หากต้องการเติมข้อมูลรหัส RP บนเซิร์ฟเวอร์ ให้ใช้ฟังก์ชันหรือตัวแปรฝั่งเซิร์ฟเวอร์ที่ระบุชื่อโฮสต์ของเว็บแอปพลิเคชัน เช่น example.com
  • user.name และ user.displayName: หากต้องการเติมข้อมูลในช่องเหล่านี้ ให้ใช้ข้อมูลเซสชันของผู้ใช้ที่ลงชื่อเข้าใช้ (หรือข้อมูลบัญชีผู้ใช้ใหม่หากผู้ใช้กำลังสร้างพาสคีย์เมื่อลงชื่อสมัครใช้) โดยทั่วไป user.name จะเป็นอีเมลและไม่เฉพาะเจาะจงสำหรับ RP user.displayName คือชื่อที่ใช้ง่าย โปรดทราบว่าบางแพลตฟอร์มจะใช้ displayName
  • user.id: สตริงแบบสุ่มที่ไม่ซ้ำกันที่สร้างขึ้นเมื่อสร้างบัญชี ซึ่งควรเป็นชื่อผู้ใช้ถาวร ซึ่งต่างจากชื่อผู้ใช้ที่อาจแก้ไขได้ User ID จะระบุบัญชี แต่ไม่ควรมีข้อมูลส่วนบุคคลที่ระบุตัวบุคคลนั้นได้ (PII) คุณอาจมี User-ID อยู่ในระบบอยู่แล้ว แต่หากจำเป็น ให้สร้างรหัสเฉพาะสำหรับพาสคีย์เพื่อให้ไม่มี PII
  • excludeCredentials: รายการรหัสของข้อมูลเข้าสู่ระบบที่มีอยู่เพื่อป้องกันไม่ให้มีพาสคีย์ซ้ำจากผู้ให้บริการพาสคีย์ หากต้องการป้อนข้อมูลในช่องนี้ ให้ค้นหาในฐานข้อมูลที่มีอยู่สำหรับผู้ใช้รายนี้ ตรวจสอบรายละเอียดที่หัวข้อป้องกันการสร้างพาสคีย์ใหม่หากมีอยู่แล้ว
  • challenge: สำหรับการลงทะเบียนข้อมูลเข้าสู่ระบบ ภารกิจนี้จะไม่มีผล เว้นแต่คุณจะใช้เอกสารรับรอง ซึ่งเป็นเทคนิคขั้นสูงกว่าในการยืนยันตัวตนของผู้ให้บริการพาสคีย์และข้อมูลที่ส่ง อย่างไรก็ตาม แม้ว่าคุณจะไม่ได้ใช้เอกสารรับรอง การยืนยันตัวตนก็ยังคงเป็นช่องที่ต้องกรอก ในกรณีนี้ คุณสามารถตั้งค่าการทดสอบนี้เป็น 0 รายการเดียวเพื่อให้เรียบง่าย วิธีการสร้างคำถามที่ปลอดภัยสำหรับการตรวจสอบสิทธิ์อยู่ในการตรวจสอบสิทธิ์ด้วยพาสคีย์ฝั่งเซิร์ฟเวอร์

การเข้ารหัสและถอดรหัส

PublicKeyCredentialCreationOptions ส่งโดยเซิร์ฟเวอร์
PublicKeyCredentialCreationOptions ส่งมาโดยเซิร์ฟเวอร์ challenge, user.id และ excludeCredentials.credentials ต้องเข้ารหัสฝั่งเซิร์ฟเวอร์เป็น base64URL เพื่อให้ PublicKeyCredentialCreationOptions แสดงผ่าน HTTPS ได้

PublicKeyCredentialCreationOptions มีช่องที่เป็น ArrayBuffer ดังนั้น JSON.stringify() จึงไม่รองรับช่องเหล่านั้น ซึ่งหมายความว่าในขณะนี้ ในการส่ง PublicKeyCredentialCreationOptions ผ่าน HTTPS จะต้องเข้ารหัสบางช่องด้วยตนเองบนเซิร์ฟเวอร์โดยใช้ base64URL จากนั้นจึงถอดรหัสบนไคลเอ็นต์

  • บนเซิร์ฟเวอร์ โดยทั่วไปแล้ว ไลบรารีฝั่งเซิร์ฟเวอร์ FIDO จะจัดการการเข้ารหัสและถอดรหัส
  • บนไคลเอ็นต์ คุณต้องเข้ารหัสและถอดรหัสด้วยตนเองในขณะนี้ และจะง่ายขึ้นในอนาคต โดยจะมีเมธอดในการแปลงตัวเลือกเป็น JSON เป็น PublicKeyCredentialCreationOptions ให้ใช้งาน ดูสถานะการใช้งานใน Chrome

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

เราใช้ไลบรารี SimpleWebAuthn ในตัวอย่างของเรา ในส่วนนี้ เราได้ส่งต่อการสร้างตัวเลือกข้อมูลเข้าสู่ระบบคีย์สาธารณะให้กับฟังก์ชัน generateRegistrationOptions

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';

router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
  const { user } = res.locals;
  // 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 {
    // `excludeCredentials` prevents users from re-registering existing
    // credentials for a given passkey provider
    const excludeCredentials = [];
    const credentials = Credentials.findByUserId(user.id);
    if (credentials.length > 0) {
      for (const cred of credentials) {
        excludeCredentials.push({
          id: isoBase64URL.toBuffer(cred.id),
          type: 'public-key',
          transports: cred.transports,
        });
      }
    }

    // Generate registration options for WebAuthn create
    const options = generateRegistrationOptions({
      rpName: process.env.RP_NAME,
      rpID: process.env.HOSTNAME,
      userID: user.id,
      userName: user.username,
      userDisplayName: user.displayName || '',
      attestationType: 'none',
      excludeCredentials,
      authenticatorSelection: {
        authenticatorAttachment: 'platform',
        requireResidentKey: true
      },
    });

    // Keep the challenge in the session
    req.session.challenge = options.challenge;

    return res.json(options);
  } catch (e) {
    console.error(e);
    return res.status(400).send({ error: e.message });
  }
});

เก็บคีย์สาธารณะ

PublicKeyCredentialCreationOptions ส่งโดยเซิร์ฟเวอร์
navigator.credentials.create แสดงผลออบเจ็กต์ PublicKeyCredential

เมื่อแก้ไข navigator.credentials.create ในไคลเอ็นต์ได้สำเร็จ แสดงว่าสร้างพาสคีย์เรียบร้อยแล้ว และส่งกลับออบเจ็กต์ PublicKeyCredential

ออบเจ็กต์ PublicKeyCredential มีออบเจ็กต์ AuthenticatorAttestationResponse ซึ่งแสดงถึงคำตอบของผู้ให้บริการพาสคีย์ต่อวิธีการสร้างพาสคีย์ของลูกค้า โดยจะมีข้อมูลเกี่ยวกับข้อมูลเข้าสู่ระบบใหม่ที่คุณต้องใช้ในฐานะ RP เพื่อตรวจสอบสิทธิ์ผู้ใช้ในภายหลัง ดูข้อมูลเพิ่มเติมเกี่ยวกับ AuthenticatorAttestationResponse ในภาคผนวก: AuthenticatorAttestationResponse

ส่งออบเจ็กต์ PublicKeyCredential ไปยังเซิร์ฟเวอร์ เมื่อได้รับแล้ว โปรดยืนยัน

ส่งขั้นตอนการยืนยันนี้ไปยังไลบรารีฝั่งเซิร์ฟเวอร์ FIDO โดยทั่วไปจะมีฟังก์ชันยูทิลิตีสําหรับวัตถุประสงค์นี้ SimpleWebAuthn มีข้อเสนอ เช่น verifyRegistrationResponse ดูสิ่งที่เกิดขึ้นขั้นสูงได้ในภาคผนวก: การยืนยันการตอบกลับการจดทะเบียน

เมื่อการยืนยันสำเร็จแล้ว ให้จัดเก็บข้อมูลเข้าสู่ระบบไว้ในฐานข้อมูลของคุณ เพื่อให้ผู้ใช้สามารถตรวจสอบสิทธิ์ด้วยพาสคีย์ที่เชื่อมโยงกับข้อมูลเข้าสู่ระบบดังกล่าวในภายหลังได้

ใช้ตารางเฉพาะสำหรับข้อมูลเข้าสู่ระบบคีย์สาธารณะที่เชื่อมโยงกับพาสคีย์ ผู้ใช้จะมีรหัสผ่านได้เพียงรหัสเดียว แต่จะมีพาสคีย์ได้หลายรายการ เช่น พาสคีย์ที่ซิงค์ผ่านพวงกุญแจ iCloud ของ Apple และพาสคีย์ผ่านเครื่องมือจัดการรหัสผ่านบน Google

ตัวอย่างสคีมาที่คุณใช้เพื่อจัดเก็บข้อมูลเข้าสู่ระบบได้มีดังนี้

สคีมาฐานข้อมูลสำหรับพาสคีย์

  • ตารางผู้ใช้:
    • user_id: รหัสผู้ใช้หลัก รหัสถาวรแบบสุ่มที่ไม่ซ้ำกันสำหรับผู้ใช้ ใช้คีย์นี้เป็นคีย์หลักสำหรับตารางผู้ใช้
    • username ชื่อผู้ใช้ที่กำหนดโดยผู้ใช้ ซึ่งอาจแก้ไขได้
    • passkey_user_id: รหัสผู้ใช้ที่ไม่มี PII สำหรับพาสคีย์โดยเฉพาะ ซึ่งจะแสดงโดย user.id ในตัวเลือกการลงทะเบียน เมื่อผู้ใช้พยายามตรวจสอบสิทธิ์ในภายหลัง Authenticator จะทำให้ passkey_user_id นี้พร้อมใช้งานในการตอบกลับการตรวจสอบสิทธิ์ใน userHandle ขอแนะนำว่าอย่าตั้งค่า passkey_user_id เป็นคีย์หลัก คีย์หลักมักจะกลายเป็น PII ในระบบเนื่องจากเป็นการใช้งานอย่างแพร่หลาย
  • ตารางข้อมูลเข้าสู่ระบบคีย์สาธารณะ
    • id: รหัสข้อมูลเข้าสู่ระบบ ใช้คีย์นี้เป็นคีย์หลักสำหรับตารางข้อมูลเข้าสู่ระบบคีย์สาธารณะ
    • public_key: คีย์สาธารณะของข้อมูลเข้าสู่ระบบ
    • passkey_user_id: ใช้คีย์นี้เป็นคีย์นอกเพื่อสร้างลิงก์ที่มีตารางผู้ใช้
    • backed_up: ระบบจะสำรองข้อมูลพาสคีย์ หากผู้ให้บริการพาสคีย์ซิงค์ข้อมูล การจัดเก็บสถานะการสำรองข้อมูลจะเป็นประโยชน์หากคุณต้องการพิจารณาทิ้งรหัสผ่านในอนาคตสำหรับผู้ใช้ที่มีพาสคีย์ backed_up รายการ คุณตรวจสอบได้ว่ามีการสำรองข้อมูลพาสคีย์หรือไม่ด้วยการตรวจสอบ Flag ใน authenticatorData หรือใช้ฟีเจอร์คลังฝั่งเซิร์ฟเวอร์ของ FIDO ที่โดยทั่วไปแล้วให้บริการเพื่อให้คุณเข้าถึงข้อมูลนี้ได้อย่างง่ายดาย การจัดเก็บสิทธิ์การสำรองข้อมูลจะช่วยตอบคําถามของผู้ที่อาจเป็นผู้ใช้ได้
    • name: ชื่อที่แสดงของข้อมูลเข้าสู่ระบบ (ไม่บังคับ) เพื่ออนุญาตให้ผู้ใช้ตั้งชื่อที่กำหนดเองให้กับข้อมูลเข้าสู่ระบบ
    • transports: อาร์เรย์ของการขนส่ง การจัดเก็บการรับส่งข้อมูลมีประโยชน์ต่อประสบการณ์ของผู้ใช้การตรวจสอบสิทธิ์ เมื่อมีบริการขนส่ง เบราว์เซอร์จะทำงานตามนั้นและแสดง UI ที่ตรงกับการนำส่งที่ผู้ให้บริการพาสคีย์ใช้ในการสื่อสารกับลูกค้า โดยเฉพาะกรณีการใช้งานของการตรวจสอบสิทธิ์ซ้ำที่ allowCredentials ไม่ว่างเปล่า

ข้อมูลอื่นๆ อาจเป็นประโยชน์สำหรับการจัดเก็บเพื่อวัตถุประสงค์ด้านประสบการณ์ของผู้ใช้ ซึ่งรวมถึงรายการต่างๆ เช่น ผู้ให้บริการพาสคีย์ เวลาที่สร้างข้อมูลเข้าสู่ระบบ และเวลาที่ใช้ล่าสุด อ่านเพิ่มเติมได้ในการออกแบบอินเทอร์เฟซผู้ใช้ของพาสคีย์

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

เราใช้ไลบรารี SimpleWebAuthn ในตัวอย่างของเรา ในกรณีนี้ เราจะส่งต่อการยืนยันการตอบกลับการลงทะเบียนให้กับฟังก์ชัน verifyRegistrationResponse

import { isoBase64URL } from '@simplewebauthn/server/helpers';


router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;
  const response = req.body;
  // This sample code is for registering a passkey for an existing,
  // signed-in user

  // 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 {
    // Verify the credential
    const { verified, registrationInfo } = await verifyRegistrationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      requireUserVerification: false,
    });

    if (!verified) {
      throw new Error('Verification failed.');
    }

    const { credentialPublicKey, credentialID } = registrationInfo;

    // Existing, signed-in user
    const { user } = res.locals;
    
    // Save the credential
    await Credentials.update({
      id: base64CredentialID,
      publicKey: base64PublicKey,
      // Optional: set the platform as a default name for the credential
      // (example: "Pixel 7")
      name: req.useragent.platform, 
      transports: response.response.transports,
      passkey_user_id: user.passkey_user_id,
      backed_up: registrationInfo.credentialBackedUp
    });

    // Kill the challenge for this session
    delete req.session.challenge;

    return res.json(user);
  } catch (e) {
    delete req.session.challenge;

    console.error(e);
    return res.status(400).send({ error: e.message });
  }
});

ภาคผนวก: AuthenticatorAttestationResponse

AuthenticatorAttestationResponse มีออบเจ็กต์ที่สำคัญ 2 รายการ ได้แก่

  • response.clientDataJSON เป็นข้อมูลไคลเอ็นต์เวอร์ชัน JSON ซึ่งในเว็บจะเป็นข้อมูลที่เบราว์เซอร์มองเห็น ข้อความจะมีต้นทางของ RP, ความท้าทาย และ androidPackageName หากไคลเอ็นต์เป็นแอป Android ในฐานะ RP การอ่านclientDataJSONจะให้คุณเข้าถึงข้อมูลที่เบราว์เซอร์เห็นเมื่อมีการขอ create
  • response.attestationObjectประกอบด้วยข้อมูล 2 ส่วนต่อไปนี้
    • attestationStatement ซึ่งไม่เกี่ยวข้อง เว้นแต่คุณจะใช้เอกสารรับรอง
    • authenticatorData คืออินเทอร์เน็ตตามที่ผู้ให้บริการพาสคีย์เห็น ในฐานะ RP การอ่านauthenticatorDataจะมอบสิทธิ์ให้คุณเข้าถึงข้อมูลที่ผู้ให้บริการพาสคีย์เห็นและส่งคืนให้ ณ เวลาที่มีการขอ create

authenticatorDataมีข้อมูลสำคัญเกี่ยวกับข้อมูลเข้าสู่ระบบคีย์สาธารณะที่เชื่อมโยงกับพาสคีย์ที่สร้างขึ้นใหม่ ดังนี้

  • ทั้งข้อมูลเข้าสู่ระบบของคีย์สาธารณะและรหัสข้อมูลเข้าสู่ระบบที่ไม่ซ้ำกัน
  • รหัส RP ที่เชื่อมโยงกับข้อมูลเข้าสู่ระบบ
  • การแจ้งซึ่งอธิบายสถานะผู้ใช้เมื่อสร้างพาสคีย์: ดูว่าผู้ใช้มีอยู่จริงหรือไม่และผู้ใช้ได้รับการยืนยันหรือไม่ (ดู userVerification)
  • AAGUID ซึ่งระบุผู้ให้บริการพาสคีย์ การแสดงผู้ให้บริการพาสคีย์อาจเป็นประโยชน์สำหรับผู้ใช้ โดยเฉพาะหากผู้ใช้มีการลงทะเบียนพาสคีย์สําหรับบริการของคุณกับผู้ให้บริการพาสคีย์หลายราย

แม้ว่า authenticatorData จะฝังอยู่ใน attestationObject แต่ข้อมูลที่มีอยู่ในพาสคีย์นั้นจำเป็นต่อการใช้งานพาสคีย์ไม่ว่าคุณจะใช้เอกสารรับรองหรือไม่ authenticatorData จะได้รับการเข้ารหัสและมีช่องที่เข้ารหัสในรูปแบบไบนารี โดยทั่วไปไลบรารีฝั่งเซิร์ฟเวอร์จะจัดการการแยกวิเคราะห์และถอดรหัส หากคุณไม่ได้ใช้ไลบรารีฝั่งเซิร์ฟเวอร์ ให้พิจารณาใช้ฝั่งไคลเอ็นต์ getAuthenticatorData() เพื่อช่วยลดการแยกวิเคราะห์และถอดรหัสงานฝั่งเซิร์ฟเวอร์

ภาคผนวก: การยืนยันการตอบกลับการลงทะเบียน

การดำเนินการขั้นสูง การยืนยันการตอบกลับการจดทะเบียนจะประกอบด้วยการตรวจสอบดังต่อไปนี้

  • ตรวจสอบว่ารหัส RP ตรงกับเว็บไซต์ของคุณ
  • ตรวจสอบว่าต้นทางของคำขอเป็นต้นทางที่คาดไว้สำหรับเว็บไซต์ของคุณ (URL ของเว็บไซต์หลัก, แอป Android)
  • หากคุณกำหนดให้มีการยืนยันผู้ใช้ ให้ตรวจดูว่าแฟล็กการยืนยันผู้ใช้ authenticatorData.uv คือ true ตรวจสอบว่าแฟล็กสถานะผู้ใช้ authenticatorData.up คือ true เนื่องจากการแสดงข้อมูลผู้ใช้เป็นสิ่งจำเป็นสำหรับพาสคีย์
  • ตรวจสอบว่าลูกค้าตอบคำถามตามที่ขอได้ หากคุณไม่ได้ใช้เอกสารรับรอง การตรวจสอบนี้ก็จะไม่สำคัญ อย่างไรก็ตาม การใช้การตรวจสอบนี้เป็นแนวทางปฏิบัติแนะนำ เนื่องจากจะทำให้โค้ดพร้อมใช้งานหากคุณตัดสินใจที่จะใช้เอกสารรับรองในอนาคต
  • ตรวจสอบว่ายังไม่ได้ลงทะเบียนรหัสข้อมูลเข้าสู่ระบบสำหรับผู้ใช้
  • ตรวจสอบว่าอัลกอริทึมที่ผู้ให้บริการพาสคีย์ใช้เพื่อสร้างข้อมูลเข้าสู่ระบบเป็นอัลกอริทึมที่คุณระบุไว้ (ในช่อง alg แต่ละช่องของ publicKeyCredentialCreationOptions.pubKeyCredParams ซึ่งโดยปกติจะกำหนดไว้ในไลบรารีฝั่งเซิร์ฟเวอร์และไม่ปรากฏให้เห็นจากคุณ) วิธีนี้ช่วยให้มั่นใจว่าผู้ใช้จะลงทะเบียนได้ด้วยอัลกอริทึมที่คุณเลือกอนุญาตเท่านั้น

หากต้องการดูข้อมูลเพิ่มเติม โปรดตรวจสอบซอร์สโค้ดสำหรับ verifyRegistrationResponse ของ SimpleWebAuthn หรือเจาะลึกรายการการยืนยันทั้งหมดในข้อกำหนด

ถัดไป

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