वेब2 प्रमाणीकरण के लिए एथेरियम का उपयोग करना
परिचय
SAML (opens in a new tab) वेब2 पर इस्तेमाल किया जाने वाला एक मानक है जो एक पहचान प्रदाता (IdP) (opens in a new tab) को सेवा प्रदाताओं (SP) (opens in a new tab) के लिए यूज़र जानकारी प्रदान करने की अनुमति देता है।
इस ट्यूटोरियल में आप सीखेंगे कि एथेरियम हस्ताक्षरों को SAML के साथ कैसे एकीकृत किया जाए ताकि यूज़र अपने एथेरियम वॉलेट का उपयोग उन वेब2 सेवाओं में खुद को प्रमाणित करने के लिए कर सकें जो अभी तक एथेरियम को मूल रूप से समर्थन नहीं करती हैं।
ध्यान दें कि यह ट्यूटोरियल दो अलग-अलग दर्शकों के लिए लिखा गया है:
- एथेरियम से जुड़े लोग जो एथेरियम को समझते हैं और जिन्हें SAML सीखने की ज़रूरत है
- वेब2 से जुड़े लोग जो SAML और वेब2 प्रमाणीकरण को समझते हैं और जिन्हें एथेरियम सीखने की ज़रूरत है
परिणामस्वरूप, इसमें बहुत सारी परिचयात्मक सामग्री होने वाली है जिसे आप पहले से जानते हैं। इसे छोड़ने के लिए स्वतंत्र महसूस करें।
एथेरियम लोगों के लिए SAML
SAML एक केंद्रीकृत प्रोटोकॉल है। एक सेवा प्रदाता (SP) किसी पहचान प्रदाता (IdP) से केवल तभी दावे (जैसे "यह मेरा यूज़र जॉन है, उसके पास A, B, और C करने की अनुमति होनी चाहिए") स्वीकार करता है, जब उसका या तो उसके साथ या उस प्रमाणपत्र प्राधिकारी (opens in a new tab) के साथ पहले से मौजूद विश्वास संबंध हो, जिसने उस IdP के प्रमाणपत्र पर हस्ताक्षर किए हों।
उदाहरण के लिए, SP एक ट्रैवल एजेंसी हो सकती है जो कंपनियों को यात्रा सेवाएं प्रदान करती है, और IdP एक कंपनी की आंतरिक वेब साइट हो सकती है। जब कर्मचारियों को व्यावसायिक यात्रा बुक करने की आवश्यकता होती है, तो ट्रैवल एजेंसी उन्हें वास्तव में यात्रा बुक करने देने से पहले प्रमाणीकरण के लिए कंपनी के पास भेजती है।
यह वह तरीका है जिससे तीन संस्थाएँ, ब्राउज़र, SP, और IdP, पहुँच के लिए बातचीत करती हैं। SP को ब्राउज़र का उपयोग करने वाले यूज़र के बारे में पहले से कुछ भी जानने की आवश्यकता नहीं है, बस IdP पर भरोसा करना है।
SAML लोगों के लिए एथेरियम
एथेरियम एक विकेंद्रीकृत प्रणाली है।
यूज़र्स के पास एक निजी चाबी होती है (आमतौर पर एक ब्राउज़र एक्सटेंशन में रखी जाती है)। निजी चाबी से आप एक सार्वजनिक कुंजी प्राप्त कर सकते हैं, और उससे एक 20-बाइट का पता। जब यूज़र्स को किसी सिस्टम में लॉग इन करने की आवश्यकता होती है, तो उन्हें एक नॉन्स (एकल-उपयोग मान) के साथ एक संदेश पर हस्ताक्षर करने के लिए अनुरोध किया जाता है। सर्वर यह सत्यापित कर सकता है कि हस्ताक्षर उस पते द्वारा बनाया गया था।
हस्ताक्षर केवल एथेरियम पते की पुष्टि करता है। अन्य यूज़र विशेषताओं को प्राप्त करने के लिए, आप आमतौर पर साक्षियों (opens in a new tab) का उपयोग करते हैं। एक साक्षी में आमतौर पर ये फ़ील्ड होती हैं:
- साक्षीकर्ता, वह पता जिसने साक्षी बनाई
- प्राप्तकर्ता, वह पता जिस पर साक्षी लागू होती है
- डेटा, प्रमाणित किया जा रहा डेटा, जैसे नाम, अनुमतियाँ, आदि।
- स्कीमा, डेटा की व्याख्या के लिए उपयोग किए गए स्कीमा की ID।
एथेरियम की विकेन्द्रीकृत प्रकृति के कारण, कोई भी यूज़र साक्षी बना सकता है। साक्षीकर्ता की पहचान यह पहचानने के लिए महत्वपूर्ण है कि हम किन साक्षियों को विश्वसनीय मानते हैं।
सेटअप
पहला कदम एक SAML SP और एक SAML IdP का आपस में संचार करना है।
-
सॉफ़्टवेयर डाउनलोड करें। इस लेख के लिए नमूना सॉफ़्टवेयर github पर (opens in a new tab) है। विभिन्न चरण विभिन्न शाखाओं में संग्रहीत हैं, इस चरण के लिए आप
saml-onlyचाहते हैं1git clone https://github.com/qbzzt/250420-saml-ethereum -b saml-only2cd 250420-saml-ethereum3pnpm install -
स्व-हस्ताक्षरित प्रमाणपत्रों के साथ कुंजियाँ बनाएँ। इसका मतलब है कि कुंजी इसका अपना प्रमाणपत्र प्राधिकारी है, और इसे सेवा प्रदाता में मैन्युअल रूप से आयात करने की आवश्यकता है। अधिक जानकारी के लिए OpenSSL दस्तावेज़ (opens in a new tab) देखें।
1mkdir keys2cd keys3openssl req -new -x509 -days 365 -nodes -sha256 -out saml-sp.crt -keyout saml-sp.pem -subj /CN=sp/4openssl req -new -x509 -days 365 -nodes -sha256 -out saml-idp.crt -keyout saml-idp.pem -subj /CN=idp/5cd .. -
सर्वर प्रारंभ करें (SP और IdP दोनों)
1pnpm start -
URL http://localhost:3000/ (opens in a new tab) पर SP पर ब्राउज़ करें और IdP (पोर्ट 3001) पर रीडायरेक्ट होने के लिए बटन पर क्लिक करें।
-
IdP को अपना ईमेल पता प्रदान करें और सेवा प्रदाता में लॉगिन करें पर क्लिक करें। देखें कि आपको सेवा प्रदाता (पोर्ट 3000) पर वापस रीडायरेक्ट कर दिया गया है और यह आपको आपके ईमेल पते से जानता है।
विस्तृत व्याख्या
यह चरण-दर-चरण होता है:
src/config.mts
इस फ़ाइल में पहचान प्रदाता और सेवा प्रदाता दोनों के लिए कॉन्फ़िगरेशन शामिल है। आम तौर पर ये दोनों अलग-अलग संस्थाएँ होंगी, लेकिन यहाँ हम सरलता के लिए कोड साझा कर सकते हैं।
1const fs = await import("fs")23const protocol="http"अभी के लिए हम सिर्फ़ परीक्षण कर रहे हैं, इसलिए HTTP का उपयोग करना ठीक है।
1export const spCert = fs.readFileSync("keys/saml-sp.crt").toString()2export const idpCert = fs.readFileSync("keys/saml-idp.crt").toString()सार्वजनिक कुंजियों को पढ़ें, जो आम तौर पर दोनों घटकों के लिए उपलब्ध होती हैं (और या तो सीधे विश्वसनीय होती हैं, या एक विश्वसनीय प्रमाणपत्र प्राधिकारी द्वारा हस्ताक्षरित होती हैं)।
1export const spPort = 30002export const spHostname = "localhost"3export const spDir = "sp"45export const idpPort = 30016export const idpHostname = "localhost"7export const idpDir = "idp"89export const spUrl = `${protocol}://${spHostname}:${spPort}/${spDir}`10export const idpUrl = `${protocol}://${idpHostname}:${idpPort}/${idpDir}`सभी दिखाएँदोनों घटकों के लिए URL।
1export const spPublicData = {सेवा प्रदाता के लिए सार्वजनिक डेटा।
1 entityID: `${spUrl}/metadata`,परंपरा के अनुसार, SAML में entityID वह URL है जहाँ इकाई का मेटाडेटा उपलब्ध होता है। यह मेटाडेटा यहाँ सार्वजनिक डेटा से मेल खाता है, सिवाय इसके कि यह XML रूप में है।
1 wantAssertionsSigned: true,2 authnRequestsSigned: false,3 signingCert: spCert,4 allowCreate: true,5 assertionConsumerService: [{6 Binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',7 Location: `${spUrl}/assertion`,8 }]9 }सभी दिखाएँहमारे उद्देश्यों के लिए सबसे महत्वपूर्ण परिभाषा assertionConsumerServer है। इसका मतलब है कि सेवा प्रदाता को कुछ दावा करने के लिए (उदाहरण के लिए, "यह जानकारी भेजने वाला यूज़र somebody@example.com (opens email client) है") हमें URL http://localhost:3000/sp/assertion पर HTTP POST (opens in a new tab) का उपयोग करने की आवश्यकता है।
1export const idpPublicData = {2 entityID: `${idpUrl}/metadata`,3 signingCert: idpCert,4 wantAuthnRequestsSigned: false,5 singleSignOnService: [{6 Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",7 Location: `${idpUrl}/login`8 }],9 singleLogoutService: [{10 Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",11 Location: `${idpUrl}/logout`12 }],13 }सभी दिखाएँपहचान प्रदाता के लिए सार्वजनिक डेटा समान है। यह निर्दिष्ट करता है कि किसी यूज़र को लॉग इन करने के लिए आप http://localhost:3001/idp/login पर पोस्ट करते हैं और किसी यूज़र को लॉग आउट करने के लिए आप http://localhost:3001/idp/logout पर पोस्ट करते हैं।
src/sp.mts
यह वह कोड है जो एक सेवा प्रदाता को लागू करता है।
1import * as config from "./config.mts"2const fs = await import("fs")3const saml = await import("samlify")हम SAML को लागू करने के लिए samlify (opens in a new tab) लाइब्रेरी का उपयोग करते हैं।
1import * as validator from "@authenio/samlify-node-xmllint"2saml.setSchemaValidator(validator)samlify लाइब्रेरी यह उम्मीद करती है कि एक पैकेज यह सत्यापित करेगा कि XML सही है, अपेक्षित सार्वजनिक कुंजी के साथ हस्ताक्षरित है, आदि। हम इस उद्देश्य के लिए @authenio/samlify-node-xmllint (opens in a new tab) का उपयोग करते हैं।
1const express = (await import("express")).default2const spRouter = express.Router()3const app = express()एक express (opens in a new tab) Router (opens in a new tab) एक "मिनी वेब साइट" है जिसे एक वेब साइट के अंदर माउंट किया जा सकता है। इस मामले में, हम इसका उपयोग सभी सेवा प्रदाता परिभाषाओं को एक साथ समूहित करने के लिए करते हैं।
1const spPrivateKey = fs.readFileSync("keys/saml-sp.pem").toString()23const sp = saml.ServiceProvider({4 privateKey: spPrivateKey, 5 ...config.spPublicData6})सेवा प्रदाता का स्वयं का प्रतिनिधित्व सभी सार्वजनिक डेटा है, और वह निजी चाबी है जिसका उपयोग वह जानकारी पर हस्ताक्षर करने के लिए करता है।
1const idp = saml.IdentityProvider(config.idpPublicData);सार्वजनिक डेटा में वह सब कुछ होता है जो सेवा प्रदाता को पहचान प्रदाता के बारे में जानने की आवश्यकता होती है।
1spRouter.get(`/metadata`, 2 (req, res) => res.header("Content-Type", "text/xml").send(sp.getMetadata())3)अन्य SAML घटकों के साथ अंतर-संचालनीयता को सक्षम करने के लिए, सेवा और पहचान प्रदाताओं के पास /metadata में XML प्रारूप में उनका सार्वजनिक डेटा (मेटाडेटा कहा जाता है) उपलब्ध होना चाहिए।
1spRouter.post(`/assertion`,यह वह पृष्ठ है जिस पर ब्राउज़र अपनी पहचान बनाने के लिए पहुँचता है। दावे में यूज़र पहचानकर्ता (यहाँ हम ईमेल पते का उपयोग करते हैं) शामिल है, और इसमें अतिरिक्त विशेषताएँ शामिल हो सकती हैं। यह ऊपर के अनुक्रम आरेख में चरण 7 के लिए हैंडलर है।
1 async (req, res) => {2 // console.log(`SAML response:\n${Buffer.from(req.body.SAMLResponse, 'base64').toString('utf-8')}`)आप दावे में प्रदान किए गए XML डेटा को देखने के लिए टिप्पणी की गई कमांड का उपयोग कर सकते हैं। यह base64 एन्कोडेड (opens in a new tab) है।
1 try {2 const loginResponse = await sp.parseLoginResponse(idp, 'post', req);पहचान सर्वर से लॉगिन अनुरोध को पार्स करें।
1 res.send(`2 <html>3 <body>4 <h2>Hello ${loginResponse.extract.nameID}</h2>5 </body>6 </html>7 `)8 res.send();एक HTML प्रतिक्रिया भेजें, बस यूज़र को यह दिखाने के लिए कि हमें लॉगिन मिल गया है।
1 } catch (err) {2 console.error('Error processing SAML response:', err);3 res.status(400).send('SAML authentication failed');4 }5 }6)विफलता की स्थिति में यूज़र को सूचित करें।
1spRouter.get('/login',जब ब्राउज़र इस पृष्ठ को प्राप्त करने का प्रयास करता है तो एक लॉगिन अनुरोध बनाएँ। यह ऊपर के अनुक्रम आरेख में चरण 1 के लिए हैंडलर है।
1 async (req, res) => {2 const loginRequest = await sp.createLoginRequest(idp, "post")लॉगिन अनुरोध पोस्ट करने के लिए जानकारी प्राप्त करें।
1 res.send(`2 <html>3 <body>4 <script>5 window.onload = function () { document.forms[0].submit(); } 6 </script>यह पृष्ठ फ़ॉर्म (नीचे देखें) को स्वचालित रूप से सबमिट करता है। इस तरह यूज़र को रीडायरेक्ट होने के लिए कुछ भी करने की ज़रूरत नहीं है। यह ऊपर के अनुक्रम आरेख में चरण 2 है।
1 <form method="post" action="${loginRequest.entityEndpoint}">loginRequest.entityEndpoint (पहचान प्रदाता एंडपॉइंट का URL) पर पोस्ट करें।
1 <input type="hidden" name="${loginRequest.type}" value="${loginRequest.context}" />इनपुट नाम loginRequest.type (SAMLRequest) है। उस फ़ील्ड के लिए सामग्री loginRequest.context है, जो फिर से XML है जो base64 एन्कोडेड है।
1 </form>2 </body>3 </html>4 `) 5 }6)78app.use(express.urlencoded({extended: true}))यह मिडलवेयर (opens in a new tab) HTTP अनुरोध (opens in a new tab) के मुख्य भाग को पढ़ता है। डिफ़ॉल्ट रूप से एक्सप्रेस इसे अनदेखा करता है, क्योंकि अधिकांश अनुरोधों के लिए इसकी आवश्यकता नहीं होती है। हमें इसकी आवश्यकता है क्योंकि POST मुख्य भाग का उपयोग करता है।
1app.use(`/${config.spDir}`, spRouter)सेवा प्रदाता निर्देशिका (/sp) में राउटर को माउंट करें।
1app.get("/", (req, res) => {2 res.send(`3 <html>4 <body>5 <button onClick="document.location.href='${config.spUrl}/login'">6 Click here to log on7 </button>8 </body>9 </html>10 `)11})सभी दिखाएँयदि कोई ब्राउज़र रूट निर्देशिका प्राप्त करने का प्रयास करता है, तो उसे लॉगिन पृष्ठ का एक लिंक प्रदान करें।
1app.listen(config.spPort, () => {2 console.log(`service provider is running on http://${config.spHostname}:${config.spPort}`)3})इस एक्सप्रेस एप्लिकेशन के साथ spPort को सुनें।
src/idp.mts
यह पहचान प्रदाता है। यह सेवा प्रदाता के बहुत समान है, नीचे दी गई व्याख्याएँ उन हिस्सों के लिए हैं जो अलग हैं।
1const xmlParser = new (await import("fast-xml-parser")).XMLParser(2 {3 ignoreAttributes: false, // Preserve attributes4 attributeNamePrefix: "@_", // Prefix for attributes5 }6)हमें सेवा प्रदाता से प्राप्त XML अनुरोध को पढ़ने और समझने की आवश्यकता है।
1const getLoginPage = requestId => `यह फ़ंक्शन स्वचालित रूप से सबमिट किए गए फ़ॉर्म के साथ पृष्ठ बनाता है जो ऊपर अनुक्रम आरेख के चरण 4 में लौटाया जाता है।
1<html>2 <head>3 <title>लॉगिन पेज</title>4 </head>5 <body>6 <h2>लॉगिन पेज</h2>7 <form method="post" action="./loginSubmitted">8 <input type="hidden" name="requestId" value="${requestId}" />9 ईमेल पता: <input name="email" />10 <br />11 <button type="Submit">12 सेवा प्रदाता पर लॉगिन करें13 </button>सभी दिखाएँहम सेवा प्रदाता को दो फ़ील्ड भेजते हैं:
requestIdजिसका हम जवाब दे रहे हैं।- यूज़र पहचानकर्ता (हम अभी के लिए यूज़र द्वारा प्रदान किए गए ईमेल पते का उपयोग करते हैं)।
1 </form>2 </body>3</html>45const idpRouter = express.Router()67idpRouter.post("/loginSubmitted", async (req, res) => {8 const loginResponse = await idp.createLoginResponse(यह ऊपर के अनुक्रम आरेख में चरण 5 के लिए हैंडलर है। idp.createLoginResponse (opens in a new tab) लॉगिन प्रतिक्रिया बनाता है।
1 sp, 2 {3 authnContextClassRef: 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport',4 audience: sp.entityID,दर्शक सेवा प्रदाता है।
1 extract: {2 request: {3 id: req.body.requestId4 }5 },अनुरोध से निकाली गई जानकारी। अनुरोध में जिस एक पैरामीटर की हमें परवाह है, वह है requestId, जो सेवा प्रदाता को अनुरोधों और उनकी प्रतिक्रियाओं का मिलान करने देता है।
1 signingKey: { privateKey: idpPrivateKey, publicKey: config.idpCert } // Ensure signingहमें प्रतिक्रिया पर हस्ताक्षर करने के लिए डेटा रखने के लिए signingKey की आवश्यकता है। सेवा प्रदाता अहस्ताक्षरित अनुरोधों पर भरोसा नहीं करता है।
1 },2 "post",3 {4 email: req.body.emailयह यूज़र जानकारी वाला फ़ील्ड है जिसे हम सेवा प्रदाता को वापस भेजते हैं।
1 }2 );34 res.send(`5 <html>6 <body>7 <script>8 window.onload = function () { document.forms[0].submit(); }9 </script>10 11 <form method="post" action="${loginResponse.entityEndpoint}">12 <input type="hidden" name="${loginResponse.type}" value="${loginResponse.context}" />13 </form>14 </body>15 </html>16 `)17})सभी दिखाएँफिर से, एक ऑटो-सबमिट किए गए फ़ॉर्म का उपयोग करें। यह ऊपर के अनुक्रम आरेख में चरण 6 है।
12// IdP endpoint for login requests3idpRouter.post(`/login`,यह वह एंडपॉइंट है जो सेवा प्रदाता से लॉगिन अनुरोध प्राप्त करता है। यह ऊपर के अनुक्रम आरेख के चरण 3 के लिए हैंडलर है।
1 async (req, res) => {2 try {3 // Workaround because I couldn't get parseLoginRequest to work.4 // const loginRequest = await idp.parseLoginRequest(sp, 'post', req)5 const samlRequest = xmlParser.parse(Buffer.from(req.body.SAMLRequest, 'base64').toString('utf-8'))6 res.send(getLoginPage(samlRequest["samlp:AuthnRequest"]["@_ID"]))हमें प्रमाणीकरण अनुरोध की ID पढ़ने के लिए idp.parseLoginRequest (opens in a new tab) का उपयोग करने में सक्षम होना चाहिए। हालाँकि, मैं इसे काम नहीं करवा सका और इस पर बहुत समय खर्च करना उचित नहीं था इसलिए मैं बस एक सामान्य-उद्देश्य XML पार्सर (opens in a new tab) का उपयोग करता हूँ। हमें जिस जानकारी की आवश्यकता है, वह <samlp:AuthnRequest> टैग के अंदर ID विशेषता है, जो XML के शीर्ष स्तर पर है।
एथेरियम हस्ताक्षरों का उपयोग करना
अब जब हम सेवा प्रदाता को यूज़र की पहचान भेज सकते हैं, तो अगला कदम एक विश्वसनीय तरीके से यूज़र की पहचान प्राप्त करना है। वीएम हमें वॉलेट से यूज़र के पते के लिए पूछने की अनुमति देता है, लेकिन इसका मतलब ब्राउज़र से जानकारी मांगना है। हम ब्राउज़र को नियंत्रित नहीं करते हैं, इसलिए हम इससे मिलने वाली प्रतिक्रिया पर स्वचालित रूप से भरोसा नहीं कर सकते हैं।
इसके बजाय, IdP ब्राउज़र को हस्ताक्षर करने के लिए एक स्ट्रिंग भेजेगा। यदि ब्राउज़र में वॉलेट इस स्ट्रिंग पर हस्ताक्षर करता है, तो इसका मतलब है कि यह वास्तव में वह पता है (यानी, यह उस पते से मेल खाने वाली निजी चाबी जानता है)।
इसे क्रिया में देखने के लिए, मौजूदा IdP और SP को रोकें और इन कमांड को चलाएँ:
1git checkout eth-signatures2pnpm install3pnpm startफिर SP (opens in a new tab) पर ब्राउज़ करें और निर्देशों का पालन करें।
ध्यान दें कि इस समय हम यह नहीं जानते हैं कि एथेरियम पते से ईमेल पता कैसे प्राप्त करें, इसलिए इसके बजाय हम SP को <ethereum address>@bad.email.address रिपोर्ट करते हैं।
विस्तृत व्याख्या
परिवर्तन पिछले आरेख में चरण 4-5 में हैं।
हमने जिस एकमात्र फ़ाइल को बदला है वह idp.mts है। यहाँ बदले हुए भाग हैं।
1import { v4 as uuidv4 } from 'uuid'2import { verifyMessage } from 'viem'हमें इन दो अतिरिक्त पुस्तकालयों की आवश्यकता है। हम नॉन्स (opens in a new tab) मान बनाने के लिए uuid (opens in a new tab) का उपयोग करते हैं। मान स्वयं मायने नहीं रखता है, बस यह तथ्य है कि इसका उपयोग केवल एक बार किया जाता है।
viem (opens in a new tab) लाइब्रेरी हमें एथेरियम परिभाषाओं का उपयोग करने देती है। यहाँ हमें यह सत्यापित करने की आवश्यकता है कि हस्ताक्षर वास्तव में मान्य है।
1const loginPrompt = "सेवा प्रदाता तक पहुँचने के लिए, इस नॉन्स पर हस्ताक्षर करें: "वॉलेट यूज़र से संदेश पर हस्ताक्षर करने की अनुमति मांगता है। एक संदेश जो केवल एक नॉन्स है, यूज़र्स को भ्रमित कर सकता है, इसलिए हम इस प्रॉम्प्ट को शामिल करते हैं।
1// Keep requestIDs here2let nonces = {}हमें इसका जवाब देने में सक्षम होने के लिए अनुरोध जानकारी की आवश्यकता है। हम इसे अनुरोध (चरण 4) के साथ भेज सकते हैं, और इसे वापस प्राप्त कर सकते हैं (चरण 5)। हालाँकि, हम ब्राउज़र से मिलने वाली जानकारी पर भरोसा नहीं कर सकते, जो संभावित रूप से शत्रुतापूर्ण यूज़र के नियंत्रण में है। तो इसे यहाँ संग्रहीत करना बेहतर है, कुंजी के रूप में नॉन्स के साथ।
ध्यान दें कि हम इसे यहाँ सरलता के लिए एक चर के रूप में कर रहे हैं। हालाँकि, इसके कई नुकसान हैं:
- हम सेवा से इनकार के हमले के प्रति संवेदनशील हैं। एक दुर्भावनापूर्ण यूज़र कई बार लॉग ऑन करने का प्रयास कर सकता है, जिससे हमारी मेमोरी भर जाएगी।
- यदि IdP प्रक्रिया को पुनरारंभ करने की आवश्यकता है, तो हम मौजूदा मान खो देते हैं।
- हम कई प्रक्रियाओं में लोड को संतुलित नहीं कर सकते हैं, क्योंकि प्रत्येक का अपना चर होगा।
एक उत्पादन प्रणाली पर हम एक डेटाबेस का उपयोग करेंगे और किसी प्रकार की समाप्ति तंत्र लागू करेंगे।
1const getSignaturePage = requestId => {2 const nonce = uuidv4()3 nonces[nonce] = requestIdएक नॉन्स बनाएँ, और भविष्य में उपयोग के लिए requestId को संग्रहीत करें।
1 return `2<html>3 <head>4 <script type="module">यह जावास्क्रिप्ट पृष्ठ लोड होने पर स्वचालित रूप से निष्पादित हो जाता है।
1 import { createWalletClient, custom, getAddress } from 'https://esm.sh/viem'हमें viem से कई फ़ंक्शन की आवश्यकता है।
1 if (!window.ethereum) {2 alert("कृपया MetaMask या एक संगत वॉलेट स्थापित करें और फिर पुनः लोड करें")3 }हम केवल तभी काम कर सकते हैं जब ब्राउज़र पर कोई वॉलेट हो।
1 const [account] = await window.ethereum.request({method: 'eth_requestAccounts'})वॉलेट (window.ethereum) से खातों की सूची का अनुरोध करें। मान लें कि कम से कम एक है, और केवल पहले वाले को संग्रहीत करें।
1 const walletClient = createWalletClient({2 account,3 transport: custom(window.ethereum)4 })ब्राउज़र वॉलेट के साथ इंटरैक्ट करने के लिए एक वॉलेट क्लाइंट (opens in a new tab) बनाएँ।
1 window.goodSignature = () => {2 walletClient.signMessage({3 message: "${loginPrompt}${nonce}"यूज़र से एक संदेश पर हस्ताक्षर करने के लिए कहें। क्योंकि यह पूरा HTML एक टेम्पलेट स्ट्रिंग (opens in a new tab) में है, हम idp प्रक्रिया में परिभाषित चर का उपयोग कर सकते हैं। यह अनुक्रम आरेख में चरण 4.5 है।
1 }).then(signature => {2 const path= "/${config.idpDir}/signature/${nonce}/" + account + "/" + signature3 window.location.href = path4 })5 }/idp/signature/<nonce>/<address>/<signature> पर रीडायरेक्ट करें। यह अनुक्रम आरेख में चरण 5 है।
1 window.badSignature = () => {2 const path= "/${config.idpDir}/signature/${nonce}/" + 3 getAddress("0x" + "BAD060A7".padEnd(40, "0")) + 4 "/0x" + "BAD0516".padStart(130, "0")5 window.location.href = path6 }हस्ताक्षर ब्राउज़र द्वारा वापस भेजा जाता है, जो संभावित रूप से दुर्भावनापूर्ण है (ब्राउज़र में http://localhost:3001/idp/signature/bad-nonce/bad-address/bad-signature खोलने से आपको कोई नहीं रोक सकता)। इसलिए, यह सत्यापित करना महत्वपूर्ण है कि IdP प्रक्रिया खराब हस्ताक्षरों को सही ढंग से संभालती है।
1 </script>2 </head>3 <body>4 <h2>कृपया हस्ताक्षर करें</h2>5 <button onClick="window.goodSignature()">6 एक अच्छा (मान्य) हस्ताक्षर सबमिट करें7 </button>8 <br/>9 <button onClick="window.badSignature()">10 एक खराब (अमान्य) हस्ताक्षर सबमिट करें11 </button>12 </body>13</html> 14`15}सभी दिखाएँबाकी सब मानक HTML है।
1idpRouter.get("/signature/:nonce/:account/:signature", async (req, res) => {यह अनुक्रम आरेख में चरण 5 के लिए हैंडलर है।
1 const requestId = nonces[req.params.nonce]2 if (requestId === undefined) {3 res.send("Bad nonce")4 return ;5 } 6 7 nonces[req.params.nonce] = undefinedअनुरोध ID प्राप्त करें, और यह सुनिश्चित करने के लिए कि इसका पुन: उपयोग नहीं किया जा सकता है, nonces से नॉन्स को हटा दें।
1 try {क्योंकि हस्ताक्षर के अमान्य होने के इतने सारे तरीके हैं, हम इसे एक try ... में लपेटते हैं। catch ब्लॉक किसी भी फेंकी गई त्रुटियों को पकड़ने के लिए।
1 const validSignature = await verifyMessage({2 address: req.params.account,3 message: `${loginPrompt}${req.params.nonce}`,4 signature: req.params.signature5 })अनुक्रम आरेख में चरण 5.5 को लागू करने के लिए verifyMessage (opens in a new tab) का उपयोग करें।
1 if (!validSignature)2 throw("Bad signature")3 } catch (err) {4 res.send("Error:" + err)5 return ;6 }हैंडलर का बाकी हिस्सा /loginSubmitted हैंडलर में पहले किए गए काम के बराबर है, सिवाय एक छोटे से बदलाव के।
1 const loginResponse = await idp.createLoginResponse(2 .3 .4 .5 {6 email: req.params.account + "@bad.email.address"7 }8 );हमारे पास वास्तविक ईमेल पता नहीं है (हम इसे अगले अनुभाग में प्राप्त करेंगे), इसलिए अभी के लिए हम एथेरियम पता लौटाते हैं और इसे स्पष्ट रूप से एक ईमेल पते के रूप में चिह्नित नहीं करते हैं।
1// IdP endpoint for login requests2idpRouter.post(`/login`,3 async (req, res) => {4 try {5 // Workaround because I couldn't get parseLoginRequest to work.6 // const loginRequest = await idp.parseLoginRequest(sp, 'post', req)7 const samlRequest = xmlParser.parse(Buffer.from(req.body.SAMLRequest, 'base64').toString('utf-8'))8 res.send(getSignaturePage(samlRequest["samlp:AuthnRequest"]["@_ID"]))9 } catch (err) {10 console.error('Error processing SAML response:', err);11 res.status(400).send('SAML authentication failed');12 }13 }14)सभी दिखाएँgetLoginPage के बजाय, अब चरण 3 हैंडलर में getSignaturePage का उपयोग करें।
ईमेल पता प्राप्त करना
अगला कदम ईमेल पता प्राप्त करना है, जो सेवा प्रदाता द्वारा अनुरोधित पहचानकर्ता है। ऐसा करने के लिए, हम एथेरियम साक्षी सेवा (EAS) (opens in a new tab) का उपयोग करते हैं।
साक्षियाँ प्राप्त करने का सबसे आसान तरीका ग्राफक्यूएल API (opens in a new tab) का उपयोग करना है। हम इस क्वेरी का उपयोग करते हैं:
1query GetAttestationsByRecipient {2 attestations(3 where: { 4 recipient: { equals: "${getAddress(ethAddr)}" }5 schemaId: { equals: "0xfa2eff59a916e3cc3246f9aec5e0ca00874ae9d09e4678e5016006f07622f977" }6 }7 take: 18 ) { 9 data10 id11 attester12 }13}सभी दिखाएँइस schemaId (opens in a new tab) में केवल एक ई-मेल पता शामिल है। यह क्वेरी इस स्कीमा की साक्षियों के लिए पूछती है। साक्षी का विषय recipient कहलाता है। यह हमेशा एक एथेरियम पता होता है।
चेतावनी: जिस तरह से हम यहाँ साक्षियाँ प्राप्त कर रहे हैं, उसमें दो सुरक्षा मुद्दे हैं।
-
हम API एंडपॉइंट,
https://optimism.easscan.org/graphqlपर जा रहे हैं, जो एक केंद्रीकृत घटक है। हमidविशेषता प्राप्त कर सकते हैं और फिर यह सत्यापित करने के लिए ऑन-चेन लुकअप कर सकते हैं कि एक साक्षी वास्तविक है, लेकिन API एंडपॉइंट अभी भी हमें उनके बारे में न बताकर साक्षियों को सेंसर कर सकता है।इस समस्या को हल करना असंभव नहीं है, हम अपना स्वयं का ग्राफक्यूएल एंडपॉइंट चला सकते हैं और चेन लॉग से साक्षियाँ प्राप्त कर सकते हैं, लेकिन यह हमारे उद्देश्यों के लिए अत्यधिक है।
-
हम साक्षीकर्ता की पहचान को नहीं देखते हैं। कोई भी हमें झूठी जानकारी दे सकता है। एक वास्तविक दुनिया के कार्यान्वयन में हमारे पास विश्वसनीय साक्षीकर्ताओं का एक सेट होगा और केवल उनकी साक्षियों को देखेंगे।
इसे क्रिया में देखने के लिए, मौजूदा IdP और SP को रोकें और इन कमांड को चलाएँ:
1git checkout email-address2pnpm install3pnpm startफिर अपना ई-मेल पता प्रदान करें। ऐसा करने के आपके पास दो तरीके हैं:
-
एक निजी चाबी का उपयोग करके एक वॉलेट आयात करें, और परीक्षण निजी चाबी
0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80का उपयोग करें। -
अपने स्वयं के ई-मेल पते के लिए एक साक्षी जोड़ें:
-
साक्षी एक्सप्लोरर में स्कीमा (opens in a new tab) पर ब्राउज़ करें।
-
स्कीमा के साथ साक्षी दें पर क्लिक करें।
-
प्राप्तकर्ता के रूप में अपना एथेरियम पता, ईमेल पते के रूप में अपना ई-मेल पता दर्ज करें, और ऑनचेन चुनें। फिर साक्षी बनाएँ पर क्लिक करें।
-
अपने वॉलेट में लेनदेन को स्वीकृत करें। गैस का भुगतान करने के लिए आपको ऑप्टिमिज्म ब्लॉकचेन (opens in a new tab) पर कुछ ETH की आवश्यकता होगी।
-
किसी भी तरह, ऐसा करने के बाद http://localhost:3000 (opens in a new tab) पर ब्राउज़ करें और निर्देशों का पालन करें। यदि आपने परीक्षण निजी चाबी आयात की है, तो आपको मिलने वाला ई-मेल test_addr_0@example.com है। यदि आपने अपने स्वयं के पते का उपयोग किया है, तो यह वह होना चाहिए जो आपने प्रमाणित किया है।
विस्तृत व्याख्या
नए चरण ग्राफक्यूएल संचार हैं, चरण 5.6 और 5.7।
फिर से, यहाँ idp.mts के बदले हुए भाग हैं।
1import { GraphQLClient } from 'graphql-request'2import { SchemaEncoder } from '@ethereum-attestation-service/eas-sdk'हमें जिन पुस्तकालयों की आवश्यकता है, उन्हें आयात करें।
1const graphqlEndpointUrl = "https://optimism.easscan.org/graphql"प्रत्येक ब्लॉकचेन के लिए एक अलग एंडपॉइंट (opens in a new tab) है।
1const graphqlClient = new GraphQLClient(graphqlEndpointUrl, { fetch })एक नया GraphQLClient क्लाइंट बनाएँ जिसका उपयोग हम एंडपॉइंट की क्वेरी के लिए कर सकते हैं।
1const graphqlSchema = 'string emailAddress'2const graphqlEncoder = new SchemaEncoder(graphqlSchema)ग्राफक्यूएल हमें केवल बाइट्स के साथ एक अपारदर्शी डेटा ऑब्जेक्ट देता है। इसे समझने के लिए हमें स्कीमा की आवश्यकता है।
1const ethereumAddressToEmail = async ethAddr => {एक एथेरियम पते से एक ई-मेल पते पर जाने के लिए एक फ़ंक्शन।
1 const query = `2 query GetAttestationsByRecipient {यह एक ग्राफक्यूएल क्वेरी है।
1 attestations(हम साक्षियों की तलाश कर रहे हैं।
1 where: { 2 recipient: { equals: "${getAddress(ethAddr)}" }3 schemaId: { equals: "0xfa2eff59a916e3cc3246f9aec5e0ca00874ae9d09e4678e5016006f07622f977" }4 }हम जो साक्षियाँ चाहते हैं, वे हमारे स्कीमा में हैं, जहाँ प्राप्तकर्ता getAddress(ethAddr) है। getAddress (opens in a new tab) फ़ंक्शन यह सुनिश्चित करता है कि हमारे पते में सही चेकसम (opens in a new tab) है। यह आवश्यक है क्योंकि ग्राफक्यूएल केस-महत्वपूर्ण है। "0xBAD060A7", "0xBad060A7", और "0xbad060a7" अलग-अलग मान हैं।
1 take: 1चाहे हमें कितनी भी साक्षियाँ मिलें, हम केवल पहली वाली चाहते हैं।
1 ) {2 data3 id4 attester5 }6 }`वे फ़ील्ड जिन्हें हम प्राप्त करना चाहते हैं।
attester: वह पता जिसने साक्षी सबमिट की। आम तौर पर इसका उपयोग यह तय करने के लिए किया जाता है कि साक्षी पर भरोसा किया जाए या नहीं।id: साक्षी ID। आप इस मान का उपयोग साक्षी को ऑन-चेन पढ़ने (opens in a new tab) के लिए कर सकते हैं ताकि यह सत्यापित हो सके कि ग्राफक्यूएल क्वेरी से जानकारी सही है।data: स्कीमा डेटा (इस मामले में, ई-मेल पता)।
1 const queryResult = await graphqlClient.request(query)23 if (queryResult.attestations.length == 0)4 return "no_address@available.is"यदि कोई साक्षी नहीं है, तो एक मान लौटाएँ जो स्पष्ट रूप से गलत है, लेकिन जो सेवा प्रदाता के लिए मान्य दिखाई देगा।
1 const attestationDataFields = graphqlEncoder.decodeData(queryResult.attestations[0].data)2 return attestationDataFields[0].value.value3}यदि कोई मान है, तो डेटा को डिकोड करने के लिए decodeData का उपयोग करें। हमें इसके द्वारा प्रदान किए गए मेटाडेटा की आवश्यकता नहीं है, बस मान ही चाहिए।
1 const loginResponse = await idp.createLoginResponse(2 sp, 3 {4 .5 .6 .7 },8 "post",9 {10 email: await ethereumAddressToEmail(req.params.account)11 }12 );सभी दिखाएँई-मेल पता प्राप्त करने के लिए नए फ़ंक्शन का उपयोग करें।
विकेंद्रीकरण के बारे में क्या?
इस कॉन्फ़िगरेशन में यूज़र वह व्यक्ति होने का दिखावा नहीं कर सकते जो वे नहीं हैं, जब तक कि हम एथेरियम से ई-मेल पता मैपिंग के लिए भरोसेमंद साक्षीकर्ताओं पर भरोसा करते हैं। हालाँकि, हमारा पहचान प्रदाता अभी भी एक केंद्रीकृत घटक है। जिसके पास भी पहचान प्रदाता की निजी चाबी है, वह सेवा प्रदाता को झूठी जानकारी भेज सकता है।
मल्टी-पार्टी कंप्यूटेशन (MPC) (opens in a new tab) का उपयोग करके एक समाधान हो सकता है। मुझे उम्मीद है कि मैं भविष्य के ट्यूटोरियल में इसके बारे में लिखूँगा।
निष्कर्ष
एक लॉग ऑन मानक, जैसे कि एथेरियम हस्ताक्षर, को अपनाने में चिकन और अंडे की समस्या का सामना करना पड़ता है। सेवा प्रदाता व्यापक संभव बाजार को आकर्षित करना चाहते हैं। यूज़र अपने लॉग ऑन मानक का समर्थन करने की चिंता किए बिना सेवाओं तक पहुँचने में सक्षम होना चाहते हैं। एडेप्टर बनाना, जैसे कि एथेरियम IdP, हमें इस बाधा को दूर करने में मदद कर सकता है।
मेरे और काम के लिए यहाँ देखें (opens in a new tab)।
पेज का अंतिम अपडेट: 23 नवंबर 2025





