পাসকী যাচাইকরণ

এই নির্দেশিকায় বিস্তারিতভাবে জানানো হয়েছে যে কীভাবে এক্সপ্লিসিট বোতাম ট্রিগার এবং বাধাহীন ব্রাউজার অটোফিল সাজেশন (কন্ডিশনাল UI), দুটির মাধ্যমেই খুঁজে নেওয়ার যোগ্য ক্রেডেনশিয়াল ব্যবহার করে ফিরে আসা ব্যবহারকারীর যাচাইকরণ প্রয়োগ করা যায়।

সার্ভার-সাইড

বিকল্প তৈরি করা

এমন একটি এন্ডপয়েন্ট তৈরি করুন যা স্ট্যান্ডার্ড অনুযায়ী যাচাই করা লাইব্রেরি ব্যবহার করে WebAuthn অনুরোধের প্যারামিটার তৈরি করে।

  1. আগে থেকে নির্ধারিত RP ID ব্যবহার করা: কনস্ট্যান্ট স্ট্রিং হিসেবে আগে থেকে নির্ধারিত সঠিক RP ID ব্যবহার করুন।
  2. চ্যালেঞ্জ জেনারেট করা: হাই-এনট্রপি, ক্রিপ্টোগ্রাফিকভাবে সুরক্ষিত র‍্যান্ডম বাফার জেনারেট করুন, ব্যবহারকারীর সেশনে এটি নিরাপদে স্টোর করুন এবং Base64URL হিসেবে এনকোড করুন।
  3. খুঁজে পাওয়ার যোগ্য ক্রেডেনশিয়াল ম্যাপিং: allowCredentials-এর জন্য খালি অ্যারে [] উল্লেখ করুন। এটি খুঁজে পাওয়ার যোগ্য ক্রেডেনশিয়ালের জন্য অনুরোধ করে, যার অর্থ হল ব্যবহারকারীকে প্রথমে তার ইউজারনেম লিখতে হবে না; পাসকী প্রদানকারী উপলভ্য অ্যাকাউন্ট দেখাবে।
  4. ব্যবহারকারী যাচাইকরণ লেভেল: userVerification: "preferred" সেট করুন (অথবা কর্পোরেট কমপ্লায়েন্স নীতি অনুযায়ী স্পষ্টভাবে বাধ্যতামূলক করা হলে "required")।
    • অনুরোধ করা userVerification কনস্ট্রেন্ট লেভেল অবশ্যই বিকল্প এন্ডপয়েন্টে সার্ভার সেশন রেকর্ডের মধ্যে থাকতে হবে, কোয়েরি স্ট্রিংয়ের মাধ্যমে ক্লায়েন্ট থেকে ফেরত পাঠানো যাবে না। এটি যাচাইকরণ এন্ডপয়েন্টকে ক্লায়েন্ট ম্যানিপুলেশনের ঝুঁকি ছাড়াই কঠোরভাবে ম্যাচিং সংক্রান্ত সীমাবদ্ধতা নিরাপদে এনফোর্স করার অনুমতি দেয়।
// Options generation example (discoverable flow)
const options = {
  challenge: serverGeneratedBase64UrlChallenge, // High-entropy random challenge stored in session
  rpId: "example.com",
  allowCredentials: [], // Request discoverable passkeys
  userVerification: "preferred",
};

// Persist expected UV level to user session
req.session.expectedUserVerification = "preferred";

যাচাইকরণ এন্ডপয়েন্ট

ব্যবহারকারীকে যাচাই করার জন্য ক্লায়েন্টের পাঠানো অ্যাসারশন নিরাপদে যাচাই করুন:

  1. সেশন চ্যালেঞ্জ যাচাই করা: ক্লায়েন্টের উত্তর এবং সেশনে স্টোর করা প্রত্যাশিত চ্যালেঞ্জের মধ্যে কঠোর চ্যালেঞ্জ ম্যাচিং প্রয়োগ করুন।
  2. UV সংক্রান্ত পছন্দ এনফোর্স করুন:
    • সেশনের expectedUserVerification "preferred"-এর অনুরোধ করলে, UV-লেস যাচাইকারীকে (যেমন, যাচাইকারীর স্ক্রিন লক বন্ধ করা) অনুমতি দিন, আপনার সার্ভার-সাইড যাচাইকরণ লাইব্রেরিতে requireUserVerification: false পাস করার মাধ্যমে। "required" অনুরোধ করা হলে, বায়োমেট্রিক্স/পিন এন্ট্রি কঠোরভাবে এনফোর্স করুন।
  3. ক্লিন সার্ভার এরর 404: ক্লায়েন্টের পাঠানো ক্রেডেনশিয়াল আইডি ডেটাবেসে খুঁজে পাওয়া না গেলে, একটি স্পষ্ট HTTP 404 এরর পাঠান যাতে ক্লায়েন্ট সিগন্যাল API ট্রিগার করতে পারে।

