سایت خود را با احراز هویت دو مرحله ای با یک کلید امنیتی (WebAuthn) ایمن کنید

1. آنچه می سازید

شما با یک برنامه وب پایه شروع خواهید کرد که از ورود مبتنی بر رمز عبور پشتیبانی می کند.

سپس از طریق یک کلید امنیتی، بر اساس WebAuthn، پشتیبانی از احراز هویت دو مرحله‌ای را اضافه می‌کنید. برای انجام این کار، موارد زیر را پیاده سازی خواهید کرد:

  • راهی برای کاربر برای ثبت اعتبار WebAuthn.
  • یک جریان احراز هویت دو عاملی که در آن از کاربر در صورت ثبت نام فاکتور دوم - یک اعتبار WebAuthn - خواسته می شود.
  • یک رابط مدیریت اعتبار: لیستی از اعتبارنامه ها که به کاربران امکان می دهد نام کاربری خود را تغییر داده و آنها را حذف کنند.

16ce77744061c5f7.png

به برنامه وب تمام شده نگاهی بیندازید و آن را امتحان کنید.

2. درباره WebAuthn

اصول اولیه WebAuthn

چرا WebAuthn؟

فیشینگ یک مشکل امنیتی بزرگ در وب است: بیشتر موارد نقض حساب از رمزهای عبور ضعیف یا دزدیده شده استفاده می شود که مجدداً در سراسر سایت ها استفاده می شود. پاسخ جمعی صنعت به این مشکل، احراز هویت چند عاملی بوده است، اما پیاده‌سازی‌ها تکه تکه شده‌اند و بسیاری هنوز به اندازه کافی به فیشینگ نمی‌پردازند.

Web Authentication API یا WebAuthn یک پروتکل استاندارد شده مقاوم در برابر فیشینگ است که توسط هر برنامه وب قابل استفاده است.

چگونه کار می کند

منبع: webauthn.guide

WebAuthn به سرورها اجازه می دهد تا کاربران را با استفاده از رمزنگاری کلید عمومی به جای رمز عبور ثبت و احراز هویت کنند. وب‌سایت‌ها می‌توانند یک اعتبار ، متشکل از یک جفت کلید عمومی خصوصی ایجاد کنند.

  • کلید خصوصی به طور ایمن در دستگاه کاربر ذخیره می شود.
  • کلید عمومی و شناسه اعتبار به طور تصادفی تولید شده برای ذخیره سازی به سرور ارسال می شود.

کلید عمومی توسط سرور برای اثبات هویت کاربر استفاده می شود. مخفی نیست، زیرا بدون کلید خصوصی مربوطه بی فایده است.

فواید

WebAuthn دو مزیت اصلی دارد:

  • بدون راز مشترک: سرور هیچ رازی را ذخیره نمی کند. این باعث می شود پایگاه داده ها برای هکرها جذابیت کمتری داشته باشند، زیرا کلیدهای عمومی برای آنها مفید نیستند.
  • اعتبار دامنه: اعتبار ثبت شده برای site.example نمی تواند در evil-site.example استفاده شود. این باعث می شود WebAuthn ضد فیشینگ باشد.

موارد استفاده کنید

یکی از موارد استفاده WebAuthn احراز هویت دو مرحله ای با یک کلید امنیتی است. این ممکن است به ویژه برای برنامه های کاربردی وب سازمانی مرتبط باشد.

پشتیبانی از مرورگر

این توسط W3C و FIDO با مشارکت گوگل، موزیلا، مایکروسافت، یوبیکو و دیگران نوشته شده است.

واژه نامه

  • Authenticator: یک نهاد نرم افزاری یا سخت افزاری که می تواند کاربر را ثبت کند و بعداً مالکیت اعتبار ثبت شده را اعلام کند. دو نوع احراز هویت وجود دارد:
  • احراز هویت رومینگ: یک احراز هویت قابل استفاده با هر دستگاهی که کاربر سعی دارد از آن وارد سیستم شود. مثال: یک کلید امنیتی USB، یک گوشی هوشمند.
  • Authenticator پلتفرم: احراز هویتی که در دستگاه کاربر تعبیه شده است. مثال: تاچ آیدی اپل.
  • اعتبار: جفت کلید خصوصی-عمومی
  • طرف متکی: (سرور) وب سایتی که سعی در احراز هویت کاربر دارد
  • سرور FIDO: سروری که برای احراز هویت استفاده می شود. FIDO خانواده ای از پروتکل ها است که توسط اتحاد FIDO توسعه یافته است. یکی از این پروتکل ها WebAuthn است.

در این کارگاه، از احراز هویت رومینگ استفاده خواهیم کرد.

3. قبل از شروع

آنچه شما نیاز دارید

برای تکمیل این کد لبه، شما نیاز دارید:

  • درک اولیه WebAuthn.
  • دانش اولیه جاوا اسکریپت و HTML.
  • یک مرورگر به روز که از WebAuthn پشتیبانی می کند.
  • یک کلید امنیتی که با U2F سازگار است.

می توانید از یکی از موارد زیر به عنوان کلید امنیتی استفاده کنید:

  • یک گوشی اندروید با Android>=7 (نوقا) که Chrome را اجرا می کند. در این مورد، به یک دستگاه Windows، macOS، یا Chrome OS با بلوتوث فعال نیز نیاز دارید.
  • یک کلید USB، مانند YubiKey .

6539dc7ffec2538c.png

منبع: https://www.yubico.com/products/security-key/

dd56e2cfe0f7ced2.png

چیزی که یاد خواهید گرفت

✅ یاد خواهید گرفت

  • نحوه ثبت و استفاده از کلید امنیتی به عنوان عامل دوم برای احراز هویت WebAuthn.
  • چگونه این فرآیند را کاربر پسند کنیم.

