← All posts

कई टेनेंट के लिए एक BFF, और वह एक अनुरोध जो बिना किसी कुकी के आता है

Authagonal·July 28, 2026

सिंगल-पेज ऐप्स को अपने टोकन खुद संभालना सिखाया गया था। ऐप ब्राउज़र में ही OAuth फ़्लो चलाता है, एक एक्सेस टोकन और आमतौर पर एक रिफ़्रेश टोकन वापस पाता है, उन्हें JavaScript में कहीं रखता है, और हर API कॉल के साथ जोड़ देता है। यह अच्छी तरह प्रलेखित है, यही ज़्यादातर ट्यूटोरियल दिखाते हैं, और यह आपकी सबसे लंबी उम्र वाली साख को ठीक उसी एक जगह रख देता है जिसकी आप रक्षा नहीं कर सकते: एक ऐसा रनटाइम जो पेज पर पहुँच जाने वाली किसी भी चीज़ को खुशी-खुशी चला देगा। एक सफल इंजेक्शन, आपके बिल्ड में कहीं एक सेंध लगी निर्भरता, और किसी को किसी को फ़िश करने की ज़रूरत ही नहीं पड़ती। वे टोकन पढ़ते हैं और निकल जाते हैं, और यह तब तक काम करता रहता है जब तक इसकी अवधि समाप्त नहीं हो जाती।

backend-for-frontend पैटर्न इस साख को पहुँच से बाहर कर देता है। ब्राउज़र आपके ऐप के एक छोटे सर्वर से बात करता है, और वही सर्वर गोपनीय OAuth क्लाइंट है। वही कोड एक्सचेंज चलाता है, वही एक्सेस और रिफ़्रेश टोकन रखता है, और ब्राउज़र को एक अपारदर्शी कुकी के सिवा कुछ नहीं थमाता। यह इस बात का विवरण है कि हमने अपना कैसे बनाया, और उस एक हिस्से का जो सचमुच दिलचस्प निकला: इसे एक से ज़्यादा टेनेंट की सेवा करने लायक बनाना।

ब्राउज़र के हाथ आख़िर में क्या रहता है

एक कुकी जिसका नाम __Host-agbff है, HttpOnly और SameSite=Lax से चिह्नित, पूरे पाथ पर दायरे वाली, बिना किसी समाप्ति-तिथि के, ताकि यह ब्राउज़र सत्र के साथ ही मर जाए। इसका मान 256 बिट यादृच्छिकता है और इसके सिवा कुछ नहीं। यह कोई टोकन नहीं है, यह किसी चीज़ में डिकोड नहीं होती, और TLS पर किसी __Host- कुकी को तार से चुरा लेना ऐसी बात नहीं जो आप कर सकें।

जो कुछ असली है वह सब सर्वर की ओर बैठता है, एक वितरित कैश में एक सत्र-रिकॉर्ड के भीतर: एक्सेस टोकन, रिफ़्रेश टोकन, id टोकन, एक्सेस टोकन कब समाप्त होगा, और सत्र किस टेनेंट का है। लॉगिन स्वयं PKCE के साथ एक सामान्य ऑथराइज़ेशन कोड फ़्लो है, जिसे एक गोपनीय क्लाइंट चलाता है जो अपने सीक्रेट से टोकन एंडपॉइंट पर स्वयं को प्रमाणित करता है। वापसी के रास्ते पर id टोकन को ठीक से सत्यापित किया जाता है: जारीकर्ता, ऑडियंस, प्रकाशित कुंजियों के विरुद्ध हस्ताक्षर, आयु, और फिर रीडायरेक्ट से पहले छिपाकर रखे गए मान के विरुद्ध nonce की स्थिर-समय तुलना। इन सब के बाद ही कोई सत्र अस्तित्व में आता है और कोई कुकी सेट होती है।

एक हेडर, जिसका बस अस्तित्व जाँचा जाता है, और कुछ नहीं

ब्राउज़र BFF को अपनी ही उपयोगकर्ता जानकारी के लिए और प्रॉक्सी किए गए API तक पहुँचने के लिए कॉल करता है, और वे कॉल एक कस्टम हेडर x-authagonal-bff साथ लाती हैं। इसका मान अप्रासंगिक है। बस अस्तित्व ही पूरी जाँच है।