ক্লায়েন্ট-সাইড লজিক

HTML ফর্ম অ্যানোটেশন

নেটিভভাবে কন্ডিশনাল UI-এর সুবিধা নিতে আপনার ইউজারনেম ও পাসওয়ার্ড ইনপুট অ্যানোটেট করুন। অটোমেটিক সম্পূর্ণ করার টোকেন webauthn স্পেক প্যারামিটারকে একত্রিত করে এবং ইনপুট ফোকাস করা হলে অটোফোকাস ব্রাউজার অটোফিল পপ-আপকে অবিলম্বে ট্রিগার করে।

<!-- Autocomplete tokens must contain webauthn space-separated -->
<form id="signin-form">
  <input
    type="text"
    name="username"
    autocomplete="username webauthn"
    autofocus
    data-testid="username-field"
  />
  <input type="password" name="password" autocomplete="current-password" />
  <button type="submit">Sign in</button>
</form>

এক্সপ্লিসিট বোতাম ফ্লো

ব্যবহারকারী "পাসকী দিয়ে সাইন-ইন করুন" বোতামে ক্লিক করলে পাসকী যাচাইকরণ ট্রিগার করুন। পাসকী প্রম্পট দেখানোর আগে যেকোনও চলমান ফর্ম অটোফিল (কন্ডিশনাল গেট) কল বাতিল করুন।

কন্ডিশনাল মিডিয়েশন ফ্লো (ফর্ম অটোফিল)

ব্যবহারকারী সাইন-ইন ফিল্ডে ফোকাস করলে, নেটিভভাবে পাসকী যাচাইকরণের সুবিধা দিতে, পৃষ্ঠা লোড হওয়ার সময় ফর্ম অটোফিল সাজেশন অ্যাক্টিভেট করুন:

  1. ফিচার শনাক্ত করা: পৃষ্ঠা লোড হওয়ার সময় PublicKeyCredential.getClientCapabilities() কল করুন এবং conditionalGet উপলভ্য না থাকলে পাসকী দিয়ে সাইন-ইন করার বিকল্প এড়িয়ে যান।
  2. ডিকোড করার বিকল্প: PublicKeyCredential.parseRequestOptionsFromJSON()-এর মাধ্যমে ফেচ করা ক্রেডেনশিয়াল JSON অবজেক্ট ডিকোড করুন।
  3. কন্ডিশনাল গেট ইনভোক করা: mediation: "conditional"-এর সাথে navigator.credentials.get()-কে কল করুন এবং একটি AbortController সিগন্যাল পাস করুন। এটি পাসকী ডায়ালগ রেন্ডার না করেই অটোফিল রেজিস্টার করে।
  4. Try/Catch এক্সেপশন সেগ্রিগেশন: navigator.credentials.get কলকে try/catch ব্লকে র‍্যাপ করুন:
    • NotAllowedError: ব্যবহারকারী পাসকী লগ-ইন প্রম্পট বাতিল করেছেন বা সময় শেষ হয়ে গেছে।
    • AbortError: যাচাইকরণের অনুরোধ প্রোগ্রাম্যাটিক্যালি বাতিল করা হয়েছে।
  5. Call Signal API: try/catch ব্লকে সার্ভার যাচাইকরণ fetch() কল র‍্যাপ করুন:
    • কী সমস্যা হয়েছে তা ব্যবহারকারীকে বোঝানোর জন্য একটি ত্রুটি মেসেজ দেখান।
    • সার্ভার স্পষ্টভাবে HTTP স্ট্যাটাস 404 (ক্রেডেনশিয়াল পাওয়া যায়নি) দিয়ে উত্তর দিলে এবং ব্যবহারকারী যাচাই না করা হলে, তবেই signalUnknownCredential() কল করুন।
    • signalUnknownCredential()-এ পাস করা credentialId প্যারামিটারটি অবশ্যই Base64URL-এনকোড করা ক্রেডেনশিয়াল আইডি স্ট্রিং (যেমন, encoded.id) হতে হবে, কাঁচা ArrayBuffer অবজেক্ট credential.rawId নয়।
  6. উত্তরের এনকোড করা: যাচাইকরণের জন্য সার্ভারে পাঠানোর আগে .toJSON()-এর সাথে ক্রেডেনশিয়াল AuthenticatorAssertionResponse এনকোড করুন।
