ثبت گذرکلید سمت سرور

نمای کلی

در اینجا نمای کلی سطح بالایی از مراحل کلیدی مربوط به ثبت گذرکلید ارائه شده است:

جریان ثبت گذرکلید

  • گزینه‌های ایجاد گذرکلید را تعریف کنید. آن‌ها را به مشتری ارسال کنید تا بتوانید آن‌ها را به فراخوانی ایجاد گذرکلیدتان منتقل کنید: فراخوانی 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 معمولاً نشانی ایمیل است و برای «طرف اعتمادکننده» منحصربه‌فرد است. user.displayName نام کاربرپسند است. توجه داشته باشید که همه پلاتفرم‌ها از displayName استفاده نمی‌کنند.
  • ‫user.id: رشته‌ای تصادفی و یکتا که هنگام ایجاد حساب تولید می‌شود. این شناسه باید دائمی باشد، برخلاف نام کاربری که ممکن است قابل‌ویرایش باشد. شناسه کاربر حسابی را شناسایی می‌کند، اما نباید حاوی هیچ‌گونه اطلاعات شناساننده شخصی (PII) باشد. احتمالاً ازقبل شناسه کاربری در سیستم خود دارید، اما درصورت نیاز، شناسه کاربری را به‌طور خاص برای گذرکلیدها ایجاد کنید تا عاری از هرگونه اطلاعات شناساننده شخصی باشد.
  • excludeCredentials: فهرستی از شناسه‌های اطلاعات کاربری موجود برای جلوگیری از تکرار گذرکلید از ارائه‌دهنده گذرکلید. برای پر کردن این فیلد، اطلاعات کاربری موجود برای این کاربر را در پایگاه داده‌تان جستجو کنید. جزئیات را در جلوگیری از ایجاد گذرکلید جدید درصورت وجود گذرکلید مرور کنید.
  • challenge: برای ثبت اطلاعات کاربری، چالش مربوط نیست، مگراینکه از گواهی استفاده کنید که تکنیک پیشرفته‌تری برای درستی‌سنجی هویت ارائه‌دهنده گذرکلید و داده‌های منتشرشده از آن است. بااین‌حال، حتی اگر از گواهی استفاده نمی‌کنید، چالش همچنان فیلد الزامی است. دستورالعمل‌های ایجاد چالش امن برای اصالت‌سنجی در اصالت‌سنجی گذرکلید سمت سرور دردسترس است.

کدبندی و کدگشایی

PublicKeyCredentialCreationOptions ارسال‌شده توسط سرور
PublicKeyCredentialCreationOptions ارسال‌شده توسط سرور. فیلدهای دودویی مانند challenge و user.id در سمت سرور به base64URL کدگذاری می‌شوند تا گزینه‌ها بتوانند به‌عنوان JSON ازطریق HTTPS ارائه شوند.

شیء استاندارد WebAuthn PublicKeyCredentialCreationOptions شامل فیلدهایی است که ArrayBuffer هستند و نمی‌توانند مستقیماً با JSON.stringify() سریال‌سازی شوند. برای ارسال این گزینه‌ها ازطریق HTTPS، فیلدهای دودویی باید به‌صورت رشته‌های base64URL کدگذاری شوند:

  • در سرور، کتابخانه سمت سرور FIDO شما (مانند SimpleWebAuthn) به‌طور خودکار یک شیء گزینه‌های قابل‌سریال‌سازی JSON (مانند PublicKeyCredentialCreationOptionsJSON) با فیلدهای کدگذاری‌شده Base64URL تولید می‌کند.
  • در سمت کارخواه، مرورگرهای نو از PublicKeyCredential.parseCreationOptionsFromJSON() برای تبدیل مستقیم گزینه‌های JSON به PublicKeyCredentialCreationOptions پشتیبانی می‌کنند. هنگام استفاده از کتابخانه‌های سمت کارخواه مانند @simplewebauthn/browser، توابعی مانند startRegistration({ optionsJSON: options }) این تبدیل را به‌طور خودکار انجام می‌دهند.

