ইমেল আইডির মালিকানা যাচাই করুন

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

ইমেল যাচাইকরণ API (ক্লায়েন্ট-সাইড HTML এক্সটেনশন) এবং ইমেল যাচাইকরণ প্রোটোকল (EVP) (ব্যাকএন্ড ক্রিপ্টোগ্রাফিক প্রোটোকল) কোনও যাচাইকরণ ইমেল না পাঠিয়েই, কোনও ব্যবহারকারী ইমেল আইডি নিয়ন্ত্রণ করেন কিনা তা যাচাই করার জন্য কোনও রিলাইং পার্টিকে সক্ষম করে। ব্রাউজার, রিলাইং পার্টি ও ব্যবহারকারীর ইমেল পরিষেবা প্রদানকারীর (ইস্যুকারী) মধ্যে মধ্যস্থতা করে:

  1. রিলাইং পার্টিতে autocomplete="email-verification-token" সহ একটি লুকানো <input> এবং ইমেল ইনপুটের মতো একই <form>-এ একটি সিঙ্গল-ইউজ ক্রিপ্টোগ্রাফিক nonce অন্তর্ভুক্ত থাকে।
  2. ব্যবহারকারী কোনও ইমেল আইডি বেছে নিলে বা লিখলে, ব্রাউজার ডোমেনের _email-verification.<domain> DNS TXT রেকর্ড খুঁজে দেখে, ব্যবহারকারীর সাথে অথরিটেটিভ ইস্যুকারীর অ্যাক্টিভ সেশন আছে কিনা তা যাচাই করে এবং ইস্যুকারীর কাছে রিলাইং পার্টির পরিচয় প্রকাশ না করেই ইমেল যাচাইকরণ টোকেন (EVT)-এর অনুরোধ করে।
  3. ব্রাউজারটি EVT-কে রিলাইং পার্টির অরিজিন ও সেশন nonce-এর সাথে কী বাইন্ডিং JWT (KB-JWT)-তে বাইন্ড করে, যা একটি ক্ষণস্থায়ী ব্রাউজার কী পেয়ার (EVT + KB-JWT) দিয়ে সাইন করা হয়, লুকানো ইনপুট পূরণ করে এবং ফর্ম জমা দেয়।
  4. নির্ভরশীল পার্টির ব্যাকএন্ড ক্রিপ্টোগ্রাফিক চেন যাচাই করে এবং সেটি সঠিক হলে ইমেল ওটিপি বা ম্যাজিক লিঙ্ক সংক্রান্ত ধাপটি এড়িয়ে যায়।

১. অরিজিন ট্রায়াল চালু করা এবং যাচাইকরণ ফর্ম রেন্ডার করা

