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

ภาพรวม

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

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

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

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

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

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

  • บนเซิร์ฟเวอร์ โดยทั่วไปไลบรารีฝั่งเซิร์ฟเวอร์ 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 ในตัวเลือกการลงทะเบียน เมื่อผู้ใช้พยายามตรวจสอบสิทธิ์ในภายหลัง ตัวตรวจสอบสิทธิ์จะทำให้ passkey_user_id นี้พร้อมใช้งานในการตอบกลับการตรวจสอบสิทธิ์ใน userHandle เราขอแนะนำว่าอย่าตั้งค่า passkey_user_id เป็นคีย์หลัก คีย์หลักมักจะกลายเป็น PII โดยแท้จริงเพราะระบบมีการใช้คีย์นี้อย่างแพร่หลาย
  • ตารางข้อมูลเข้าสู่ระบบคีย์สาธารณะ:
    • id: รหัสการรับรอง โปรดใช้คีย์นี้เป็นคีย์หลักสำหรับตารางข้อมูลเข้าสู่ระบบคีย์สาธารณะ
    • public_key: คีย์สาธารณะของข้อมูลเข้าสู่ระบบ
    • passkey_user_id: ใช้คีย์นี้เป็นคีย์นอกเพื่อสร้างลิงก์ด้วยตารางผู้ใช้
    • backed_up: ระบบจะสำรองข้อมูลพาสคีย์หากผู้ให้บริการพาสคีย์ซิงค์ข้อมูลไว้ การจัดเก็บสถานะการสำรองข้อมูลจะมีประโยชน์ในกรณีที่ต้องการจะลืมรหัสผ่านในอนาคตสําหรับผู้ใช้ที่มีพาสคีย์ backed_up รายการ คุณสามารถตรวจสอบว่ามีการสำรองข้อมูลพาสคีย์หรือไม่โดยตรวจสอบธงใน authenticatorData หรือใช้ฟีเจอร์ไลบรารีฝั่งเซิร์ฟเวอร์ของ FIDO ที่โดยปกติจะพร้อมใช้งานเพื่อช่วยให้คุณเข้าถึงข้อมูลนี้ได้อย่างง่ายดาย การจัดเก็บการมีสิทธิ์สํารองข้อมูลอาจมีประโยชน์ในการตอบคำถามที่มีโอกาสเป็นผู้ใช้
    • name: (ไม่บังคับ) ชื่อที่แสดงสำหรับข้อมูลเข้าสู่ระบบเพื่อให้ผู้ใช้ตั้งชื่อข้อมูลเข้าสู่ระบบที่กำหนดเองได้
    • transports: อาร์เรย์ของการขนส่ง การจัดเก็บการส่งมีประโยชน์สำหรับประสบการณ์ของผู้ใช้ในการตรวจสอบสิทธิ์ เมื่อมีการรับส่งข้อมูลพร้อมใช้งาน เบราว์เซอร์จะทำงานตามนั้นและแสดง UI ที่ตรงกับการรับส่งข้อมูลที่ผู้ให้บริการพาสคีย์ใช้ในการสื่อสารกับไคลเอ็นต์ โดยเฉพาะอย่างยิ่งสำหรับ Use Case การตรวจสอบสิทธิ์อีกครั้งโดยที่ 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 หรือเจาะลึกรายการการยืนยันทั้งหมดในข้อกำหนด

รายการถัดไป

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