کد نمونه: ایجاد گزینه‌های ایجاد اعتبارنامه

در مثال‌هایمان از کتابخانه SimpleWebAuthn استفاده می‌کنیم. در اینجا، ایجاد گزینه‌های اطلاعات کاربری کلید عمومی را به تابع generateRegistrationOptions آن واگذار می‌کنیم.

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
} from '@simplewebauthn/server';
import { isoBase64URL, isoUint8Array } 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: cred.id,
          type: 'public-key',
          transports: cred.transports,
        });
      }
    }

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

    // 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 است که نشان‌دهنده پاسخ ارائه‌دهنده گذرکلید به دستور مشتری برای ایجاد گذرکلید است. این پاسخ حاوی اطلاعاتی درباره اطلاعات اعتباری جدید است که شما به‌عنوان «طرف معتمد» برای اصالت‌سنجی کاربر در آینده به آن نیاز دارید. درباره AuthenticatorAttestationResponse در پیوست: AuthenticatorAttestationResponse بیشتر بدانید.

شیء PublicKeyCredential را به سرور ارسال کنید. وقتی آن را دریافت کردید، درستی‌سنجی‌اش کنید.

این مرحله درستی‌سنجی را به کتابخانه سمت سرور FIDO خود واگذار کنید. معمولاً برای این منظور یک تابع سودمندی ارائه می‌دهد. برای مثال، SimpleWebAuthn verifyRegistrationResponse را ارائه می‌دهد. در پیوست: درستی‌سنجی پاسخ ثبت‌نام با آنچه در پشت صحنه اتفاق می‌افتد آشنا شوید.

پس‌از موفقیت‌آمیز بودن درستی‌سنجی، اطلاعات اعتبارنامه را در پایگاه داده‌تان ذخیره کنید تا کاربر بتواند بعداً با گذرکلید منسوب به آن اعتبارنامه اصالت‌سنجی کند.

از جدول اختصاصی برای اطلاعات کاربری کلید عمومی مرتبط با گذرکلیدها استفاده کنید. کاربر فقط می‌تواند یک گذرواژه داشته باشد، اما می‌تواند چند گذرکلید داشته باشد — برای مثال، یک گذرکلید همگام‌سازی‌شده ازطریق Apple iCloud Keychain و یک گذرکلید ازطریق «مدیر گذرواژه Google».

در اینجا نمونه‌ای از طرح‌واره‌ای که می‌توانید برای ذخیره اطلاعات اعتبارنامه استفاده کنید آورده شده است:

