मुझे कई स्वतंत्र वेबसाइटों को सामान्य ब्राउज़र आइसोलेशन कमजोर किए बिना एक ही अकाउंट सिस्टम की तरह काम कराना था। उपयोगकर्ता एक डोमेन पर साइन इन कर सके, बाद में किसी दूसरे डोमेन को सीधे खोले, लॉग इन पर क्लिक करे और दोबारा क्रेडेंशियल डाले बिना वहीं आगे बढ़ सके।
महत्वपूर्ण बाधा यह थी कि ये स्वतंत्र रूट डोमेन थे। एक रूट डोमेन द्वारा बनाई गई सेशन कुकी को किसी असंबंधित रूट डोमेन तक बस स्कोप नहीं किया जा सकता। जब मैंने दो अक्सर मिला दिए जाने वाले विचारों को अलग किया, तो डिजाइन को समझना बहुत आसान हो गया: ग्लोबल आइडेंटिटी और लोकल ब्राउज़र सेशन।
नतीजे का मॉडल सीधा है: बैकएंड एक साझा अकाउंट आइडेंटिटी को पहचानता है, हर डोमेन अपना होस्ट-बाउंड सेशन रखता है, और कम समय के लिए मान्य एक वन-टाइम हैंडऑफ किसी ऑथेंटिकेटेड भरोसेमंद ओरिजिन को दूसरे भरोसेमंद ओरिजिन पर नया सेशन बनाने की अनुमति देने देता है।
ग्लोबल आइडेंटिटी के लिए साझा कुकी जरूरी नहीं है
ब्राउज़र सेशन लोकल हों, फिर भी अकाउंट ग्लोबल हो सकता है।
हर साइट होस्ट-ओनली सेशन कुकी रखती है। एक रूट डोमेन द्वारा जारी कुकी को Domain एट्रिब्यूट के जरिए किसी असंबंधित रूट डोमेन तक नहीं बढ़ाया जा सकता। यह एट्रिब्यूट कुकी का दायरा केवल उसी डोमेन हायरार्की के भीतर बढ़ा सकता है जिसे जारी करने वाले होस्ट को इस्तेमाल करने की अनुमति है।
ऐसे सेशन के लिए जिसे एक ही होस्ट से बंधा रहना चाहिए, __Host- प्रीफिक्स वाली कुकी अच्छी तरह फिट बैठती है। अवधारणात्मक रूप से:
Set-Cookie: __Host-<session-cookie>=<opaque-token>; Path=/; Secure; HttpOnly; SameSite=Laxमहत्वपूर्ण गुण ये सीमाएं हैं:
- साइट A, साइट B की होस्ट-ओनली सेशन कुकी नहीं पढ़ सकती।
- साइट B, साइट A की होस्ट-ओनली सेशन कुकी नहीं पढ़ सकती।
- फिर भी दोनों सेशन उसी बैकएंड आइडेंटिटी पर रिज़ॉल्व हो सकते हैं।
इसलिए क्रॉस-डोमेन SSO को कुकी को एक साइट से दूसरी साइट तक ले जाने की कोशिश नहीं करनी चाहिए। उसे एक अस्थायी प्रमाण ट्रांसफर करना चाहिए कि भरोसेमंद सोर्स ने उपयोगकर्ता को पहले ही ऑथेंटिकेट कर लिया है।
वन-टाइम हैंडऑफ पैटर्न
ऑथेंटिकेटेड सोर्स एक छोटा हैंडऑफ ऑपरेशन उपलब्ध कराता है। ब्राउज़र इसे केवल तभी कॉल करता है जब क्रॉस-साइट ऑथेंटिकेशन ट्रांज़िशन की जरूरत हो।
बैकएंड क्रिप्टोग्राफिक रूप से रैंडम टिकट बनाता है, रॉ वैल्यू ब्राउज़र को केवल एक बार लौटाता है, और ट्रांसफर को वैलिडेट करने के लिए जरूरी न्यूनतम जानकारी के साथ केवल उसका हैश स्टोर करता है:
{
tokenHash,
userId,
sourceOrigin,
targetSite,
returnPath,
expiresAt
}टिकट कम समय के लिए मान्य होना चाहिए और केवल एक टार्गेट के लिए वैध होना चाहिए। सोर्स-साइड इश्यूअन्स रिक्वेस्ट पहले से ऑथेंटिकेटेड और CSRF से सुरक्षित होनी चाहिए। कुछ भी जारी करने से पहले मांगे गए टार्गेट और रिटर्न डेस्टिनेशन को वैलिडेट करना चाहिए।
इसके बाद ब्राउज़र टार्गेट को टॉप-लेवल POST करता है। टार्गेट टिकट को कंज्यूम करता है, साझा यूज़र आइडेंटिटी रिज़ॉल्व करता है, अपना लोकल सेशन बनाता है और ब्राउज़र को इच्छित पाथ पर रीडायरेक्ट कर देता है।
अवधारणात्मक रूप से:
record = consumeOnce({
tokenHash: hash(ticket),
targetSite: currentSite,
sourceOrigin: requestOrigin,
notExpired: true
})
if (!record) deny()
user = loadSharedUser(record.userId)
createLocalSession(user)
redirect(record.returnPath)टिकट टार्गेट सेशन नहीं है। वह टार्गेट सेशन बनाने के लिए वन-टाइम ऑथराइज़ेशन है।
एटॉमिक कंजम्प्शन रीप्ले विंडो को बंद करता है
कमजोर इम्प्लीमेंटेशन टिकट को लोड करता, वैलिडेट करता, सेशन बनाता और उसके बाद टिकट डिलीट करता। इससे एक विंडो बनती है जिसमें दो रिक्वेस्ट्स एक ही अभी भी वैध रिकॉर्ड को देख सकती हैं।
ज्यादा सुरक्षित डिजाइन वैलिडेशन और इनवैलिडेशन को एक ही एटॉमिक कंज्यूम ऑपरेशन में जोड़ता है। कोई रिकॉर्ड मैच न करे तो ऑथेंटिकेशन विफल हो जाता है। अगर रिकॉर्ड लौटता है, तो सेशन बनने से पहले वह टिकट उपयोग योग्य सेट से हट चुका होता है।
इससे टार्गेट एक ही ऑपरेशन में कई शर्तें भी बांध सकता है: अपेक्षित टार्गेट, अपेक्षित सोर्स ओरिजिन, टोकन हैश और एक्सपायरी।
ओरिजिन वैलिडेशन को फेल-क्लोज़्ड होना चाहिए
ओरिजिन बाइंडिंग एक और उपयोगी गुण जोड़ता है: किसी भरोसेमंद सोर्स के जरिए जारी टिकट को किसी असंबंधित ओरिजिन से कंज्यूम नहीं किया जा सकना चाहिए।
ओरिजिन जाँच को कंज्यूम करने की शर्त का हिस्सा ही होना चाहिए। इसका मतलब है कि गलत ओरिजिन से आया रिक्वेस्ट वैध टिकट को कंज्यूम नहीं करता, जबकि वैध सोर्स बाद में हैंडऑफ पूरा कर सकता है।
एक महत्वपूर्ण सावधानी है: हर ब्राउज़र कॉन्टेक्स्ट में Origin हेडर में सामान्य ओरिजिन स्ट्रिंग होने की गारंटी नहीं है। कुछ रिक्वेस्ट्स उपयोग योग्य वैल्यू के बिना आ सकती हैं, और ओपेक कॉन्टेक्स्ट्स Origin: null पैदा कर सकते हैं।
मेरा नियम है कि अगर एप्लिकेशन में कोई स्पष्ट वैकल्पिक वैलिडेशन मेकैनिज़्म नहीं है, तो अनुपस्थित या ओपेक Origin को विफल ओरिजिन जाँच माना जाए। लिटरल वैल्यू null को केवल कम्पैटिबिलिटी के लिए विश्वसनीय ओरिजिन नहीं बनना चाहिए।
यह फेल-क्लोज़्ड विकल्प है। अगर वैध फ्लो को ऐसे कॉन्टेक्स्ट का सपोर्ट करना है जहां उपयोग योग्य ओरिजिन उपलब्ध नहीं है, तो सामान्य जाँच को चुपचाप कमजोर करने के बजाय उस अपवाद को स्पष्ट रूप से डिज़ाइन करना चाहिए।
हैंडऑफ क्रेडेंशियल को URL से बाहर रखें
मैं अस्थायी टिकट को क्वेरी स्ट्रिंग में डालने के बजाय टॉप-लेवल फॉर्म POST से भेजना पसंद करता हूं।
एक सामान्यीकृत क्लाइंट-साइड वर्ज़न यह है:
const form = document.createElement('form');
form.method = 'POST';
form.action = targetOrigin + '/auth/consume';
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'ticket';
input.value = ticket;
form.append(input);
document.body.append(form);
form.submit();टार्गेट टिकट को अपेक्षित POST बॉडी से स्वीकार करता है और URL के जरिए वही वैल्यू देने की कोशिशों को अस्वीकार करता है।
इससे क्रेडेंशियल हानिरहित नहीं हो जाता, लेकिन वह सामान्य नेविगेशन URL से बाहर रहता है और URL हिस्ट्री, कॉपी किए गए लिंक्स, रेफरर्स तथा URL-केंद्रित लॉगिंग के जरिए अनजाने एक्सपोज़र कम होता है।
ऑथेंटिकेटेड क्रॉस-साइट लिंक्स खुद को अपग्रेड कर सकते हैं
अगर मौजूदा साइट पहले से जानती है कि यूज़र ऑथेंटिकेटेड है, तो किसी दूसरी विश्वसनीय साइट का सामान्य लिंक अवसर मिलने पर हैंडऑफ कर सकता है।
लिंक में फिर भी असली href होना चाहिए। SSO सामान्य नेविगेशन का सुधार है, उसका विकल्प नहीं।
सरल किया गया क्लाइंट फ्लो ऐसा दिखता है:
if (session.status !== 'authenticated') {
return; // ordinary browser navigation
}
if (!isTrustedTarget(destination)) {
return;
}
event.preventDefault();
const ticket = await requestHandoff({
targetSite,
returnPath: destinationPath
});
postTicketToTarget(ticket);प्रोडक्शन वर्ज़न को डुप्लिकेट सबमिशन्स से भी बचाना चाहिए, टार्गेट को विश्वसनीय रजिस्ट्री के खिलाफ वैलिडेट करना चाहिए, और हैंडऑफ न बन पाए तो सामान्य नेविगेशन पर लौटना चाहिए।
ब्राउज़र का नेटिव व्यवहार नेटिव ही रहना चाहिए। मिडिल-क्लिक, मॉडिफ़ायर-की क्लिक्स, डाउनलोड्स और दूसरे ब्राउज़िंग कॉन्टेक्स्ट के लिए बने लिंक्स को चुपचाप ऑथेंटिकेशन रिक्वेस्ट्स में नहीं बदलना चाहिए।
दूसरी साइट के लॉग इन बटन को ज्ञात जारीकर्ता चाहिए
कठिन मामला तब शुरू होता है जब यूज़र साइट B को सीधे खोलता है। साइट B के पास लोकल सेशन नहीं है, इसलिए ब्राउज़र में साइट A का वैध सेशन अभी भी मौजूद हो, तब भी वह विज़िटर को सही तौर पर अनाम मानती है।
साइट B, साइट A की होस्ट-ओनली कुकी जाँच नहीं कर सकती। वह किसी मनमानी साइट C पर रीडायरेक्ट करके यह भी उम्मीद नहीं कर सकती कि वह साइट बाकी सिस्टम से जुड़े सेशन्स खोज लेगी। हर असंबंधित रूट डोमेन केवल अपनी कुकीज़ देख सकता है।
इसका मतलब है कि अनाम लॉग इन फ्लो को ज्ञात जारीकर्ता चाहिए: या तो कैनॉनिकल ऑथेंटिकेशन ओरिजिन, या खास तौर पर चुनी गई विश्वसनीय साइट जिसके पास पुनः उपयोग योग्य लॉगिन सेशन रहने की उम्मीद हो।
सरल साइट A → साइट B मामले में क्रम यह है:
- यूज़र साइट A पर साइन इन करता है।
- बाद में यूज़र साइट B को सीधे खोलता है।
- साइट B के पास सेशन नहीं है, इसलिए वह लॉग इन दिखाती है।
- यूज़र लॉग इन पर क्लिक करता है।
- ब्राउज़र चुना हुआ जारीकर्ता, यानी साइट A, पर टॉप-लेवल नेविगेशन करता है।
- साइट A अपनी मौजूदा होस्ट-ओनली सेशन कुकी पढ़ सकती है।
- क्योंकि वह सेशन अभी भी वैध है, साइट A तुरंत साइट B के लिए वन-टाइम हैंडऑफ जारी करती है।
- ब्राउज़र टिकट को साइट B पर POST करता है।
- साइट B अपना लोकल सेशन बनाती है और यूज़र को मांगे गए पेज पर वापस भेज देती है।
यूज़र की नजर से यह साइट A पर जाकर वापस आने वाला तेज रीडायरेक्ट है। पासवर्ड की जरूरत नहीं पड़ती, क्योंकि साइट A के पास पहले से वह सेशन है जिसे देखने की उसे अनुमति है।
सीमा भी उतनी ही महत्वपूर्ण है: अगर यूज़र केवल साइट C पर लॉग्ड इन है, तो साइट B को साइट A पर रीडायरेक्ट करने से मदद नहीं मिलती, जब तक साइट A एक असली केंद्रीय ऑथेंटिकेशन ब्रोकर न हो जिसे लॉगिन स्टेट की अपनी स्वतंत्र जानकारी हो। साझा यूज़र डेटाबेस से ब्राउज़र सेशन्स साझा नहीं हो जाते।
कैनॉनिकल जारीकर्ता सिस्टम को सरल क्यों बनाता है
जैसे-जैसे स्वतंत्र साइट्स की संख्या बढ़ती है, कैनॉनिकल जारीकर्ता मनमाने डोमेन्स में सेशन डिस्कवरी की जरूरत खत्म कर देता है।
इसके बिना अनाम साइट B को किसी तरह तय करना पड़ता कि किस दूसरी साइट पर इस समय उपयोगी कुकी हो सकती है। साइट A, साइट C की कुकी जाँच नहीं कर सकती, साइट C, साइट A की कुकी जाँच नहीं कर सकती, और साइट B दोनों में से किसी की भी नहीं।
कैनॉनिकल जारीकर्ता हर अनाम लॉगिन प्रयास को एक पूर्वानुमेय पाथ देता है:
Site B
→ authentication issuer
→ issuer session exists?
yes → create one-time handoff
no → show normal login
→ Site B consumes handoff
→ Site B creates local sessionजारीकर्ता को बाकी एप्लिकेशन सर्व करने की जरूरत नहीं है। उसकी संबंधित जिम्मेदारी पुनः उपयोग योग्य ऑथेंटिकेशन सेशन को बनाए रखना और विश्वसनीय साइट्स के लिए कड़े तौर पर स्कोप्ड हैंडऑफ्स जारी करना है।
छोटे सिस्टम में कोई मौजूदा साइट यह भूमिका निभा सकती है। बड़े आर्किटेक्चर में समर्पित ऑथेंटिकेशन ओरिजिन ट्रस्ट मॉडल को समझना आसान बना सकता है।
हर विज़िटर को जारीकर्ता के रास्ते मत घुमाइए
जारीकर्ता फ्लो को हर अनाम पेज व्यू पर अपने-आप नहीं चलना चाहिए।
सिर्फ यह जांचने के लिए कि मौजूदा सेशन है या नहीं, हर विज़िटर को दूसरे डोमेन पर रीडायरेक्ट करना अनावश्यक नेविगेशन, अतिरिक्त लेटेंसी, ज्यादा विफलता मोड्स, शोर-भरा एनालिटिक्स और अधिक जटिल कैशिंग व्यवहार पैदा करता है।
यह सार्वजनिक, SEO-संवेदनशील पेजों के लिए भी खराब फिट है। किसी सार्वजनिक URL का रिक्वेस्ट करने वाले क्रॉलर या सामान्य विज़िटर को पहले ऑथेंटिकेशन डिटूर से गुजरने पर मजबूर करने के बजाय वही सार्वजनिक पेज सीधे मिलना चाहिए।
मैं यह नियम इस्तेमाल करता हूं:
पेज लोड पर जारीकर्ता को प्रोब मत करें। क्रॉस-डोमेन ऑथेंटिकेशन केवल स्पष्ट लॉग इन एक्शन या जानबूझकर किए गए ऑथेंटिकेटेड क्रॉस-साइट नेविगेशन के बाद शुरू करें।
इससे सार्वजनिक ट्रैफ़िक सरल रहता है और जिस यूज़र के पास चुने हुए जारीकर्ता पर पहले से वैध सेशन है उसके लिए ऑथेंटिकेशन फिर भी तेज रहता है।
रिटर्न-पाथ वैलिडेशन को सिर्फ प्रीफिक्स जाँच से ज्यादा सावधानी चाहिए
हैंडऑफ को आम तौर पर याद रखना होता है कि ब्राउज़र को बाद में कहां पहुंचना चाहिए। अगर इस वैल्यू को मनमाना क्लाइंट-नियंत्रित URL माना जाए, तो यह ओपन रीडायरेक्ट बन सकता है।
जहां संभव हो, सबसे सुरक्षित डिजाइन है कि पूरा डेस्टिनेशन URL स्वीकार ही न किया जाए। ज्ञात डेस्टिनेशन्स के लिए छोटा आइडेंटिफ़ायर या सर्वर-साइड मैपिंग जरूरी URL पार्सिंग और वैलिडेशन की मात्रा घटाता है।
अगर रिलेटिव पाथ स्वीकार करना जरूरी हो, तो उसे सावधानी से वैलिडेट करें और टार्गेट साइट तक सीमित रखें।
जानबूझकर सरल किया गया उदाहरण—पूरा रीडायरेक्ट-वैलिडेशन रूटीन नहीं—यह है:
function isSafeReturnPath(value) {
if (!value.startsWith('/')) return false;
if (value.startsWith('//')) return false;
if (value.includes('\\\\')) return false;
const base = 'https://example.invalid';
const parsed = new URL(value, base);
return parsed.origin === base;
}वास्तविक इम्प्लीमेंटेशन को डीकोडिंग व्यवहार, खराब इनपुट, कंट्रोल कैरेक्टर्स, नॉर्मलाइज़ेशन, एप्लिकेशन रूटिंग नियम और रीडायरेक्ट बनने से पहले होने वाले प्रॉक्सी या फ्रेमवर्क ट्रांसफॉर्मेशन्स पर भी विचार करना पड़ता है।
सिक्योरिटी इनवैरिएंट पार्सर विवरण से सरल है: हैंडऑफ टार्गेट एप्लिकेशन के भीतर अनुमत लोकेशन चुन सकता है, मनमाना बाहरी डेस्टिनेशन कभी नहीं।
विफलता मामले हैपी पाथ से ज्यादा महत्वपूर्ण हैं
सफल लॉगिन केवल यह साबित करता है कि बेसिक पाथ काम करता है। ज्यादा उपयोगी टेस्ट्स ट्रस्ट सीमाओं को परखते हैं।
मैंने इन जैसे मामलों में व्यवहार वेरिफ़ाई किया:
- अनऑथेंटिकेटेड सोर्स द्वारा हैंडऑफ जारी करने की कोशिश;
- CSRF प्रोटेक्शन का अनुपस्थित होना;
- अनुपस्थित, ओपेक या अनपेक्षित ओरिजिन;
- अविश्वसनीय टार्गेट;
- असुरक्षित रिटर्न डेस्टिनेशन;
- टिकट को गलत टार्गेट पर पेश करना;
- गलत-ओरिजिन कंज्यूम प्रयास;
- टिकट रीप्ले;
- टिकट एक्सपायरी;
- ऐसा यूज़र जो अब मौजूद नहीं है;
- अपेक्षित POST बॉडी के बजाय URL के जरिए टिकट देना।
दो इनवैरिएंट्स को सीधे टेस्ट करना खास तौर पर उपयोगी है: अस्वीकृत रिक्वेस्ट को वैध फ्लो के लिए अभी भी वैध टिकट को गलती से नष्ट नहीं करना चाहिए, और सफल कंज्यूम को उस टिकट को तुरंत अनुपयोगी बना देना चाहिए।
प्रोटोकॉल पेयरवाइज़ इंटीग्रेशन्स के बिना स्केल करता है
और साइट्स जोड़ने पर हैंडऑफ डिज़ाइन मूल रूप से नहीं बदलता।
हर भाग लेने वाली साइट को वही छोटा कॉन्ट्रैक्ट चाहिए:
- स्थिर लॉजिकल आइडेंटिफ़ायर;
- विश्वसनीय सार्वजनिक ओरिजिन;
- साझा अकाउंट आइडेंटिटी तक एक्सेस;
- ऑथेंटिकेटेड सोर्सेज के लिए हैंडऑफ-इश्यूअन्स ऑपरेशन;
- हैंडऑफ-कंज्यूम ऑपरेशन;
- विश्वसनीय-साइट रजिस्ट्री;
- होस्ट-बाउंड लोकल सेशन;
- सुरक्षित रिटर्न-डेस्टिनेशन पॉलिसी;
- और अनाम लॉग इन फ्लोज़ के लिए ज्ञात जारीकर्ता।
स्केल करने का महत्वपूर्ण चुनाव कस्टम पेयरवाइज़ ऑथेंटिकेशन लॉजिक से बचना है। पहले से ऑथेंटिकेटेड साइट किसी दूसरी विश्वसनीय साइट के लिए उसी तरह का हैंडऑफ जारी कर सकती है। अनाम साइट ब्राउज़र को ज्ञात जारीकर्ता पर भेज सकती है।
इस तरह सिस्टम विशेष मामलों के बढ़ते मैट्रिक्स के बजाय एक प्रोटोकॉल पर आधारित रहता है।
लॉगिन पोर्टेबिलिटी, लॉगआउट प्रोपेगेशन से अलग है
स्वतंत्र होस्ट-बाउंड सेशन्स एक और उपयोगी फर्क बनाते हैं: लॉगिन को पोर्टेबल बनाना ग्लोबल लॉगआउट को अपने-आप परिभाषित नहीं करता।
अगर यूज़र साइट A से साइन आउट करता है, तो दूसरी साइट का लोकल सेशन वैध रह सकता है, जब तक बैकएंड उसे जानबूझकर रिवोक न करे। यह सेशन-मैनेजमेंट पॉलिसी है, SSO हैंडऑफ की विफलता नहीं।
सिस्टम लोकल लॉगआउट, ग्लोबल लॉगआउट या स्पष्ट सेशन मैनेजमेंट में से चुन सकता है। इस निर्णय को हैंडऑफ से अलग रखने पर दोनों मेकैनिज़्म्स को समझना आसान रहता है।
मैं जो मॉडल इस्तेमाल करता हूं
जब मैंने समस्या को “डोमेन्स के बीच लॉगिन कुकीज़ शेयर करना” कहना बंद किया, तो आर्किटेक्चर सरल हो गया।
एक ग्लोबल आइडेंटिटी है, हर डोमेन अपना ब्राउज़र सेशन रखता है, और कम समय के लिए वैध वन-टाइम हैंडऑफ किसी ऑथेंटिकेटेड विश्वसनीय ओरिजिन को दूसरा लोकल सेशन बनाने की अनुमति देने देता है।
अनाम साइट पर सीधे आने के लिए एक अतिरिक्त नियम महत्वपूर्ण है: ब्राउज़र को ऐसे जारीकर्ता के पास भेजना चाहिए जो वास्तव में यूज़र का मौजूदा सेशन देख सके। अगर यूज़र ने पहले उस जारीकर्ता पर साइन इन किया था, तो राउंड ट्रिप लगभग दिखाई न देने जितना तेज हो सकता है। अगर जारीकर्ता के पास वैध सेशन नहीं है, तो उसे यह दिखावा करने के बजाय सामान्य ऑथेंटिकेशन पर लौटना चाहिए कि वह किसी दूसरे असंबंधित डोमेन का सेशन देख सकता है।
यह मॉडल ब्राउज़र की डोमेन सीमाएं बनाए रखता है, सार्वजनिक विज़िटर्स के लिए अनावश्यक ऑथेंटिकेशन रीडायरेक्ट्स से बचता है, और फिर भी इच्छित अनुभव देता है: जारीकर्ता पर साइन इन करें, बाद में दूसरी विश्वसनीय साइट खोलें, लॉग इन पर क्लिक करें, और पासवर्ड दोबारा डाले बिना नए लोकल सेशन के साथ वापस आएं।