ईमेल पते के मालिकाना हक की पुष्टि करना

साइन-अप, साइन-इन, चेकआउट या खाता वापस पाने की प्रोसेस के दौरान ईमेल पता इकट्ठा करते समय, मालिकाना हक की पुष्टि करने के लिए, आम तौर पर एक बार इस्तेमाल होने वाला पासकोड (ओटीपी) या मैजिक लिंक भेजना पड़ता है. इसकी वजह से, उपयोगकर्ताओं को आपका पेज छोड़ना पड़ता है, ईमेल मिलने का इंतज़ार करना पड़ता है, और कोड कॉपी करने पड़ते हैं. इससे उपयोगकर्ताओं को परेशानी होती है, वे पेज छोड़ देते हैं, और फ़िशिंग का खतरा बढ़ जाता है. साथ ही, ईमेल की सेवा देने वाली कंपनी को यह पता चल जाता है कि उपयोगकर्ता किस भरोसेमंद पक्ष (आरपी) की वेबसाइट पर जा रहा है.

ईमेल पते की पुष्टि करने वाला एपीआई (क्लाइंट-साइड एचटीएमएल एक्सटेंशन) और ईमेल पते की पुष्टि करने वाला प्रोटोकॉल (ईवीपी) (बैकएंड क्रिप्टोग्राफ़िक प्रोटोकॉल), भरोसा करने वाली इकाई को यह पुष्टि करने की सुविधा देते हैं कि उपयोगकर्ता के पास ईमेल पते का कंट्रोल है. इसके लिए, पुष्टि के लिए भेजा गया ईमेल भेजने की ज़रूरत नहीं पड़ती. ब्राउज़र, भरोसा करने वाली इकाई और उपयोगकर्ता को ईमेल की सेवा देने वाली कंपनी (ईमेल जारी करने वाली कंपनी) के बीच मध्यस्थ के तौर पर काम करता है:

  1. भरोसेमंद पक्ष, ईमेल इनपुट वाले <form> में autocomplete="email-verification-token" के साथ छिपा हुआ <input> और एक बार इस्तेमाल होने वाला क्रिप्टोग्राफ़िक nonce शामिल करता है.
  2. जब उपयोगकर्ता कोई ईमेल पता चुनता है या डालता है, तो ब्राउज़र डोमेन का _email-verification.<domain> डीएनएस TXT रिकॉर्ड खोजता है. साथ ही, यह पुष्टि करता है कि उपयोगकर्ता का सेशन, आधिकारिक तौर पर जारी करने वाली इकाई के साथ चालू है. इसके बाद, वह ईमेल पते की पुष्टि करने वाले टोकन (ईवीटी) का अनुरोध करता है. इसके लिए, वह जारी करने वाली इकाई को भरोसा करने वाली इकाई की पहचान ज़ाहिर नहीं करता.
  3. ब्राउज़र, ईवीटी को भरोसा करने वाली पार्टी के ऑरिजिन और सेशन nonce से बाइंड करता है. इसके लिए, वह की बाइंडिंग JWT (केबी-JWT) का इस्तेमाल करता है. इस JWT पर, ब्राउज़र की कुछ समय के लिए इस्तेमाल होने वाली कुंजी के जोड़े (EVT + KB-JWT) से हस्ताक्षर किया जाता है. इसके बाद, ब्राउज़र छिपे हुए इनपुट को भरता है और फ़ॉर्म सबमिट करता है.
  4. आरपी का बैकएंड, क्रिप्टोग्राफ़िक चेन की पुष्टि करता है. अगर यह मान्य है, तो ईमेल पर ओटीपी या मैजिक लिंक भेजने का चरण छोड़ दिया जाता है.

1. ऑरिजिन ट्रायल चालू करना और पुष्टि करने वाला फ़ॉर्म रेंडर करना