طرح پایگاه داده برای گذرکلیدها

  • جدول کاربران:
    • user_id: شناسه کاربر اصلی. شناسه دائمی، یکتا، و تصادفی برای کاربر. از این به‌عنوان کلید اصلی برای جدول کاربران استفاده کنید.
    • username. نام کاربری تعریف‌شده توسط کاربر، احتمالاً قابل‌ویرایش.
    • passkey_user_id: شناسه کاربر بدون اطلاعات شناساننده شخصی مختص گذرکلید، که با user.id در گزینه‌های ثبت‌نام شما نشان داده می‌شود. وقتی کاربر بعداً تلاش می‌کند اصالت‌سنجی کند، اصالت‌سنج این passkey_user_id را در پاسخ اصالت‌سنجی خود در userHandle دردسترس قرار می‌دهد. توصیه می‌کنیم passkey_user_id را به‌عنوان کلید اصلی تنظیم نکنید. کلیدهای اصلی معمولاً در سیستم‌ها به اطلاعات شناساننده شخصی واقعی تبدیل می‌شوند، زیرا به‌طور گسترده استفاده می‌شوند.
  • جدول اعتبارنامه کلید عمومی:
    • id: شناسه اطلاعات اعتباری. از این به‌عنوان کلید اصلی برای جدول اعتبارنامه کلید عمومی استفاده کنید.
    • ‫public_key: کلید عمومی اطلاعات کاربری.
    • ‫passkey_user_id: از این به‌عنوان کلید خارجی برای ایجاد پیوند با جدول کاربران استفاده کنید.
    • backed_up: اگر گذرکلید توسط ارائه‌دهنده گذرکلید همگام‌سازی شود، پشتیبان‌گیری می‌شود. ذخیره کردن وضعیت پشتیبان‌گیری درصورتی مفید است که بخواهید در آینده گذرواژه‌ها را برای کاربرانی که گذرکلید backed_up دارند حذف کنید. می‌توانید با بررسی پرچم BE در authenticatorData یا بااستفاده از ویژگی کتابخانه سمت سرور FIDO که معمولاً برای دسترسی آسان به این اطلاعات دردسترس است، بررسی کنید که آیا از گذرکلید پشتیبان‌گیری شده است یا نه. ذخیره کردن واجدشرایط بودن پشتیبان‌گیری می‌تواند برای رسیدگی به پرسش‌های احتمالی کاربر مفید باشد.
    • name: به‌صورت اختیاری، نام نمایشی برای اطلاعات کاربری تا کاربران بتوانند نام‌های سفارشی به اطلاعات کاربری بدهند.
    • transports: آرایه‌ای از انتقال‌ها. ذخیره کردن انتقال‌ها برای تجربه کاربری اصالت‌سنجی مفید است. وقتی انتقال‌ها دردسترس باشند، مرورگر می‌تواند مطابق با آن رفتار کند و میانای کاربری‌ای را نمایش دهد که با انتقالی که ارائه‌دهنده گذرکلید برای برقراری ارتباط با کارخواهان استفاده می‌کند مطابقت داشته باشد—به‌ویژه برای موارد استفاده از اصالت‌سنجی مجدد که 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 {
      aaguid,
      credential,
      credentialBackedUp
    } = registrationInfo;

    // Name the credential based on AAGUID
    const name =
      aaguid === undefined ||
      aaguid === '00000000-0000-0000-0000-000000000000' ?
        req.useragent?.platform : aaguids[aaguid]?.name;

    const base64PublicKey = isoBase64URL.fromBuffer(credential.publicKey);

    // Existing, signed-in user
    const { user } = res.locals;

    // Save the credential
    await Credentials.update({
      id: credential.id,
      passkey_user_id: user.passkey_user_id,
      publicKey: base64PublicKey,
      name,
      aaguid,
      transports: credential.transports || response.response.transports,
      backed_up: credentialBackedUp,
      registered_at: new Date().getTime()
    });

    // 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 شامل دو شیء مهم است:

  • response.clientDataJSON نسخه JSON داده‌های کارخواه است که در وب، داده‌ها همان‌طور که توسط مرورگر دیده می‌شوند هستند. این شیء حاوی مبدأ RP، چالش، و androidPackageName (اگر کارخواه برنامه Android باشد) است. به‌عنوان RP، خواندن clientDataJSON به شما امکان می‌دهد به اطلاعاتی که مرورگر در زمان درخواست create دیده است دسترسی پیدا کنید.
  • response.attestationObjectحاوی دو بخش اطلاعات است:
    • attestationStatement که مربوط نیست مگراینکه از گواهی استفاده کنید.
    • authenticatorData داده‌هایی است که ارائه‌دهنده گذرکلید می‌بیند. به‌عنوان «طرف اعتمادکننده»، خواندن authenticatorData به شما امکان می‌دهد به داده‌هایی که ارائه‌دهنده گذرکلید می‌بیند و در زمان درخواست create برگردانده می‌شود دسترسی داشته باشید.