यह आलस जैसा दिखता है पर है नहीं। क्रॉस-साइट रिक्वेस्ट फ़ोर्जरी का पूरा वर्ग एक फ़ॉर्म पोस्ट, एक इमेज टैग, या ऐसे किसी नेविगेशन पर टिका है जिसे कोई दूसरा ऑरिजिन तब चला सकता है जब आपकी कुकी साथ सवार हो, और इनमें से कोई भी कस्टम हेडर सेट नहीं कर सकता। जिस क्षण हमला करती JavaScript एक जोड़ने की कोशिश करती है, वह सामान्य अनुरोध होना बंद कर देता है और एक प्रीफ़्लाइटेड अनुरोध बन जाता है, जिसे आपकी CORS नीति ठुकरा देती है। हेडर कोई अनुमान लगाने योग्य रहस्य नहीं है, यह इस बात का प्रमाण है कि अनुरोध मार्कअप से नहीं, कोड से आया है।

यह स्क्रिप्ट से की गई कॉल्स पर अपेक्षित है और उन पर जानबूझकर नहीं जो स्वभाव से ही टॉप-लेवल नेविगेशन हैं: कोई लॉगिन शुरू करना, आइडेंटिटी प्रोवाइडर से वापस लौटना, कोई लॉगआउट लिंक फ़ॉलो करना। किसी ब्राउज़र नेविगेशन पर कस्टम हेडर माँगना तो बस नेविगेशन को ही तोड़ देगा।

रिफ़्रेश, ठीक एक बार

हर प्रॉक्सी किया गया अनुरोध यह पा सकता है कि एक्सेस टोकन समाप्त होने ही वाला है, और एक व्यस्त पेज एक साथ कई अनुरोध करता है। इन सबको भोलेपन से रिफ़्रेश करें तो एक छोटी आफ़त मिलती है: एक घूमते रिफ़्रेश टोकन के साथ कई समवर्ती रिफ़्रेश, हर एक दूसरे को अमान्य करता हुआ, और अंत में उपयोगकर्ता अपने ही ट्रैफ़िक से लॉग आउट हो जाता है।

इसलिए रिफ़्रेश सिंगल-फ़्लाइट है, प्रति सत्र। पार होने वाला पहला अनुरोध एक ऐसा गेट ले लेता है जिसकी कुंजी सत्र आईडी है, और बाक़ी सब प्रतीक्षा करते हैं। सूक्ष्म हिस्सा यह है कि प्रतीक्षक अंदर पहुँचने पर क्या करते हैं: कुछ भी तय करने से पहले वे स्टोर से सत्र दोबारा पढ़ते हैं, क्योंकि जिस अनुरोध ने पहले गेट पकड़ा था उसने बहुत संभवतः पहले ही रिफ़्रेश कर दिया है, और क़तार में लगते समय वे जो मान थामे हुए थे वह बासी हो चुका है। एक बार रिफ़्रेश करो, फिर सब उसी नतीजे का उपयोग करते हैं।

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

एक BFF, कई टेनेंट

एक सिंगल-टेनेंट BFF शुरुआत में ही कॉन्फ़िगरेशन से सब कुछ हल कर लेता है: एक अथॉरिटी, एक क्लाइंट आईडी, एक सीक्रेट। एक ही डिप्लॉयमेंट से कई टेनेंट को सेवा देना इसे तोड़ देता है, क्योंकि अब BFF को यह पता लगाना होता है कि कोई लॉगिन किस जारीकर्ता का है, और उसे यह किसी भी सत्र के अस्तित्व में आने से पहले करना होता है।

चुनाव लॉगिन एंडपॉइंट पर होता है, एक ऐसे क्वेरी पैरामीटर के ज़रिए जिसका नाम आप रख सकते हैं, तो यह ?slug= हो सकता है या ?org= या जो भी आपका उत्पाद पहले से ग्राहक को कहता है। एक रिज़ॉल्वर उस कुंजी को एक टेनेंट कॉन्फ़िग में बदल देता है: अथॉरिटी, क्लाइंट आईडी, सीक्रेट, स्कोप। अगर यह किसी चीज़ में हल नहीं होती, तो लॉगिन वहीं और तभी ठुकरा दिया जाता है।

फिर उस कुंजी को एक ऐसे आइडेंटिटी प्रोवाइडर तक आने-जाने के सफ़र में बचे रहना होता है जो इसके बारे में कुछ नहीं जानता, और फिर लौटना होता है। इसे थामने के लिए अभी कोई सत्र है नहीं, इसलिए यह करिलेशन कुकी में सवार होकर चलती है, वही अल्पजीवी एन्क्रिप्टेड कुकी जो ठीक इसी वजह से पहले से PKCE वेरिफ़ायर, स्टेट और nonce ढोती है। पंद्रह मिनट, हर लॉगिन प्रयास के लिए एक। लौटते समय कुकी डिक्रिप्ट की जाती है, उसके भीतर की कुंजी से टेनेंट को दोबारा हल किया जाता है, और कोड को उस टेनेंट के टोकन एंडपॉइंट के विरुद्ध एक्सचेंज किया जाता है। तभी टेनेंट कुंजी सत्र में लिखी जाती है, जहाँ यह टिकाऊ जवाब बन जाती है।

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