पुष्टि करने वाली साइट (भरोसेमंद पक्ष) के तौर पर हिस्सा लेने के लिए:

  1. फ़ॉर्म को होस्ट करने वाले पेज पर ऑरिजिन ट्रायल टोकन दें. इसके लिए, Origin-Trial एचटीटीपी रिस्पॉन्स हेडर या <meta http-equiv="origin-trial"> एलिमेंट का इस्तेमाल करें.
    • तीसरे पक्ष के ऑरिजिन ट्रायल: एम्बेड किए गए आइडेंटिटी एसडीके, तीसरे पक्ष के ट्रायल टोकन को इंजेक्ट कर सकते हैं. हालांकि, इसके लिए ज़रूरी है कि ऑरिजिन ट्रायल के लिए रजिस्टर करने वाले व्यक्ति का ऑरिजिन, टोकन जारी करने वाले डोमेन से मैच करता हो. जैसे, https://issuer.example.
  2. हर फ़ॉर्म रेंडर के लिए, क्रिप्टोग्राफ़िक तरीके से रैंडम नॉन्स जनरेट करें. इसमें कम से कम 128 बिट का एन्ट्रोपी होना चाहिए (उदाहरण के लिए, node:crypto से randomBytes(32).toString('base64url')). इसे सर्वर-साइड सेशन स्टेट में सेव करें. इसे HttpOnly, Secure, SameSite=Lax सेशन आईडी कुकी से कीवर्ड किया गया हो. उम्मीद के मुताबिक nonce वैल्यू को सीधे क्लाइंट कुकी में सेव न करें, जहां हमलावर कुकी और टोकन, दोनों को फिर से चला सकता है. साथ ही, इसे छिपे हुए टोकन इनपुट के nonce एट्रिब्यूट में रेंडर करें. पेज को Cache-Control: no-store के साथ दिखाएं, ताकि बैक/फ़ॉरवर्ड नेविगेशन, पहले से इस्तेमाल किए गए नॉन्स का फिर से इस्तेमाल न करे.
  3. छिपे हुए टोकन इनपुट (autocomplete="email-verification-token") को उसी <form> में जोड़ें जिसमें ईमेल इनपुट (autocomplete="email") है.
<!-- 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>

2. सर्वर पर सबमिट किए गए टोकन की पुष्टि करना

फ़ॉर्म सबमिट होने पर, token फ़ील्ड को भरोसेमंद इनपुट न मानें. रसीद मिलने पर, अपने सर्वर-साइड सेशन स्टोर से, उम्मीद के मुताबिक nonce को अपने-आप पढ़ें और मिटाएं. अगर token फ़ील्ड खाली है (उदाहरण के लिए, ईवीपी की सुविधा न देने वाले ब्राउज़र में या जब उपयोगकर्ता ने ईमेल की सेवा देने वाली कंपनी के खाते से साइन आउट किया हो) या पुष्टि नहीं हो पाती है, तो तुरंत ईमेल पर ओटीपी या मैजिक लिंक भेजने के स्टैंडर्ड फ़्लो पर वापस जाएं:

// 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 को लागू करने के लिए, अपने बैकएंड प्लैटफ़ॉर्म के लिए हमेशा पहले से मौजूद SD-JWT और JOSE/JWT लाइब्रेरी का इस्तेमाल करें. जैसे, Node.js में @sd-jwt/core और jose, Python में sd-jwt-python / jwcrypto या JVM पर sd-jwt-java / Nimbus JOSE+JWT. कस्टम JWT या SD-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.');
}

दूसरा चरण: डीएनएस टीएक्सटी डेलिगेशन की पुष्टि करना और जारी करने वाले के JWKS को लोड करना

iss को नेटवर्क का कोई अनुरोध भेजने से पहले, पुष्टि करें कि सबमिट किए गए ईमेल का डोमेन, _email-verification.<domain> डीएनएस TXT रिकॉर्ड (iss=<issuer-host> -> https://<issuer-host>, जिसमें कोई पोर्ट, पाथ या ट्रेलिंग स्लैश न हो) के ज़रिए, उस जारी करने वाले को अधिकार देता है. जब डीएनएस डेलिगेशन, iss से पूरी तरह मैच हो जाए, तो एचटीटीपीएस की मदद से ${iss}/.well-known/email-verification को फ़ेच करें. साथ ही, पुष्टि करें कि metadata.issuer === iss और जारी करने वाले का JSON वेब की सेट (JWKS) लोड करें.

kid को मैनेज करने के बारे में जानकारी: ईमेल की सेवा देने वाली कुछ मुख्य कंपनियां (जैसे, Gmail), ईवीटी हेडर से 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));

तीसरा चरण: ईवीटी सिग्नेचर और की बाइंडिंग JWT (KB-JWT) की पुष्टि करना

आखिर में, दोनों सिग्नेचर और दोनों टोकन के बीच क्रिप्टोग्राफ़िक बाइंडिंग की पुष्टि करें. इसके लिए, अनुमति वाली सूची (['Ed25519', 'EdDSA', 'ES256']) में मौजूद, एसिमेट्रिक एल्गोरिदम का इस्तेमाल करें:

  1. ईवीटी सिग्नेचर (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,
});