// optionsFetch and loginVerifyFetch are app-defined HTTP methods
import { optionsFetch, loginVerifyFetch } from "./api.js";

let autofillAbortController = new AbortController();

async function initializeConditionalAutofill() {
  // Feature detect Conditional Get autofill support
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (capabilities.conditionalGet === true) {
    const loginOptionsJSON = await optionsFetch();
    const publicKey =
      PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

    try {
      // Initiate Conditional UI form autofill suggestions
      const credential = await navigator.credentials.get({
        publicKey,
        signal: autofillAbortController.signal,
        mediation: "conditional",
      });

      // Segregated verification fetch
      const encoded = credential.toJSON();
      const response = await loginVerifyFetch(encoded);
      if (!response.ok && response.status === 404) {
        // Note: this code path runs pre-authentication, satisfying the unauth precondition
        if (PublicKeyCredential.signalUnknownCredential) {
          await PublicKeyCredential.signalUnknownCredential({
            rpId, // RP ID must match the one defined on the server
            credentialId: encoded.id,
          });
        }
      }
    } catch (err) {
      // Silently swallow expected client WebAuthn exceptions
      if (["NotAllowedError", "AbortError"].includes(err.name)) {
        return;
      }
      console.error("Unexpected conditional get error:", err);
    }
  }
}

async function triggerButtonAuthentication() {
  // Abort any pending Conditional Get call to prevent passkey prompt collisions
  autofillAbortController.abort();
  autofillAbortController = new AbortController(); // Reset controller for next triggers

  const loginOptionsJSON = await optionsFetch();
  const publicKey =
    PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

  let credential;
  try {
    // Passkey explicit prompt trigger
    credential = await navigator.credentials.get({
      publicKey,
      signal: autofillAbortController.signal,
    });
  } catch (err) {
    if (err.name === "NotAllowedError") {
      console.log("User cancelled passkey login.");
    } else if (err.name === "AbortError") {
      console.log("The authentication operation was aborted.");
    }
    // Re-arm Conditional autofill Suggestions after cancelled explicit button prompts
    initializeConditionalAutofill();
    return; // Safe exit
  }

  // Segregated verification try/catch (HTTP 404 trigger)
  const encoded = credential.toJSON();
  try {
    const response = await loginVerifyFetch(encoded);
    if (!response.ok && response.status === 404) {
      // Note: this code path runs pre-authentication, satisfying the unauth precondition
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encoded.id, // Base64URL-encoded credential ID
      });
    }
  } catch (serverErr) {
    console.error("Verification request error:", serverErr);
  }
}

// Trigger Conditional Get on load
window.addEventListener("DOMContentLoaded", initializeConditionalAutofill);

ফলব্যাক স্ট্র্যাটেজি

পাসকী ফিচার শনাক্তকরণ ফলব্যাক

সবসময় 'webauthn-polyfills' ইনস্টল করুন এবং কনটেক্সটে এটি ইমপোর্ট করুন। যতক্ষণ PublicKeyCredential কাজ করবে, ততক্ষণ PublicKeyCredential.getClientCapabilities-ও কাজ করবে।

import 'webauthn-polyfills';

সিগন্যাল API সিঙ্ক্রোনাইজেশন ফলব্যাক

WebAuthn Signal API (webauthn-signals) হল একটি প্রোগ্রেসিভ অপ্টিমাইজেশন যা পাসওয়ার্ড ম্যানেজারকে সার্ভার ক্রেডেনশিয়াল স্ট্যাটাসের সাথে সিঙ্ক করে রাখতে ব্যবহার করা হয়।

  • ফলব্যাক অভিজ্ঞতা: if (PublicKeyCredential.signalUnknownCredential)-এর মাধ্যমে গেট করা হয়েছে। কাজ না করলে, ব্রাউজার এক্সেপশন না দেখিয়ে ব্যাকগ্রাউন্ড যাচাইকরণ সিঙ্ক বাইপাস করা হয়।

Easy JSON Serialization Fallback

সবসময় 'webauthn-polyfills' ইনস্টল করুন এবং কনটেক্সটে এটি ইমপোর্ট করুন। মনে রাখবেন, যতক্ষণ PublicKeyCredential কাজ করবে, ততক্ষণ PublicKeyCredential.parseRequestOptionsFromJSON এবং PublicKeyCredential.prototype.toJSON-ও কাজ করবে।

import 'webauthn-polyfills';