authenticatorDataحاوی اطلاعات ضروری درباره اعتبارنامه کلید عمومی است که با گذرکلید جدیداً ایجادشده مرتبط است:

  • خود اعتبارنامه کلید عمومی و شناسه اعتبارنامه یکتا برای آن.
  • شناسه «طرف محدود» مرتبط با اطلاعات کاربری.
  • پرچم‌هایی که وضعیت کاربر را هنگام ایجاد گذرکلید توصیف می‌کنند: اینکه آیا کاربر واقعاً حضور داشته است یا نه، و اینکه آیا کاربر با موفقیت درستی‌سنجی شده است یا نه (به بررسی عمیق userVerification مراجعه کنید).
  • AAGUID شناسه‌ای برای ارائه‌دهنده گذرکلید، مثل «مدیر گذرواژه Google»، است. براساس AAGUID، می‌توانید ارائه‌دهنده گذرکلید را شناسایی کنید و نام را در صفحه مدیریت گذرکلید نمایش دهید. (به تعیین ارائه‌دهنده گذرکلید با AAGUID مراجعه کنید)

اگرچه authenticatorData در attestationObject تودرتو است، اطلاعاتی که دربردارد برای پیاده‌سازی گذرکلید شما لازم است، چه از گواهی استفاده کنید یا نه. authenticatorData کدگذاری شده است و حاوی فیلدهایی است که در قالب دودویی کدگذاری شده‌اند. کتابخانه سمت سرور شما معمولاً تجزیه و رمزگشایی را انجام می‌دهد. اگر از کتابخانه سمت سرور استفاده نمی‌کنید، از getAuthenticatorData() سمت کارخواه استفاده کنید تا در تجزیه و رمزگشایی سمت سرور صرفه‌جویی کنید.

پیوست: درستی‌سنجی پاسخ ثبت‌نام

در پشت صحنه، درستی‌سنجی پاسخ ثبت شامل بررسی‌های زیر است:

  • مطمئن شوید شناسه طرف متکی با سایت شما مطابقت داشته باشد.
  • مطمئن شوید که مبدأ درخواست، مبدأ موردانتظار برای سایت شما (نشانی وب سایت اصلی، برنامه Android) باشد.
  • اگر به درستی‌سنجی کاربر نیاز دارید، مطمئن شوید که پرچم درستی‌سنجی کاربر authenticatorData.uv روی true تنظیم شده باشد.
  • پرچم حضور کاربر authenticatorData.up معمولاً باید true باشد، اما اگر اعتبارنامه به‌صورت مشروط ایجاد شده باشد، باید false باشد.
  • بررسی کنید که کارخواه توانسته باشد چالشی را که به آن داده‌اید ارائه دهد. اگر از گواهی استفاده نمی‌کنید، این بررسی اهمیتی ندارد. بااین‌حال، پیاده‌سازی این بررسی جزو روال‌های مطلوب است: این کار تضمین می‌کند که اگر در آینده تصمیم به استفاده از گواهی بگیرید، کد شما آماده است.
  • مطمئن شوید که شناسه اطلاعات کاربری هنوز برای هیچ کاربری ثبت نشده باشد.
  • تأیید کنید که الگوریتم مورداستفاده ارائه‌دهنده گذرکلید برای ایجاد اطلاعات کاربری، الگوریتمی است که شما فهرست کرده‌اید (در هر فیلد alg از publicKeyCredentialCreationOptions.pubKeyCredParams، که معمولاً در کتابخانه سمت سرور شما تعریف شده است و برای شما قابل‌مشاهده نیست). این کار تضمین می‌کند که کاربران فقط می‌توانند با الگوریتم‌هایی که شما اجازه داده‌اید ثبت‌نام کنند.

برای کسب اطلاعات بیشتر، کد منبع SimpleWebAuthn برای verifyRegistrationResponse را بررسی کنید یا به فهرست کامل درستی‌سنجی‌ها در مشخصات مراجعه کنید.

بعدی

اصالت‌سنجی گذرکلید سمت سرور