सबसे सही तरीके

  • सभी टोकन की पुष्टि, सर्वर पर ही करें. क्लाइंट-साइड JavaScript में टोकन की पुष्टि न करें या क्लाइंट-साइड पर पुष्टि की किसी भी स्थिति पर भरोसा न करें. क्लाइंट-साइड पर की जाने वाली जांच को आसानी से बायपास किया जा सकता है या इसमें धोखाधड़ी की जा सकती है. साथ ही, भरोसा करने वाली पार्टी को भरोसे की कोई गारंटी नहीं दी जाती.
  • अपने बैकएंड प्लैटफ़ॉर्म के लिए, पहले से मौजूद SD-JWT और JOSE/JWT लाइब्रेरी का इस्तेमाल ज़रूर करें. उदाहरण के लिए, Node.js में @sd-jwt/core और jose का इस्तेमाल करें. इससे EVT~KB-JWT प्रज़ेंटेशन को पार्स और उसकी पुष्टि की जा सकेगी. साथ ही, keyBindingNonce को sdJwt.verify() में पास किया जा सकेगा, ताकि की बाइंडिंग की पुष्टि को बायपास न किया जा सके. कस्टम JWT, SD-JWT या sd_hash को पार्स करने और हस्ताक्षर की पुष्टि करने के रूटीन को खुद से न बनाएं.
  • हर फ़ॉर्म रेंडर के लिए, सर्वर पर एक नया क्रिप्टोग्राफ़िक nonce (कम से कम 128 बिट, जैसे कि randomBytes(32).toString('base64url')) जनरेट करें. इसे सर्वर-साइड सेशन स्टेट में सेव करें (सीधे क्लाइंट कुकी में नहीं). साथ ही, फ़ॉर्म पेज को Cache-Control: no-store के साथ दिखाएं.
  • POST अनुरोध मिलने पर, सर्वर-साइड सेशन स्टोरेज से, उम्मीद के मुताबिक nonce को तुरंत पढ़ें और मिटाएं. इससे, इंटरसेप्ट किए गए टोकन को फिर से इस्तेमाल नहीं किया जा सकेगा.
  • evtPayload.email और सबमिट किए गए फ़ॉर्म के ईमेल पते (email.toLowerCase()) के बीच केस-इनसेंसिटिव तुलना करें. साथ ही, सबमिट की गई ईमेल स्ट्रिंग को सुरक्षित रखें.
  • जब ईवीटी हेडर में kid मौजूद न हो, तो जारी करने वाले के JWKS में मौजूद सभी कुंजियों की पुष्टि करें. Gmail जैसे सेवा देने वाले, kid को शामिल नहीं करते.
  • ईवीटी में किसी भी iss दावे पर भरोसा करने से पहले, डीएनएस डेलिगेशन (_email-verification.<domain>) की पुष्टि ज़रूर करें. साथ ही, .well-known/email-verification रिस्पॉन्स में metadata.issuer === iss की पुष्टि करें.
  • ईमेल पते के मालिकाना हक की पुष्टि को, इनबॉक्स में ईमेल डिलीवर होने के सबूत के तौर पर न देखें. ईवीपी से यह साबित होता है कि उपयोगकर्ता के पास, आधिकारिक सेवा देने वाली कंपनी के ईमेल खाते का कंट्रोल है. हालांकि, लेन-देन से जुड़े वेलकम ईमेल भेजना और बाउंस होने वाले ईमेल को सामान्य तरीके से मैनेज करना जारी रखें.
  • token खाली होने या पुष्टि न होने पर, खाता बनाने या साइन इन करने की प्रोसेस को ब्लॉक न करें. इसके बजाय, स्टैंडर्ड ईमेल ओटीपी या मैजिक लिंक फ़्लो पर स्विच करें.

फ़ॉलबैक रणनीति

ईमेल पते की पुष्टि करने की सुविधा (autocomplete="email-verification-token") को, आउट-ऑफ़-बैंड ईमेल पते की पुष्टि करने के पारंपरिक तरीकों (ओटीपी कोड या मैजिक लिंक) के मुकाबले, परतदार वृद्धि के तौर पर डिज़ाइन किया गया है:

  • इस्तेमाल न किए जा सकने वाले ब्राउज़र: autocomplete="email-verification-token" के साथ काम न करने वाले ब्राउज़र, छिपे हुए इनपुट को अनदेखा कर देते हैं और फ़ॉर्म को खाली token वैल्यू के साथ सबमिट कर देते हैं.
  • ऐसे ईमेल डोमेन जिनकी पुष्टि नहीं हुई है या जो इस सुविधा में शामिल नहीं हैं: जब कोई उपयोगकर्ता ऐसा ईमेल पता डालता है जिसका डोमेन _email-verification डीएनएस TXT रिकॉर्ड पब्लिश नहीं करता है या जब उपयोगकर्ता के पास ब्राउज़र में ईमेल की सेवा देने वाली कंपनी के साथ कोई चालू सेशन नहीं होता है, तो ब्राउज़र फ़ॉर्म को खाली token के साथ सबमिट कर देता है.
  • समयसीमा खत्म हो चुके या अमान्य टोकन: अगर nonce की समयसीमा खत्म हो गई है, उसे पहले ही इस्तेमाल कर लिया गया है या वह handleSignupSubmission में verifyEmailVerificationToken के किसी भी चरण को पूरा नहीं कर पाता है, तो सर्वर गड़बड़ी का पता लगाता है. इसके बाद, उपयोगकर्ता के साइन-अप या साइन-इन फ़्लो में रुकावट डाले बिना, सामान्य ओटीपी या मैजिक लिंक वाला ईमेल भेजता है.