یاد نخواهی گرفت ❌

  • چگونه یک سرور FIDO بسازیم - سروری که برای احراز هویت استفاده می شود. این مشکلی ندارد، زیرا معمولاً، به عنوان یک برنامه‌نویس وب یا توسعه‌دهنده سایت، به پیاده‌سازی‌های موجود سرور FIDO تکیه می‌کنید. اطمینان حاصل کنید که همیشه عملکرد و کیفیت اجرای سرور مورد اعتماد خود را تأیید می کنید. در این لبه کد، سرور FIDO از SimpleWebAuthn استفاده می کند. برای سایر گزینه‌ها، به صفحه رسمی FIDO Alliance مراجعه کنید. برای کتابخانه‌های منبع باز، webauthn.io یا AwesomeWebAuthn را ببینید.

سلب مسئولیت

کاربر برای ورود باید یک رمز عبور وارد کند. اما برای سادگی در این کد، رمز عبور ذخیره و بررسی نمی شود. در یک برنامه واقعی، می توانید بررسی کنید که سمت سرور درست باشد.

بررسی‌های امنیتی اولیه مانند بررسی‌های CSRF ، اعتبار سنجی جلسه و پاک‌سازی ورودی در این آزمایشگاه کد پیاده‌سازی شده‌اند. با این حال، بسیاری از اقدامات امنیتی چنین نیستند - برای مثال، هیچ محدودیتی برای ورود رمز عبور برای جلوگیری از حملات brute-force وجود ندارد. در اینجا مهم نیست زیرا رمزهای عبور ذخیره نمی شوند، اما مطمئن شوید که از این کد همانطور که در تولید است استفاده نکنید.

4. احراز هویت خود را تنظیم کنید

اگر از تلفن اندرویدی به عنوان تأیید کننده استفاده می کنید

  • اطمینان حاصل کنید که Chrome هم در دسکتاپ و هم در تلفن شما به روز است.
  • هم در دسک‌تاپ و هم در تلفن، Chrome را باز کنید و با نمایه‌ای که می‌خواهید برای این کارگاه استفاده کنید، وارد شوید.
  • همگام‌سازی را برای این نمایه، روی دسک‌تاپ و تلفن خود روشن کنید. برای این کار از chrome://settings/syncSetup استفاده کنید.
  • بلوتوث را هم روی دسکتاپ و هم روی گوشی خود روشن کنید.
  • در دسک‌تاپ Chrome که با همان نمایه وارد شده‌اید، webauthn.io را باز کنید.
  • یک نام کاربری ساده وارد کنید. نوع Attestation و نوع Authenticator را به مقادیر None و Unspecified (پیش‌فرض) بگذارید. ثبت نام را کلیک کنید.

6b49ff0298f5a0af.png

  • یک پنجره مرورگر باید باز شود و از شما بخواهد هویت خود را تأیید کنید. گوشی خود را در لیست انتخاب کنید.

ffebe58ac826eaf2.png852de328fcd4eb42.png

  • در تلفن خود، باید اعلانی با عنوان تأیید هویت خود دریافت کنید. روی آن ضربه بزنید.
  • در تلفنتان، کد پین تلفنتان (یا لمس حسگر اثر انگشت) از شما خواسته می‌شود. آن را وارد کنید.
  • در webauthn.io روی دسکتاپ شما، یک نشانگر "موفقیت" باید ظاهر شود.

fc0acf00a4d412fa.png

  • در webauthn.io روی دسکتاپ خود، روی دکمه ورود کلیک کنید.
  • دوباره، یک پنجره مرورگر باید باز شود. گوشی خود را در لیست انتخاب کنید
  • در تلفن خود، روی اعلانی که ظاهر می شود ضربه بزنید و پین خود را وارد کنید (یا حسگر اثر انگشت را لمس کنید).
  • webauthn.io باید به شما بگوید که وارد سیستم شده اید. تلفن شما به عنوان یک کلید امنیتی به درستی کار می کند. شما برای کارگاه آماده اید!

اگر از کلید امنیتی USB به عنوان تأیید کننده استفاده می کنید

  • در رایانه رومیزی Chrome، webauthn.io را باز کنید.
  • یک نام کاربری ساده وارد کنید. نوع Attestation و نوع Authenticator را به مقادیر None و Unspecified (پیش‌فرض) بگذارید. ثبت نام را کلیک کنید.
  • یک پنجره مرورگر باید باز شود و از شما بخواهد هویت خود را تأیید کنید. کلید امنیتی USB را در لیست انتخاب کنید.

ffebe58ac826eaf2.png9fe75f04e43da035.png

  • کلید امنیتی خود را در دسکتاپ خود وارد کرده و آن را لمس کنید.

923d5adb8aa8286c.png

  • در webauthn.io روی دسکتاپ شما، یک نشانگر "موفقیت" باید ظاهر شود.

fc0acf00a4d412fa.png

  • در webauthn.io روی دسکتاپ خود، روی دکمه ورود کلیک کنید.
  • دوباره، یک پنجره مرورگر باید باز شود. کلید امنیتی USB را در لیست انتخاب کنید.
  • کلید را لمس کنید.
  • Webauthn.io باید به شما بگوید که وارد سیستم شده اید. کلید امنیتی USB شما به درستی کار می کند. شما برای کارگاه آماده اید!

7e1c0bb19c9f3043.png

5. راه اندازی شوید

در این لبه کد، شما از Glitch استفاده خواهید کرد، یک ویرایشگر کد آنلاین که به صورت خودکار و فوری کد شما را اجرا می کند.

کد استارت را فورک کنید

پروژه شروع را باز کنید.

روی دکمه Remix کلیک کنید.

این یک کپی از کد شروع ایجاد می کند. اکنون کد خود را برای ویرایش دارید. فورک شما (که در Glitch "ریمیکس" نامیده می شود) جایی است که همه کارهای این کد لبه را انجام خواهید داد.

cf2b9f552c9809b6.png

کد شروع را کاوش کنید

کد شروعی را که اخیراً فوک کرده‌اید را برای کمی کاوش کنید.

توجه داشته باشید که در زیر libs ، کتابخانه ای به نام auth.js از قبل ارائه شده است. این یک کتابخانه سفارشی است که از منطق احراز هویت سمت سرور مراقبت می کند. از کتابخانه fido به عنوان یک وابستگی استفاده می کند.

6. اجرای ثبت اعتبار

اجرای ثبت اعتبار