वह अनुरोध जो बिना किसी कुकी के आता है

बैक-चैनल लॉगआउट वह जगह है जहाँ यह डिज़ाइन अपनी क़ीमत वसूल करता है। जब कोई सत्र कहीं और समाप्त होता है, तो आइडेंटिटी प्रोवाइडर सीधे आपके सर्वर पर एक लॉगआउट टोकन POST कर देता है। कोई कुकी नहीं। कोई सत्र नहीं। कोई ब्राउज़र बिलकुल शामिल नहीं। एक सिंगल-टेनेंट BFF में यह कोई ख़ास बात नहीं, क्योंकि सिर्फ़ एक ही जारीकर्ता है जिससे यह आ सकता है। एक मल्टी-टेनेंट में आपको "यह किस टेनेंट के लिए है?" का जवाब देना पड़ता है, इससे पहले कि आप "यह असली भी है या नहीं?" का जवाब दे सकें, और उपलब्ध एकमात्र सामग्री स्वयं टोकन है, जिसे आपने सत्यापित नहीं किया है और इसलिए जिस पर भरोसा नहीं कर सकते।

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

उसके बाद वे जाँचें आती हैं जो किसी लॉगआउट टोकन को लॉगआउट टोकन बनाती हैं: इसमें nonce नहीं होना चाहिए, क्योंकि यह किसी को प्रमाणित नहीं कर रहा; इसमें बैक-चैनल लॉगआउट इवेंट होना चाहिए; और इसे या तो किसी विशिष्ट सत्र की या किसी सब्जेक्ट की पहचान बतानी चाहिए। एक सत्र आईडी एक सत्र को ख़त्म करती है। एक सब्जेक्ट उस उपयोगकर्ता के हर सत्र को ख़त्म कर देता है, और यही वह रूप है जो हम भेजते हैं, क्योंकि "मुझे हर जगह से लॉग आउट कर दो" का यही मतलब लोग सचमुच रखते हैं जब वे हर जगह से लॉग आउट पर क्लिक करते हैं। इसीलिए सत्र-स्टोर सत्र आईडी के हिसाब से एक इंडेक्स रखता है और सब्जेक्ट के हिसाब से एक और, न कि सिर्फ़ कुकी मान के हिसाब से।

सिंगल-टेनेंट उबाऊ ही रहता है

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

चुनाव को भरोसे से अलग रखिए

जब आप किसी चीज़ को मल्टी-टेनेंट बनाते हैं, तो करने लायक अभ्यास यह है कि हर प्रवेश-बिंदु को गिनें और पूछें कि उनमें से हर एक पर टेनेंट की पहचान क्या बताती है। ब्राउज़र नेविगेशन एक ऐसी कुकी लाते हैं जिसे आपने जारी किया और नियंत्रित करते हैं। रीडायरेक्ट कॉलबैक ऐसी स्टेट लाते हैं जिस पर आपने हस्ताक्षर किया। सर्वर-से-सर्वर कॉलबैक सिर्फ़ वही लाते हैं जो कॉल करने वाले ने भेजना चुना, और कॉल करने वाला झूठ बोल रहा हो सकता है।

वह असहज प्रवेश-बिंदु, जिस पर कुछ भी भरोसेमंद नहीं होता, वही वह है जिसका डिज़ाइन तय करता है कि पूरा तंत्र ठोस है या नहीं, और यही आम तौर पर वह है जिसके बारे में कोई देर तक सोचता ही नहीं। जिस पैटर्न ने हमें इससे पार निकाला वह सामान्य रूप से लागू होता है: चुनाव को भरोसे से अलग रखिए। अविश्वसनीय डेटा पर रूट करना ठीक है, बशर्ते रूटिंग सिर्फ़ यह चुनती हो कि किस भरोसे के लंगर को आख़िरी फ़ैसला लेने की अनुमति है, और आख़िरी फ़ैसला किसी ऐसी चीज़ से लिया जाए जिसे हमलावर जाली नहीं बना सकता।

अगर आप यह सब खुद लिखना नहीं चाहेंगे, तो Authagonal .NET के लिए और Node के लिए BFF देता है, और पोर्टल उस गोपनीय क्लाइंट को, रीडायरेक्ट URI, बैक-चैनल लॉगआउट एंडपॉइंट और सब कुछ के साथ, एक क्लिक में प्रावधानित कर देगा।