যাচাইকারী সাইট (নির্ভরশীল পার্টি) হিসেবে অংশগ্রহণ করতে:

  1. Origin-Trial HTTP রেসপন্স হেডার বা <meta http-equiv="origin-trial"> এলিমেন্ট ব্যবহার করে ফর্ম হোস্ট করা পৃষ্ঠায় অরিজিন ট্রায়াল টোকেন দিন।
    • থার্ড-পার্টি অরিজিন ট্রায়াল: এম্বেড করা পরিচয় SDK থার্ড-পার্টি ট্রায়াল টোকেন ইনজেক্ট করতে পারে, তবে অরিজিন ট্রায়াল রেজিস্টারকারীর অরিজিন ইস্যুকারীর ডোমেনের সাথে মিলতে হবে (ইস্যুকারীর সাথে একই সাইট, যেমন https://issuer.example)।
  2. কমপক্ষে ১২৮ বিট এনট্রপি (যেমন, node:crypto থেকে randomBytes(32).toString('base64url')) সহ প্রতি ফর্ম রেন্ডারে ক্রিপ্টোগ্রাফিকভাবে র‍্যান্ডম ননস জেনারেট করুন, এটি HttpOnly, Secure, SameSite=Lax সেশন আইডি কুকি দ্বারা কী করা সার্ভার-সাইড সেশন স্টেটে স্টোর করুন (কোনও ক্লায়েন্ট কুকির মধ্যে সরাসরি প্রত্যাশিত nonce ভ্যালু কখনই স্টোর করবেন না যেখানে আক্রমণকারী কুকি এবং টোকেন উভয়ই রিপ্লে করতে পারে) এবং লুকানো টোকেন ইনপুটের nonce অ্যাট্রিবিউটে এটি রেন্ডার করুন। Cache-Control: no-store-এর মাধ্যমে পৃষ্ঠাটি পরিবেশন করুন যাতে ব্যাক/ফরওয়ার্ড নেভিগেশন আগে থেকে ব্যবহার করা ননস আবার ব্যবহার না করে।
  3. ইমেল ইনপুটের (autocomplete="email") মতো একই <form>-এর মধ্যে লুকানো টোকেন ইনপুট যোগ করুন (autocomplete="email-verification-token")।
<!-- Serve with:
     Origin-Trial: <YOUR_ORIGIN_TRIAL_TOKEN>
     Cache-Control: no-store -->
<form method="POST" action="/api/verify-email">
  <label for="email">Email address:</label>
  <input
    type="email"
    id="email"
    name="email"
    autocomplete="email"
    required
  />

  <!-- MANDATORY: Must be in the same <form> as the email input.
       The nonce value must be freshly generated on the server per form render. -->
  <input
    type="hidden"
    name="token"
    nonce="SERVER_GENERATED_PER_FORM_NONCE"
    autocomplete="email-verification-token"
  />

  <button type="submit">Continue</button>
</form>

২. সার্ভারে জমা দেওয়া টোকেন যাচাই করুন

ফর্ম জমা দেওয়ার সময়, token ফিল্ডকে বিশ্বস্ত নয় এমন ইনপুট হিসেবে বিবেচনা করুন। রিসিভ করার পরে আপনার সার্ভার-সাইড সেশন স্টোর থেকে প্রত্যাশিত nonce অ্যাটমিক্যালি পড়ুন এবং মুছে দিন। token ফিল্ড খালি থাকলে (যেমন, EVP কাজ করে না এমন ব্রাউজারে অথবা ব্যবহারকারী তার ইমেল পরিষেবা প্রদানকারী থেকে সাইন-আউট করে থাকলে) অথবা যাচাইকরণ ব্যর্থ হলে, অবিলম্বে আপনার স্ট্যান্ডার্ড ইমেল ওটিপি বা ম্যাজিক লিঙ্ক ফ্লোতে ফিরে যান:

// Replace with your public relying-party origin
const RP_ORIGIN = 'https://rp.example.com';

export async function handleSignupSubmission(request) {
  const formData = await request.formData();
  const submittedEmail = String(formData.get('email') || '').trim();
  const rawToken = String(formData.get('token') || '').trim();
  // Atomically read and delete the single-use nonce from server-side session state
  const expectedNonce = await consumeSessionNonce(request);

  if (rawToken && expectedNonce) {
    try {
      await verifyEmailVerificationToken({
        rawToken,
        submittedEmail,
        expectedNonce,
        expectedAudience: RP_ORIGIN,
      });
      // Fast path: email ownership is cryptographically verified; skip OTP email.
      return completeAccountCreation(submittedEmail);
    } catch (err) {
      // Log token verification failures before falling back to standard email verification.
      console.error('Email verification token check failed:', err);
    }
  }

  // Fallback path: send standard OTP code or magic link email to submittedEmail.
  return sendTraditionalVerificationEmail(submittedEmail);
}

verifyEmailVerificationToken প্রয়োগ করার জন্য, কাস্টম JWT বা SD-JWT পার্সিং কোড ম্যানুয়ালি তৈরি করার পরিবর্তে, আপনার ব্যাকএন্ড প্ল্যাটফর্মের জন্য (যেমন Node.js-এ @sd-jwt/core এবং jose, Python-এ sd-jwt-python / jwcrypto অথবা JVM-এ sd-jwt-java / Nimbus JOSE+JWT) সবসময় প্রতিষ্ঠিত SD-JWT এবং JOSE/JWT লাইব্রেরি ব্যবহার করুন।

ধাপ ১: SD-JWT দাবি পার্স করুন এবং ইমেল আইডি যাচাই করুন

সার্ভার-সাইড সেশন স্টোরেজ থেকে সিঙ্গল-ইউজ expectedNonce ব্যবহার করার পরে, SD-JWT প্রেজেন্টেশন (EVT + KB-JWT) ডিকোড করুন এবং যাচাই করুন যে:

  • ইস্যুকারী JWT (jwt) এবং কী বাইন্ডিং JWT (kbJwt) উভয়ই ইস্যুকারী ক্লেম (iss)-এর সাথে উপস্থিত আছে।
  • evtPayload.email_verified === true।
  • evtPayload.email কেস-ইনসেনসিটিভ তুলনা (toLowerCase()) ব্যবহার করে জমা দেওয়া ফর্মের ইমেলের সাথে ম্যাচ করে।
import { createHash } from 'node:crypto';
import { decodeSdJwtSync } from '@sd-jwt/core';

const hasher = (data, alg) =>
  createHash(alg === 'sha-256' ? 'sha256' : alg).update(data).digest();

const decoded = decodeSdJwtSync(rawToken, hasher);
const evtPayload = decoded.jwt.payload;
const iss = evtPayload.iss;

if (!expectedNonce || !decoded.kbJwt || !iss || evtPayload.email_verified !== true) {
  throw new Error('Missing nonce, KB-JWT, issuer, or email_verified !== true.');
}
// MANDATORY: Compare email addresses case-insensitively
if (evtPayload.email?.toLowerCase() !== submittedEmail.toLowerCase()) {
  throw new Error('Submitted email does not match the EVT email claim.');
}

ধাপ ২: DNS TXT ডেলিগেশন যাচাই করুন এবং ইস্যুকারীর JWKS লোড করুন

iss-এ কোনও নেটওয়ার্ক অনুরোধ করার আগে, জমা দেওয়া ইমেলের ডোমেন _email-verification.<domain> ডিএনএস TXT রেকর্ডের (iss=<issuer-host> -> https://<issuer-host> কোনও পোর্ট, পাথ বা ট্রেলিং স্ল্যাশ ছাড়া) মাধ্যমে সেই নির্দিষ্ট ইস্যুকারীকে কর্তৃত্ব অর্পণ করে কিনা তা যাচাই করুন। ডিএনএস ডেলিগেশন iss বাইট-ফর-বাইটের সাথে মিলে গেলে, HTTPS-এর মাধ্যমে ${iss}/.well-known/email-verification ফেচ করুন, metadata.issuer === iss কনফার্ম করুন এবং ইস্যুকারীর JSON ওয়েব কী সেট (JWKS) লোড করুন।

kid হ্যান্ডেল করা সংক্রান্ত নোট: কিছু প্রধান ইমেল পরিষেবা প্রদানকারী (যেমন Gmail) EVT হেডার থেকে kid বাদ দেয়। kid অনুপস্থিত থাকলে এবং JWKS-এ একাধিক কী থাকলে, jose ERR_JWKS_MULTIPLE_MATCHING_KEYS (ম্যাচিং CryptoKey-এর AsyncIterable)—প্রতিটি কী ট্রায়াল-যাচাই করতে for await (const publicKey of err) ইটারেট করুন, যেমনটি ৩ নম্বর ধাপে দেখানো হয়েছে।

import dns from 'node:dns/promises';
import { createRemoteJWKSet } from 'jose';

const domain = submittedEmail.split('@')[1];
const txtRecords = (await dns.resolveTxt(`_email-verification.${domain}`)).map((r) => r.join(''));
if (!txtRecords.some((txt) => txt.startsWith('iss=') && `https://${txt.slice(4).trim()}` === iss)) {
  throw new Error(`Issuer "${iss}" is not delegated via DNS for "${domain}".`);
}

const discoveryUrl = new URL('/.well-known/email-verification', iss);
if (discoveryUrl.protocol !== 'https:') throw new Error('Issuer must use HTTPS.');
const metadata = await (await fetch(discoveryUrl)).json();
if (metadata.issuer !== iss || !metadata.jwks_uri?.startsWith('https://')) {
  throw new Error('Invalid issuer metadata or non-HTTPS jwks_uri.');
}
const issuerJwks = createRemoteJWKSet(new URL(metadata.jwks_uri));

ধাপ ৩: EVT স্বাক্ষর ও কী বাইন্ডিং JWT (KB-JWT) যাচাই করুন

সবশেষে, এক্সপ্লিসিট অ্যাসিমেট্রিক অ্যালগরিদম অনুমোদিত তালিকা (['Ed25519', 'EdDSA', 'ES256']) ব্যবহার করে দুটি টোকেনের মধ্যে দুটি স্বাক্ষর এবং ক্রিপ্টোগ্রাফিক বাইন্ডিং যাচাই করুন:

  1. EVT স্বাক্ষর (verifier): issuerJwks-এর সাথে যাচাই করুন (kid বাদ দেওয়া হলে ক্যান্ডিডেট কী ইটারেট করার জন্য ERR_JWKS_MULTIPLE_MATCHING_KEYS ক্যাচ করা), iss এবং একটি টাইট ফ্রেশনেস উইন্ডো (maxTokenAge: '5m', clockTolerance: '1m') এনফোর্স করা।
  2. হোল্ডার বাইন্ডিং (kbVerifier এবং sdJwt.verify বিকল্প): evtPayload.cnf.jwk থেকে ব্রাউজারের ক্ষণস্থায়ী সর্বজনীন কী এক্সট্র্যাক্ট করুন, কনফার্ম করুন যে cnf.jwk.alg এবং decoded.kbJwt.header.alg দুটিই ALLOWED_ALGS-এ আছে এবং ম্যাচ করে (অরিজিন ট্রায়াল ইমপ্লিমেন্টেশন RFC 9864 'Ed25519'-এ ট্রানজিশন করার সময় 'Ed25519' এবং 'EdDSA'-কে সমতুল্য হিসেবে বিবেচনা করে) এবং typ: 'kb+jwt'-এর মাধ্যমে KB-JWT স্বাক্ষর যাচাই করুন। keyBindingNonce: expectedNonce, expectedKeyBindingAudience: expectedAudience এবং keyBindingMaxAgeSeconds: 300-কে sdJwt.verify()-এ পাস করুন—@sd-jwt/core শুধুমাত্র kbVerifier-কে ইনভোক করে এবং keyBindingNonce প্রদান করা হলে nonce, aud, iat এবং sd_hash-কে যাচাই করে।
import { SDJwtInstance } from '@sd-jwt/core';
import { importJWK, jwtVerify } from 'jose';

// IETF draft-hardt-email-verification-02 (§3.3, §5.1.1) specifies RFC 9864 'Ed25519' and 'ES256'.
// TODO: Remove 'EdDSA' once live issuers (e.g. Gmail) and Chrome Origin Trial builds finish
// transitioning from 'EdDSA' (with crv: 'Ed25519') to 'Ed25519'.
const ALLOWED_ALGS = ['Ed25519', 'EdDSA', 'ES256'];
const normalizeAlg = (alg) => (alg === 'Ed25519' ? 'EdDSA' : alg);
const evtOptions = {
  typ: 'ev+sd-jwt',
  issuer: iss,
  algorithms: ALLOWED_ALGS,
  maxTokenAge: '5m',
  clockTolerance: '1m',
};

const sdJwt = new SDJwtInstance({
  hasher,
  verifier: async (data, sig) => {
    const compactEvt = `${data}.${sig}`;
    try {
      await jwtVerify(compactEvt, issuerJwks, evtOptions);
    } catch (err) {
      // When `kid` is omitted (e.g. Gmail) and multiple keys match in the JWKS,
      // jose throws JWKSMultipleMatchingKeys, which yields each matching CryptoKey.
      if (err?.code !== 'ERR_JWKS_MULTIPLE_MATCHING_KEYS') throw err;
      let matched = false;
      for await (const publicKey of err) {
        try {
          await jwtVerify(compactEvt, publicKey, evtOptions);
          matched = true;
          break;
        } catch {}
      }
      if (!matched) throw err;
    }
    return true;
  },
  kbVerifier: async (data, sig) => {
    const holderJwk = evtPayload.cnf?.jwk;
    const holderAlg = holderJwk?.alg;
    const kbAlg = decoded.kbJwt.header.alg;
    if (
      !holderAlg ||
      !kbAlg ||
      !ALLOWED_ALGS.includes(holderAlg) ||
      !ALLOWED_ALGS.includes(kbAlg) ||
      normalizeAlg(kbAlg) !== normalizeAlg(holderAlg)
    ) {
      throw new Error('Missing, disallowed, or mismatched KB-JWT algorithm.');
    }
    const holderKey = await importJWK(holderJwk, kbAlg);
    await jwtVerify(`${data}.${sig}`, holderKey, {
      typ: 'kb+jwt',
      audience: expectedAudience,
      algorithms: [kbAlg],
      maxTokenAge: '5m',
      clockTolerance: '1m',
    });
    return true;
  },
});

const verified = await sdJwt.verify(rawToken, {
  requiredClaimKeys: ['email', 'email_verified'],
  keyBindingNonce: expectedNonce,
  expectedKeyBindingAudience: expectedAudience,
  keyBindingMaxAgeSeconds: 300,
  skewSeconds: 60,
});

পেশাদার পদ্ধতি

  • সার্ভারে সমস্ত টোকেন যাচাইকরণ কঠোরভাবে করুন। ক্লায়েন্ট-সাইড জাভাস্ক্রিপ্টে টোকেন যাচাই করবেন না অথবা কোনও ক্লায়েন্ট-সাইড যাচাইকরণ স্ট্যাটাসকে বিশ্বাস করবেন না—ক্লায়েন্ট-সাইড চেক সহজেই এড়িয়ে যাওয়া বা জাল করা যায় এবং এটি নির্ভরকারী পার্টিকে কোনও বিশ্বাসযোগ্যতার গ্যারেন্টি দেয় না।
  • আপনার ব্যাকএন্ড প্ল্যাটফর্মের জন্য (যেমন, Node.js-এ @sd-jwt/core এবং jose) প্রতিষ্ঠিত SD-JWT এবং JOSE/JWT লাইব্রেরি ব্যবহার করুন যাতে EVT~KB-JWT প্রেজেন্টেশন পার্স ও যাচাই করা যায় এবং keyBindingNonce-কে sdJwt.verify()-এ পাস করা যায়, যাতে 'কী বাইন্ডিং' যাচাইকরণ বাইপাস করা না যায়। কাস্টম JWT, SD-JWT বা sd_hash পার্সিং এবং সিগনেচার যাচাইকরণ রুটিন হ্যান্ড-রোল করবেন না।
  • প্রতিটি ফর্ম রেন্ডার করার জন্য সার্ভারে একটি নতুন ক্রিপ্টোগ্রাফিক nonce (কমপক্ষে ১২৮ বিট, যেমন randomBytes(32).toString('base64url')) জেনারেট করুন, এটি সার্ভার-সাইড সেশন স্টেট-এ (সরাসরি ক্লায়েন্ট কুকিতে নয়) স্টোর করুন এবং Cache-Control: no-store সহ ফর্ম পৃষ্ঠাটি পরিবেশন করুন।
  • POST অনুরোধ আসার সাথে সাথে সার্ভার-সাইড সেশন স্টোরেজ থেকে প্রত্যাশিত nonce অবিলম্বে অ্যাটমিক্যালি পড়ুন এবং মুছে দিন করুন যাতে ইন্টারসেপ্ট করা টোকেন আবার চালানো না যায়।
  • জমা দেওয়া ইমেল স্ট্রিং সুরক্ষিত রেখে, evtPayload.email এবং জমা দেওয়া ফর্মের ইমেল আইডির (email.toLowerCase()) মধ্যে কেস-ইনসেনসিটিভ তুলনা করুন।
  • EVT হেডারে kid না থাকলে, ইস্যুকারীর JWKS-এ সব কী ট্রায়াল-যাচাই করুন (Gmail-এর মতো পরিষেবা প্রদানকারীরা kid বাদ দেয়)।
  • EVT-তে কোনও iss দাবি বিশ্বাস করার আগে DNS ডেলিগেশন (_email-verification.<domain>) যাচাই করুন এবং .well-known/email-verification উত্তরে metadata.issuer === iss কনফার্ম করুন।
  • ইমেল মালিকানা যাচাইকরণকে ইনবক্সে ডেলিভারি হওয়ার প্রমাণ হিসেবে বিবেচনা করবেন না। EVP প্রমাণ করে যে ব্যবহারকারী অথরিটেটিভ পরিষেবা প্রদানকারীর মাধ্যমে ইমেল অ্যাকাউন্ট নিয়ন্ত্রণ করেন; এখনও ট্রানজ্যাকশন সংক্রান্ত স্বাগত ইমেল পাঠান এবং বাউন্স হওয়া ইমেল স্বাভাবিকভাবে ম্যানেজ করেন।
  • token খালি থাকলে বা যাচাইকরণ ব্যর্থ হলে অ্যাকাউন্ট তৈরি বা সাইন-ইন করা ব্লক করবেন না—সবসময় আপনার স্ট্যান্ডার্ড ইমেল ওটিপি বা ম্যাজিক লিঙ্ক ফ্লোতে সুন্দরভাবে ডিগ্রেড করুন।

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

ইমেল যাচাইকরণ (autocomplete="email-verification-token") প্রথাগত আউট-অফ-ব্যান্ড ইমেল যাচাইকরণের (ওটিপি কোড বা ম্যাজিক লিঙ্ক) উপর প্রোগ্রেসিভ এনহ্যান্সমেন্ট হিসেবে ডিজাইন করা হয়েছে:

  • কাজ করে না এমন ব্রাউজার: যেসব ব্রাউজারে autocomplete="email-verification-token" কাজ করে না সেগুলি লুকানো ইনপুট উপেক্ষা করে এবং খালি token ভ্যালু সহ ফর্ম জমা দেয়।
  • যাচাই না করা বা অংশগ্রহণ না করা ইমেল ডোমেন: কোনও ব্যবহারকারী এমন ইমেল আইডি লিখলে যার ডোমেন _email-verification DNS TXT রেকর্ড প্রকাশ করে না অথবা ব্রাউজারে ইমেল পরিষেবা প্রদানকারীর সাথে ব্যবহারকারীর কোনও অ্যাক্টিভ সেশন না থাকলে, ব্রাউজার খালি token সহ ফর্ম জমা দেয়।
  • মেয়াদ শেষ হয়ে যাওয়া বা ভুল টোকেন: nonce-এর মেয়াদ শেষ হয়ে গেলে, আগে থেকেই ব্যবহার করা হয়ে থাকলে অথবা handleSignupSubmission-এ verifyEmailVerificationToken-এর কোনও ধাপ সম্পূর্ণ করতে না পারলে, সার্ভার সমস্যাটি শনাক্ত করে এবং ব্যবহারকারীর সাইন-আপ বা সাইন-ইন ফ্লোতে বাধা না দিয়ে একটি সাধারণ ওটিপি বা ম্যাজিক লিঙ্ক ইমেল পাঠায়।