اولین چیزی که برای تنظیم احراز هویت دو مرحله ای با یک کلید امنیتی به آن نیاز داریم این است که کاربر را قادر به ایجاد یک اعتبارنامه کنیم.

اجازه دهید ابتدا تابعی را اضافه کنیم که این کار را در کد سمت کلاینت خود انجام دهد.

در public/auth.client.js ، توجه داشته باشید که تابعی به نام registerCredential() وجود دارد که هنوز کاری را انجام نمی دهد. کد زیر را به آن اضافه کنید:

async function registerCredential() {
  // Fetch the credential creation options from the backend
  const credentialCreationOptionsFromServer = await _fetch(
    "/auth/credential-options",
    "POST"
  );
  // Decode the credential creation options
  const credentialCreationOptions = decodeServerOptions(
    credentialCreationOptionsFromServer
  );
  // Create a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
  const credential = await navigator.credentials.create({
    publicKey: {
      ...credentialCreationOptions,
    }
  });
  // Encode the newly created credential to send it to the backend
  const encodedCredential = encodeCredential(credential);
  // Send the encoded credential to the backend for storage
  return await _fetch("/auth/credential", "POST", encodedCredential);
}

توجه داشته باشید که این تابع قبلاً برای شما صادر شده است.

در اینجا چیزی است که registerCredential انجام می دهد:

  • این گزینه‌های ایجاد اعتبار را از سرور واکشی می‌کند ( /auth/credential-options )
  • از آنجا که گزینه های سرور به صورت رمزگذاری شده برمی گردند، از تابع ابزار decodeServerOptions برای رمزگشایی آنها استفاده می کند.
  • با فراخوانی web API navigator.credential.create یک اعتبار ایجاد می کند. هنگامی که navigator.credential.create فراخوانی می شود، مرورگر کنترل می شود و از کاربر می خواهد یک کلید امنیتی را انتخاب کند.
  • این اعتبار جدید ایجاد شده را رمزگشایی می کند
  • با ارسال یک درخواست به /auth/credential که حاوی اعتبار رمزگذاری شده است، اعتبار جدید سمت سرور را ثبت می کند.

به کنار: به کد سرور نگاهی بیندازید

registerCredential() دو تماس با سرور برقرار می کند، پس بیایید لحظه ای به آنچه در backend اتفاق می افتد نگاه کنیم.

گزینه های ایجاد اعتبار

هنگامی که کلاینت درخواستی به ( /auth/credential-options ) می دهد، سرور یک شی گزینه تولید می کند و آن را به مشتری باز می فرستد.

سپس این شیء توسط مشتری در فراخوانی ایجاد اعتبار واقعی استفاده می شود:

navigator.credentials.create({
    publicKey: {
    // Options generated server-side
    ...credentialCreationOptions
// ...
}

بنابراین، چه چیزی در این credentialCreationOptions وجود دارد که در نهایت در registerCredential سمت کلاینت که در مرحله قبل پیاده‌سازی کرده‌اید استفاده می‌شود؟

به کد سرور در مسیر router.post("/credential-options"، ... نگاهی بیندازید.

بیایید به تک تک ویژگی‌ها نگاه نکنیم، اما در اینجا چند مورد جالب وجود دارد که می‌توانید در شیء گزینه‌های کد سرور مشاهده کنید، که با استفاده از کتابخانه fido2 تولید شده و در نهایت به مشتری بازگردانده می‌شود:

  • rpName و rpId سازمانی را توصیف می کنند که کاربر را ثبت و احراز هویت می کند. به یاد داشته باشید که در WebAuthn، اعتبارنامه ها در دامنه خاصی قرار می گیرند که یک مزیت امنیتی است. rpName و rpId در اینجا برای دامنه اعتبار استفاده می شود. یک rpId معتبر برای مثال نام میزبان سایت شما است. توجه داشته باشید که چگونه این موارد به طور خودکار به‌روزرسانی می‌شوند که پروژه آغازگر را انجام می‌دهید 🧘🏻‍♀️
  • excludeCredentials فهرستی از اعتبارنامه ها است. اعتبار جدید را نمی توان روی یک احراز هویت ایجاد کرد که حاوی یکی از اعتبارنامه های فهرست شده در excludeCredentials باشد. در آزمایشگاه کد ما، excludeCredentials فهرستی از اعتبارنامه های موجود برای این کاربر است. با استفاده از این و user.id ، ما اطمینان می‌دهیم که هر اعتبارنامه‌ای که کاربر ایجاد می‌کند روی یک احراز هویت (کلید امنیتی) متفاوت است. این یک روش خوب است زیرا به این معنی است که اگر یک کاربر چندین اعتبارنامه را ثبت کرده باشد، آنها روی احراز هویت (کلیدهای امنیتی) متفاوتی خواهند بود، بنابراین از دست دادن یک کلید امنیتی، کاربر را از حساب خود قفل نمی کند.
  • authenticatorSelection نوع احراز هویتی را که می خواهید در برنامه وب خود مجاز کنید، تعریف می کند. بیایید نگاهی دقیق‌تر به authenticatorSelection بیندازیم:
    • residentKey: preferred به این معنی است که این برنامه اعتبارنامه های قابل کشف سمت سرویس گیرنده را اعمال نمی کند. اعتبار قابل کشف سمت کلاینت نوع خاصی از اعتبار است که احراز هویت یک کاربر را بدون نیاز به شناسایی ابتدا ممکن می سازد. در اینجا، ما preferred داده‌ایم، زیرا این کد لبه روی پیاده‌سازی اولیه تمرکز دارد. اعتبارنامه های قابل کشف برای جریان های پیشرفته تر است.
    • requireResidentKey فقط برای سازگاری با Backwards با WebAuthn v1 وجود دارد.
    • userVerification: preferred به این معنی است که اگر احراز هویت از تأیید کاربر پشتیبانی کند - برای مثال، اگر یک کلید امنیتی بیومتریک یا کلیدی با ویژگی پین داخلی باشد - طرف متکی آن را هنگام ایجاد اعتبار درخواست می کند. اگر احراز هویت -کلید امنیتی پایه- این کار را نکند، سرور تأیید کاربر را درخواست نخواهد کرد.
  • ​​pubKeyCredParam به ترتیب اولویت، ویژگی‌های رمزنگاری مورد نظر اعتبارنامه را شرح می‌دهد.

همه این گزینه ها تصمیماتی هستند که برنامه وب باید برای مدل امنیتی خود اتخاذ کند. توجه داشته باشید که در سرور، این گزینه ها در یک شی authSettings تعریف شده اند.

چالش

یک بیت جالب دیگر در اینجا req.session.challenge = options.challenge; .

از آنجایی که WebAuthn یک پروتکل رمزنگاری است، برای جلوگیری از حملات مجدد به چالش‌های تصادفی بستگی دارد - زمانی که مهاجم یک بار را برای پخش مجدد احراز هویت می‌دزدد، در حالی که مالک کلید خصوصی نیست که احراز هویت را فعال می‌کند.

برای کاهش این مشکل، یک چالش بر روی سرور ایجاد می‌شود و در لحظه امضا می‌شود. سپس امضا با آنچه انتظار می رود مقایسه می شود. این تأیید می کند که کاربر کلید خصوصی را در زمان تولید اعتبار حفظ می کند.

کد ثبت اعتبار

به کد سرور زیر router.post("/credential", ... نگاهی بیندازید.

اینجا جایی است که اعتبار در سمت سرور ثبت می شود.

خب، آنجا چه خبر است؟

یکی از نکات قابل توجه در این کد، فراخوانی تأیید، از طریق fido2.verifyAttestationResponse :

  • چالش امضا شده بررسی می‌شود، و این اطمینان می‌دهد که اعتبار توسط شخصی ایجاد شده است که در واقع کلید خصوصی را در زمان ایجاد نگه داشته است.
  • شناسه طرف اتکا که به مبدأ آن قید شده است نیز تأیید می شود. این تضمین می کند که اعتبار به این برنامه وب (و فقط این برنامه وب) متصل است.

این قابلیت را به UI اضافه کنید

اکنون که تابع شما برای ایجاد یک اعتبار، «registerCredential() آماده است , اجازه دهید آن را در دسترس کاربر قرار دهیم.

می‌خواهید این کار را از صفحه حساب انجام دهید، زیرا این یک مکان معمولی برای مدیریت احراز هویت است.

در نشانه‌گذاری account.html ، زیر نام کاربری، یک div خالی تا کنون با layout class="flex-h-between" وجود دارد. ما از این div برای عناصر UI که به عملکرد 2FA مربوط می شوند استفاده خواهیم کرد.

این div را اضافه کنید:

  • عنوانی که می گوید "احراز هویت دو مرحله ای"
  • دکمه ای برای ایجاد اعتبار
 <div class="flex-h-between">
    <h3>
        Two-factor authentication
    </h3>
    <button class="create" id="registerButton" raised>
        ➕ Add a credential
    </button>
</div>

در زیر این div، یک div اعتباری اضافه کنید که بعداً به آن نیاز خواهیم داشت:

<div class="flex-h-between">
(HTML you've just added)
</div>
<div id="credentials"></div>

در اسکریپت inline account.html ، تابعی را که ایجاد کرده‌اید وارد کنید و یک تابع register که آن را فراخوانی می‌کند، و همچنین یک کنترل‌کننده رویداد متصل به دکمه‌ای که ایجاد کرده‌اید، اضافه کنید.

// Set up the handler for the button that registers credentials
const registerButton = document.querySelector('#registerButton');
registerButton.addEventListener('click', register);

// Register a credential
async function register() {
  let user = {};
  try {
    const user = await registerCredential();
  } catch (e) {
    // Alert the user that something went wrong
    if (Array.isArray(e)) {
      alert(
        // `msg` not `message`, this is the key's name as per the express validator API
        `Registration failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
      );
    } else {
      alert(`Registration failed. ${e}`);
    }
  }
}

مشخصات را برای دیدن کاربر نمایش دهید

اکنون که عملکرد ایجاد یک اعتبارنامه را اضافه کرده اید، کاربران به راهی برای دیدن اعتبارنامه هایی که اضافه کرده اند نیاز دارند.

صفحه اکانت مکان خوبی برای این کار است.

در account.html به دنبال تابعی به نام updateCredentialList() .

کد زیر را به آن اضافه کنید که یک تماس پشتیبان برای واکشی تمام اعتبارنامه‌های ثبت‌شده برای کاربر وارد شده فعلی ایجاد می‌کند و اعتبارنامه بازگشتی را نمایش می‌دهد:

// Update the list that displays credentials
async function updateCredentialList() {
  // Fetch the latest credential list from the backend
  const response = await _fetch('/auth/credentials', 'GET');
  const credentials = response.credentials || [];
  // Generate the credential list as HTML and pass remove/rename functions as args
  const credentialListHtml = getCredentialListHtml(
    credentials,
    removeEl,
    renameEl
  );
  // Display the list of credentials in the DOM
  const list = document.querySelector('#credentials');
  render(credentialListHtml, list);
}    

در حال حاضر، مهم نیست removeEl و renameEl را تغییر دهید. بعداً در این کد لبه با آنها آشنا خواهید شد.

یک تماس به updateCredentialList در ابتدای اسکریپت درون خطی خود، در account.html اضافه کنید. با این تماس، زمانی که کاربر در صفحه حساب خود فرود می‌آید، اعتبارنامه‌های موجود دریافت می‌شود.

<script type="module">
    // ... (imports)
    // Initialize the credential list by updating it once on page load
    updateCredentialList();

اکنون، زمانی که registerCredential با موفقیت تکمیل شد، updateCredentialList ، به طوری که لیست ها اعتبار جدید ایجاد شده را نشان می دهد:

async function register() {
  let user = {};
  try {
    // ...
  } catch (e) {
    // ...
  }
  // Refresh the credential list to display the new credential
  await updateCredentialList();
}

آن را امتحان کنید! 👩🏻‍💻

شما با ثبت اعتبار تمام شده اید! کاربران اکنون می توانند اعتبارنامه های مبتنی بر کلید امنیتی ایجاد کنند و آنها را در صفحه حساب خود تجسم کنند.

آن را امتحان کنید:

  • خروج از سیستم.
  • با هر کاربری و رمز عبور وارد شوید. همانطور که قبلا ذکر شد، رمز عبور در واقع از نظر صحت بررسی نمی شود تا همه چیز در این کد لبه ساده بماند. رمز عبور غیر خالی را وارد کنید.
  • وقتی در صفحه حساب شدید، روی افزودن اعتبار کلیک کنید.
  • باید از شما خواسته شود یک کلید امنیتی را وارد کرده و لمس کنید. انجام دهید.
  • پس از ایجاد موفقیت آمیز اعتبار، اعتبار باید در صفحه حساب نمایش داده شود.
  • صفحه حساب را دوباره بارگیری کنید. اعتبارنامه باید نمایش داده شود.
  • اگر دو کلید در دسترس دارید، سعی کنید دو کلید امنیتی مختلف را به عنوان اعتبارنامه اضافه کنید. هر دو باید نمایش داده شوند.
  • سعی کنید دو اعتبار را با یک احراز هویت (کلید) ایجاد کنید. متوجه خواهید شد که پشتیبانی نمی شود. این عمدی است - این به دلیل استفاده ما از excludeCredentials در backend است.

7. احراز هویت فاکتور دوم را فعال کنید

کاربران شما می‌توانند اعتبارنامه‌ها را ثبت و لغو ثبت کنند، اما اعتبارنامه‌ها فقط نمایش داده می‌شوند و هنوز واقعاً استفاده نشده‌اند.

اکنون زمان استفاده از آنها و تنظیم احراز هویت دو مرحله ای واقعی است.

در این بخش، جریان احراز هویت در برنامه وب خود را از این جریان اصلی تغییر می‌دهید:

6ff49a7e520836d0.png

به این جریان دو عاملی:

e7409946cd88efc7.png

اجرای احراز هویت عامل دوم

اجازه دهید ابتدا عملکرد مورد نیاز خود را اضافه کرده و ارتباط را با backend پیاده سازی کنیم. در مرحله بعد این را در قسمت جلو اضافه می کنیم.

آنچه شما باید در اینجا پیاده سازی کنید تابعی است که کاربر را با یک اعتبار احراز هویت می کند.

در public/auth.client.js به دنبال تابع خالی authenticateTwoFactor بگردید و کد زیر را به آن اضافه کنید:

async function authenticateTwoFactor() {
  // Fetch the 2F options from the backend
  const optionsFromServer = await _fetch("/auth/two-factor-options", "POST");
  // Decode them
  const decodedOptions = decodeServerOptions(optionsFromServer);
  // Get a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
  const credential = await navigator.credentials.get({
    publicKey: decodedOptions
  });
  // Encode the credential
  const encodedCredential = encodeCredential(credential);
  // Send it to the backend for verification
  return await _fetch("/auth/authenticate-two-factor", "POST", {
    credential: encodedCredential
  });
}

توجه داشته باشید که این تابع قبلاً برای شما صادر شده است. در مرحله بعدی به آن نیاز خواهیم داشت.

کاری که authenticateTwoFactor انجام می دهد در اینجا آمده است:

  • از سرور گزینه های احراز هویت دو عاملی را درخواست می کند. درست مانند گزینه‌های ایجاد اعتبار که قبلاً دیده‌اید، این گزینه‌ها بر روی سرور تعریف شده‌اند و به مدل امنیتی برنامه وب بستگی دارند. کد سرور را در router.post("/two-factors-options", ... برای جزئیات بیشتر وارد کنید.
  • با فراخوانی navigator.credentials.get ، به مرورگر اجازه می‌دهد کنترل را در دست بگیرد و از کاربر بخواهد کلید ثبت‌شده قبلی را وارد کرده و لمس کند. این منجر به انتخاب یک اعتبار برای این عملیات احراز هویت عامل دوم می شود.
  • سپس اعتبار انتخاب شده در یک درخواست Backend ارسال می شود تا واکشی ("/auth/authenticate-two-factor"`. اگر اعتبار برای آن کاربر معتبر باشد، کاربر احراز هویت می شود.

به کنار: به کد سرور نگاهی بیندازید

توجه داشته باشید که server.js قبلاً برخی از ناوبری و دسترسی را انجام می دهد: تضمین می کند که صفحه حساب فقط توسط کاربران تأیید شده قابل دسترسی است و برخی از تغییر مسیرهای ضروری را انجام می دهد.

اکنون به کد سرور در router.post("/initialize-authentication", ... نگاهی بیندازید.

دو نکته جالب در آنجا قابل ذکر است:

  • در این مرحله رمز عبور و اعتبار هر دو به طور همزمان بررسی می شوند. این یک اقدام امنیتی است: برای کاربرانی که احراز هویت دو مرحله‌ای را تنظیم کرده‌اند، نمی‌خواهیم جریان‌های رابط کاربری بسته به درستی یا نبودن رمز عبور متفاوت به نظر برسند. بنابراین در این مرحله رمز عبور و اعتبار را به طور همزمان بررسی می کنیم.
  • اگر رمز عبور و اعتبار هر دو معتبر باشند، سپس با فراخوانی completeAuthentication(req, res); معنی این در عمل این است که ما جلسات را از یک جلسه auth موقت که در آن کاربر هنوز احراز هویت نشده است، به جلسه main که در آن کاربر احراز هویت شده است تغییر می دهیم.

صفحه احراز هویت عامل دوم را در جریان کاربر قرار دهید

در پوشه views به صفحه جدید second-factor.html کنید.

دکمه ای دارد که می گوید از کلید امنیتی استفاده کنید ، اما در حال حاضر هیچ کاری انجام نمی دهد.

این دکمه را روی کلیک authenticateTwoFactor() فراخوانی کنید.

  • اگر authenticateTwoFactor() موفقیت آمیز بود، کاربر را به صفحه حساب خود هدایت کنید.
  • اگر موفقیت آمیز نبود، به کاربر هشدار دهید که خطا رخ داده است. در یک برنامه واقعی، شما پیام‌های خطای مفیدتری را پیاده‌سازی می‌کنید—برای سادگی در این نسخه نمایشی، ما فقط از هشدار پنجره استفاده می‌کنیم.
    <main>
...
    </main>
    <script type="module">
      import { authenticateTwoFactor, authStatuses } from "/auth.client.js";

      const button = document.querySelector("#authenticateButton");
      button.addEventListener("click", async e => {
        try {
          // Ask the user to authenticate with the second factor; this will trigger a browser prompt
          const response = await authenticateTwoFactor();
          const { authStatus } = response;
          if (authStatus === authStatuses.COMPLETE) {
            // The user is properly authenticated => Navigate to the Account page
            location.href = "/account";
          } else {
            throw new Error("Two-factor authentication failed");
          }
        } catch (e) {
          // Alert the user that something went wrong
          alert(`Two-factor authentication failed. ${e}`);
        }
      });
    </script>
  </body>
</html>

از احراز هویت فاکتور دوم استفاده کنید

اکنون برای افزودن مرحله احراز هویت عامل دوم آماده اید.

کاری که اکنون باید انجام دهید اضافه کردن این مرحله از index.html برای کاربرانی است که احراز هویت دو مرحله ای را پیکربندی کرده اند.

322a5c49d865a0d8.png

در index.html ، در زیر location.href = "/account"; ، کدی را اضافه کنید که اگر کاربر 2FA را تنظیم کرده باشد، به صورت مشروط به صفحه تأیید هویت فاکتور دوم هدایت می کند.

در این کد لبه، ایجاد یک اعتبار به طور خودکار کاربر را به احراز هویت دو مرحله‌ای انتخاب می‌کند.

توجه داشته باشید که server.js بررسی جلسه سمت سرور را نیز پیاده‌سازی می‌کند که تضمین می‌کند فقط کاربران احراز هویت شده می‌توانند به account.html دسترسی داشته باشند.

const { authStatus } = response;
if (authStatus === authStatuses.COMPLETE) {
  // The user is properly authenticated => navigate to account
  location.href = '/account';
} else if (authStatus === authStatuses.NEED_SECOND_FACTOR) {
  // Navigate to the two-factor-auth page because two-factor-auth is set up for this user
  location.href = '/second-factor';
}

آن را امتحان کنید! 👩🏻‍💻

  • با یک کاربر جدید johndoe وارد شوید.
  • خروج.
  • با نام johndoe وارد حساب کاربری خود شوید. ببینید که فقط رمز عبور لازم است.
  • اعتبارنامه ایجاد کنید. این در واقع به این معنی است که شما احراز هویت دو مرحله ای را به عنوان johndoe فعال کرده اید.
  • خروج.
  • نام کاربری johndoe و رمز عبور خود را وارد کنید.
  • ببینید چگونه به صورت خودکار به صفحه تأیید هویت فاکتور دوم می روید.
  • (سعی کنید به صفحه حساب در /account دسترسی پیدا کنید؛ توجه داشته باشید که چگونه به صفحه فهرست هدایت می شوید زیرا به طور کامل احراز هویت نشده اید: فاکتور دوم را از دست می دهید)
  • به صفحه احراز هویت فاکتور دوم برگردید و روی استفاده از کلید امنیتی برای احراز هویت فاکتور دوم کلیک کنید.
  • اکنون وارد سیستم شده اید و باید صفحه حساب خود را ببینید!

8. استفاده از اعتبارنامه ها را آسان تر کنید

کارکرد اصلی احراز هویت دو مرحله ای با یک کلید امنیتی تمام شده است 🚀

اما... دقت کردی؟

در حال حاضر، لیست اعتبار ما خیلی راحت نیست: شناسه اعتبار و کلید عمومی رشته های طولانی هستند که هنگام مدیریت اعتبارنامه ها مفید نیستند! انسان ها با رشته ها و اعداد بلند خیلی خوب نیستند 🤖

پس بیایید این را بهبود بخشیم و قابلیتی را به نام گذاری و تغییر نام اعتبارنامه ها با رشته های قابل خواندن توسط انسان اضافه کنیم.

نگاهی به renameCredential بیندازید

برای صرفه جویی در وقت شما در اجرای این تابع که کار خیلی پیشگامانه ای را انجام نمی دهد، تابعی برای تغییر نام اعتبار برای شما در کد شروع در auth.client.js شده است:

async function renameCredential(credId, newName) {
  const params = new URLSearchParams({
    credId,
    name: newName
  });
  return _fetch(
    `/auth/credential?${params}`,
    "PUT"
  );
}

این یک تماس معمولی به‌روزرسانی پایگاه داده است: مشتری یک درخواست PUT را با یک شناسه اعتبار و نام جدید برای آن اعتبار به باطن ارسال می‌کند.

نام های اعتباری سفارشی را پیاده سازی کنید

در account.html به rename تابع خالی توجه کنید.

کد زیر را به آن اضافه کنید:

// Rename a credential
async function rename(credentialId) {
  // Let the user input a new name
  const newName = window.prompt(`Name this credential:`);
  // Rename only if the user didn't cancel AND didn't enter an empty name
  if (newName && newName.trim()) {
    try {
      // Make the backend call to rename the credential (the name is sanitized) server-side
      await renameCredential(credentialId, newName);
    } catch (e) {
      // Alert the user that something went wrong
      if (Array.isArray(e)) {
        alert(
          // `msg` not `message`, this is the key's name as per the express validator API
          `Renaming failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
        );
      } else {
        alert(`Renaming failed. ${e}`);
      }
    }
    // Refresh the credential list to display the new name
    await updateCredentialList();
  }
}

تنها زمانی که اعتبارنامه با موفقیت ایجاد شد، نام بردن اعتبار ممکن است منطقی تر باشد. بنابراین بیایید یک اعتبارنامه بدون نام ایجاد کنیم و پس از ایجاد موفقیت آمیز، نام اعتبار را تغییر دهیم. هر چند این منجر به دو تماس باطن خواهد شد.

از تابع rename در register() استفاده کنید تا کاربران را قادر به نامگذاری اعتبارها در هنگام ثبت نام کنند:

async function register() {
  let user = {};
  try {
    const user = await registerCredential();
    // Get the latest credential's ID (newly created credential)
    const allUserCredentials = user.credentials;
    const newCredential = allUserCredentials[allUserCredentials.length - 1];
    // Rename it
    await rename(newCredential.credId);
  } catch (e) {
    // ...
  }
  // Refresh the credential list to display the new credential
  await updateCredentialList();
}

توجه داشته باشید که ورودی کاربر در backend تایید و پاکسازی می شود:

  check("name")
    .trim()
    .escape()

نمایش نام های اعتباری

به getCredentialHtml در templates.js بروید.

توجه داشته باشید که از قبل کدی برای نمایش نام اعتبار در بالای کارت اعتبار وجود دارد:

// Register credential
const getCredentialHtml = (credential, removeEl, renameEl) => {
 const { name, credId, publicKey } = credential;
 return html`
    <div class="credential-card">
      <div class="credential-name">
        ${name
          ? html`
              ${name}
            `
          : html`
              <span class="unnamed">(Unnamed)</span>
            `}
      </div>
     // ...
    </div>
  `;
};

آن را امتحان کنید! 👩🏻‍💻

  • اعتبارنامه ایجاد کنید.
  • از شما خواسته می شود آن را نام ببرید.
  • یک نام جدید وارد کنید و روی OK کلیک کنید.
  • اعتبار اکنون تغییر نام داده است.
  • وقتی فیلد نام را خالی می گذارید، این کار را تکرار کنید و بررسی کنید که کارها هموار باشد.

فعال کردن تغییر نام اعتبار

کاربران ممکن است نیاز به تغییر نام اعتبارنامه ها داشته باشند – برای مثال، آنها یک کلید دوم را اضافه می کنند و می خواهند نام کلید اول خود را تغییر دهند تا آنها را بهتر تشخیص دهند.

در account.html به دنبال تابع خالی renameEl و کد زیر را به آن اضافه کنید:

// Rename a credential via HTML element
async function renameEl(el) {
  // Define the ID of the credential to update
  const credentialId = el.srcElement.dataset.credentialId;
  // Rename the credential
  await rename(credentialId);
  // Refresh the credential list to display the new name
  await updateCredentialList();
}

اکنون، در getCredentialHtml templates.js در class="flex-end" div، کد زیر را اضافه کنید، این کد یک دکمه تغییر نام را به الگوی کارت اعتباری اضافه می کند. وقتی روی آن کلیک کنید، آن دکمه تابع renameEl را که به تازگی ایجاد کرده ایم فراخوانی می کند:

const getCredentialHtml = (credential, removeEl, renameEl) => {
// ...
 <div class="flex-end">
  <button
    data-credential-id="${credId}"
    @click="${renameEl}"
    class="secondary right"
  >
   Rename
  </button>
 </div>
 // ...
  `;
};

آن را امتحان کنید! 👩🏻‍💻

  • روی تغییر نام کلیک کنید.
  • وقتی از شما خواسته شد نام جدیدی وارد کنید.
  • روی OK کلیک کنید.
  • اعتبارنامه باید با موفقیت تغییر نام داده شود و لیست باید به طور خودکار به روز شود.
  • بارگیری مجدد صفحه همچنان باید نام جدید را نشان دهد (این نشان می دهد که نام جدید در سمت سرور باقی مانده است).

نمایش تاریخ ایجاد اعتبار

تاریخ ایجاد در اعتبارنامه های ایجاد شده از طریق navigator.credential.create() وجود ندارد.

اما از آنجایی که این اطلاعات می‌تواند برای کاربر برای تمایز بین اعتبارنامه‌ها مفید باشد، کتابخانه سمت سرور را در کد شروع برای شما بهینه‌سازی کرده‌ایم و پس از ذخیره اعتبار جدید، یک فیلد creationDate برابر با Date.now() اضافه کرده‌ایم.

در templates.js در class="creation-date" div ، موارد زیر را برای نمایش اطلاعات تاریخ ایجاد به کاربر اضافه کنید:

<div class="creation-date">
  <label>Created:</label>
  <div class="info">
    ${new Date(creationDate).toLocaleDateString()}
    ${new Date(creationDate).toLocaleTimeString()}
  </div>
</div>

9. کد خود را آینده پسند کنید

تا کنون فقط از کاربر خواسته ایم تا یک احراز هویت رومینگ ساده را ثبت کند که سپس به عنوان عامل دوم در هنگام ورود به سیستم استفاده می شود.

یکی از رویکردهای پیشرفته‌تر، تکیه بر نوع قوی‌تری از احراز هویت است: یک احراز هویت رومینگ تأییدکننده کاربر (UVRA). یک UVRA می تواند دو عامل احراز هویت و مقاومت فیشینگ را در جریان های ورود به سیستم تک مرحله ای ارائه دهد.

در حالت ایده آل، شما از هر دو رویکرد حمایت می کنید. برای انجام این کار، باید تجربه کاربری را سفارشی کنید:

  • اگر کاربر فقط یک احراز هویت رومینگ ساده (غیر تأییدکننده) دارد، اجازه دهید از آن برای دستیابی به یک بوت استرپ اکانت مقاوم در برابر فیشینگ استفاده کند، اما باید نام کاربری و رمز عبور را نیز تایپ کند. این همان کاری است که آزمایشگاه کد ما قبلاً انجام می دهد.
  • اگر کاربر دیگری احراز هویت رومینگ پیشرفته‌تری برای تأیید کاربر داشته باشد، می‌تواند در طول بوت استرپ حساب از مرحله رمز عبور و احتمالاً حتی مرحله نام کاربری رد شود.

در راه‌اندازی حساب مقاوم در برابر فیشینگ با ورود بدون رمز عبور اختیاری درباره این موضوع بیشتر بیاموزید.

در این لبه کد، ما در واقع تجربه کاربر را سفارشی نمی کنیم، اما پایگاه کد شما را به گونه ای تنظیم می کنیم که داده هایی را که برای سفارشی کردن تجربه کاربری نیاز دارید، در اختیار داشته باشید.

شما به دو چیز نیاز دارید:

  • تنظیم residentKey: preferred می شود. این قبلا برای شما انجام شده است.
  • راهی را برای یافتن اینکه آیا یک اعتبار شناسایی (که کلید مقیم نیز نامیده می شود) ایجاد شده است یا خیر تنظیم کنید.

برای اینکه بفهمید آیا اعتبار قابل کشف ایجاد شده است یا خیر:

  • ارزش credProps را هنگام ایجاد اعتبار جستجو کنید ( credProps: true ).
  • ارزش transports را پس از ایجاد اعتبار جستجو کنید. این به شما کمک می کند تا تعیین کنید که آیا پلتفرم زیربنایی از عملکرد UVRA پشتیبانی می کند یا خیر، به عنوان مثال، آیا واقعاً یک تلفن همراه است یا خیر.
  • ارزش credProps و transports را در backend ذخیره کنید. این قبلاً در کد شروع برای شما انجام شده است. اگر کنجکاو هستید به auth.js نگاهی بیندازید.

بیایید ارزش credProps و transports را بدست آوریم و آنها را به backend ارسال کنیم. در auth.client.js ، registerCredential را به صورت زیر تغییر دهید:

  • پس از تماس با navigator.credentials.create یک فیلد extensions اضافه کنید
  • encodedCredential.transports و encodedCredential.credProps قبل از ارسال اعتبار به باطن برای ذخیره سازی تنظیم کنید.

registerCredential باید به صورت زیر باشد:

async function registerCredential() {
  // Fetch the credential creation options from the backend
  const credentialCreationOptionsFromServer = await _fetch(
    '/auth/credential-options',
    'POST'
  );
  // Decode the credential creation options
  const credentialCreationOptions = decodeServerOptions(
    credentialCreationOptionsFromServer
  );
  // Create a credential via the browser API; this will prompt the user
  const credential = await navigator.credentials.create({
    publicKey: {
      ...credentialCreationOptions,
      extensions: {
        credProps: true,
      },
    },
  });
  // Encode the newly created credential to send it to the backend
  const encodedCredential = encodeCredential(credential);
  // Set transports and credProps for more advanced user flows
  encodedCredential.transports = credential.response.getTransports();
  encodedCredential.credProps =
    credential.getClientExtensionResults().credProps;
  // Send the encoded credential to the backend for storage
  return await _fetch('/auth/credential', 'POST', encodedCredential);
}

10. از پشتیبانی بین مرورگرها اطمینان حاصل کنید

از مرورگرهای غیر Chromium پشتیبانی کنید

در تابع registerCredential public/auth.client.js ، ما credential.response.getTransports() را روی اعتبار جدید ایجاد شده فراخوانی می کنیم تا در نهایت این اطلاعات را در backend به عنوان یک اشاره به سرور ذخیره کنیم.

با این حال، getTransports() در حال حاضر در همه مرورگرها پیاده‌سازی نمی‌شود (برخلاف getClientExtensionResults که در مرورگرها پشتیبانی می‌شود): getTransports() یک خطا در فایرفاکس و سافاری ایجاد می‌کند که از ایجاد اعتبار در این مرورگرها جلوگیری می‌کند.

برای اطمینان از اجرا شدن کد شما در همه مرورگرهای اصلی، تماس encodedCredential.transports را در شرایط زیر بپیچید:

if (credential.response.getTransports) {
  encodedCredential.transports = credential.response.getTransports();
}

توجه داشته باشید که در سرور، transports روی transports || [] تنظیم شده است transports || [] . در فایرفاکس و سافاری لیست transports undefined نخواهد بود، بلکه یک لیست خالی [] است که از بروز خطا جلوگیری می کند.

به کاربرانی که از مرورگرهایی استفاده می کنند که از WebAuthn پشتیبانی نمی کنند هشدار دهید

1e9c1be837d66ce8.png

اگرچه WebAuthn در همه مرورگرهای اصلی پشتیبانی می‌شود، بهتر است در مرورگرهایی که از WebAuthn پشتیبانی نمی‌کنند هشداری را نمایش دهید.

در index.html وجود این div را مشاهده کنید:

<div id="warningbanner" class="invisible">
⚠️ Your browser doesn't support WebAuthn. Open this demo in Chrome, Edge, Firefox or Safari.
</div>

در اسکریپت درون خطی index.html ، کد زیر را برای نمایش بنر در مرورگرهایی که از WebAuthn پشتیبانی نمی کنند، اضافه کنید:

// Display a banner in browsers that don't support WebAuthn
if (!window.PublicKeyCredential) {
  document.querySelector('#warningbanner').classList.remove('invisible');
}

در یک برنامه وب واقعی، می‌توانید کارهای دقیق‌تری انجام دهید و مکانیزم بازگشتی مناسب برای این مرورگرها داشته باشید - اما این به شما نشان می‌دهد چگونه پشتیبانی WebAuthn را بررسی کنید.

11. آفرین!

✨تموم شدی!

شما احراز هویت دو مرحله ای را با یک کلید امنیتی پیاده سازی کرده اید.

در این کد لبه به اصول اولیه پرداخته ایم. اگر می‌خواهید WebAuthn را برای 2FA بیشتر کاوش کنید، در اینجا ایده‌هایی وجود دارد که در ادامه می‌توانید امتحان کنید:

  • اطلاعات "آخرین استفاده" را به کارت اعتبار اضافه کنید. این اطلاعات مفیدی برای کاربران است تا تعیین کنند آیا یک کلید امنیتی مشخص به طور فعال استفاده می شود یا خیر - به خصوص اگر آنها چندین کلید را ثبت کرده باشند.
  • مدیریت خطای قوی تر و پیام های خطای دقیق تر را پیاده سازی کنید.
  • به auth.js نگاهی بیندازید و کاوش کنید که با تغییر برخی از تنظیمات authSettings چه اتفاقی می‌افتد، به‌ویژه هنگام استفاده از کلیدی که از تأیید کاربر پشتیبانی می‌کند.