वेब२ प्रमाणीकरणासाठी इथेरियम वापरणे
परिचय
SAML (नवीन टॅबमध्ये उघडते) हे वेब२ वर वापरले जाणारे एक मानक आहे जे ओळख प्रदाता (identity provider - IdP) (नवीन टॅबमध्ये उघडते) ला [सेवा प्रदात्यांसाठी (service providers - SP)](https://en.wikipedia.org/wiki/Service_provider_(SAML) (नवीन टॅबमध्ये उघडते) वापरकर्त्याची माहिती प्रदान करण्यास अनुमती देते).
या ट्युटोरिअलमध्ये तुम्ही इथेरियम स्वाक्षऱ्या SAML सोबत कशा एकत्रित करायच्या हे शिकाल, जेणेकरून वापरकर्ते त्यांच्या इथेरियम वॉलेटचा वापर करून स्वतःला अशा वेब२ सेवांवर प्रमाणित करू शकतील ज्या अद्याप मूळतः इथेरियमला सपोर्ट करत नाहीत.
लक्षात घ्या की हे ट्युटोरिअल दोन वेगवेगळ्या प्रेक्षकांसाठी लिहिले आहे:
- इथेरियमचे लोक ज्यांना इथेरियम समजते आणि त्यांना SAML शिकण्याची आवश्यकता आहे
- वेब२ चे लोक ज्यांना SAML आणि वेब२ प्रमाणीकरण समजते आणि त्यांना इथेरियम शिकण्याची आवश्यकता आहे
परिणामी, यात बरीच परिचयात्मक माहिती असेल जी तुम्हाला आधीच माहित असू शकते. ती वगळण्यास संकोच करू नका.
इथेरियमच्या लोकांसाठी SAML
SAML हा एक केंद्रित प्रोटोकॉल आहे. सेवा प्रदाता (SP) ओळख प्रदात्याकडून (IdP) केवळ तेव्हाच दृढकथने (assertions) (जसे की "हा माझा वापरकर्ता जॉन आहे, त्याला A, B आणि C करण्याची परवानगी असावी") स्वीकारतो जेव्हा त्याचा त्या IdP सोबत किंवा त्या IdP च्या प्रमाणपत्रावर स्वाक्षरी करणाऱ्या प्रमाणपत्र प्राधिकरणासोबत (certificate authority) (नवीन टॅबमध्ये उघडते) आधीपासूनच विश्वासाचा संबंध असतो.
उदाहरणार्थ, SP ही कंपन्यांना प्रवास सेवा पुरवणारी ट्रॅव्हल एजन्सी असू शकते आणि IdP ही कंपनीची अंतर्गत वेबसाइट असू शकते. जेव्हा कर्मचाऱ्यांना व्यावसायिक प्रवासाचे बुकिंग करायचे असते, तेव्हा ट्रॅव्हल एजन्सी त्यांना प्रत्यक्ष प्रवास बुक करू देण्यापूर्वी कंपनीद्वारे प्रमाणीकरणासाठी पाठवते.
अशा प्रकारे ब्राउझर, SP आणि IdP या तीन संस्था अॅक्सेससाठी वाटाघाटी करतात. SP ला ब्राउझर वापरणाऱ्या वापरकर्त्याबद्दल आगाऊ काहीही माहिती असण्याची आवश्यकता नसते, फक्त IdP वर विश्वास ठेवणे आवश्यक असते.
SAML च्या लोकांसाठी इथेरियम
इथेरियम ही एक विकेंद्रित प्रणाली आहे.
वापरकर्त्यांकडे एक खाजगी की असते (जी सामान्यतः ब्राउझर एक्स्टेंशनमध्ये ठेवली जाते). खाजगी की वरून तुम्ही सार्वजनिक की मिळवू शकता आणि त्यावरून 20-बाइटचा पत्ता मिळवू शकता. जेव्हा वापरकर्त्यांना सिस्टममध्ये लॉग इन करायचे असते, तेव्हा त्यांना नॉन्स (एकदाच वापरले जाणारे मूल्य) असलेल्या संदेशावर स्वाक्षरी करण्याची विनंती केली जाते. सर्व्हर पडताळणी करू शकतो की स्वाक्षरी त्याच पत्त्याद्वारे तयार केली गेली होती.
स्वाक्षरी केवळ इथेरियम पत्त्याची पडताळणी करते. वापरकर्त्याचे इतर गुणधर्म मिळवण्यासाठी, तुम्ही सामान्यतः साक्षांकने (attestations) (नवीन टॅबमध्ये उघडते) वापरता. साक्षांकनामध्ये सामान्यतः ही क्षेत्रे (fields) असतात:
- साक्षांकनकर्ता (Attestor), ज्या पत्त्याने साक्षांकन केले आहे
- प्राप्तकर्ता (Recipient), ज्या पत्त्यावर साक्षांकन लागू होते
- डेटा (Data), ज्या डेटाचे साक्षांकन केले जात आहे, जसे की नाव, परवानग्या इ.
- स्कीमा (Schema), डेटाचा अर्थ लावण्यासाठी वापरल्या जाणाऱ्या स्कीमाचा ID.
इथेरियमच्या विकेंद्रित स्वरूपामुळे, कोणताही वापरकर्ता साक्षांकने करू शकतो. कोणती साक्षांकने आपण विश्वसनीय मानतो हे ओळखण्यासाठी साक्षांकनकर्त्याची ओळख महत्त्वाची असते.
सेटअप
पहिली पायरी म्हणजे SAML SP आणि SAML IdP यांच्यात एकमेकांशी संवाद प्रस्थापित करणे.
-
सॉफ्टवेअर डाउनलोड करा. या लेखासाठी नमुना सॉफ्टवेअर GitHub वर (नवीन टॅबमध्ये उघडते) उपलब्ध आहे. वेगवेगळे टप्पे वेगवेगळ्या शाखांमध्ये (branches) साठवले आहेत, या टप्प्यासाठी तुम्हाला
saml-onlyआवश्यक आहे.git clone https://github.com/qbzzt/250420-saml-ethereum -b saml-only cd 250420-saml-ethereum pnpm install -
सेल्फ-साईन्ड प्रमाणपत्रांसह की तयार करा. याचा अर्थ असा की ही की स्वतःच तिचे प्रमाणपत्र प्राधिकरण आहे आणि ती सेवा प्रदात्याकडे मॅन्युअली इम्पोर्ट करणे आवश्यक आहे. अधिक माहितीसाठी OpenSSL चे दस्तऐवज (नवीन टॅबमध्ये उघडते) पहा.
mkdir keys cd keys openssl req -new -x509 -days 365 -nodes -sha256 -out saml-sp.crt -keyout saml-sp.pem -subj /CN=sp/ openssl req -new -x509 -days 365 -nodes -sha256 -out saml-idp.crt -keyout saml-idp.pem -subj /CN=idp/ cd .. -
सर्व्हर सुरू करा (SP आणि IdP दोन्ही)
pnpm start -
http://localhost:3000/ (नवीन टॅबमध्ये उघडते) या URL वर SP ब्राउझ करा आणि IdP (पोर्ट 3001) वर पुनर्निर्देशित (redirect) होण्यासाठी बटणावर क्लिक करा.
-
IdP ला तुमचा ईमेल पत्ता द्या आणि Login to the service provider वर क्लिक करा. पहा की तुम्हाला परत सेवा प्रदात्याकडे (पोर्ट 3000) पुनर्निर्देशित केले जाते आणि तो तुम्हाला तुमच्या ईमेल पत्त्यावरून ओळखतो.
सविस्तर स्पष्टीकरण
हे टप्प्याटप्प्याने कसे घडते ते येथे दिले आहे:
src/config.mts
या फाईलमध्ये ओळख प्रदाता आणि सेवा प्रदाता या दोन्हींसाठी कॉन्फिगरेशन आहे. सामान्यतः या दोन वेगवेगळ्या संस्था असतात, परंतु येथे साधेपणासाठी आपण कोड शेअर करू शकतो.
const fs = await import("fs")
const protocol="http"
सध्या आपण फक्त चाचणी करत आहोत, त्यामुळे HTTP वापरणे ठीक आहे.
export const spCert = fs.readFileSync("keys/saml-sp.crt").toString()
export const idpCert = fs.readFileSync("keys/saml-idp.crt").toString()
सार्वजनिक की वाचा, ज्या सामान्यतः दोन्ही घटकांसाठी उपलब्ध असतात (आणि एकतर थेट विश्वासार्ह असतात किंवा विश्वासार्ह प्रमाणपत्र प्राधिकरणाद्वारे स्वाक्षरी केलेल्या असतात).
export const spPort = 3000
export const spHostname = "localhost"
export const spDir = "sp"
export const idpPort = 3001
export const idpHostname = "localhost"
export const idpDir = "idp"
export const spUrl = `${protocol}://${spHostname}:${spPort}/${spDir}`
export const idpUrl = `${protocol}://${idpHostname}:${idpPort}/${idpDir}`
दोन्ही घटकांसाठी URL.
export const spPublicData = {
सेवा प्रदात्यासाठी सार्वजनिक डेटा.
entityID: `${spUrl}/metadata`,
प्रथेनुसार, SAML मध्ये entityID ही अशी URL असते जिथे संस्थेचा मेटाडेटा उपलब्ध असतो. हा मेटाडेटा येथील सार्वजनिक डेटाशी संबंधित असतो, फक्त तो XML स्वरूपात असतो.
wantAssertionsSigned: true,
authnRequestsSigned: false,
signingCert: spCert,
allowCreate: true,
assertionConsumerService: [{
Binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
Location: `${spUrl}/assertion`,
}]
}
आपल्या उद्देशांसाठी सर्वात महत्त्वाची व्याख्या assertionConsumerServer ही आहे. याचा अर्थ असा की सेवा प्रदात्याला काहीतरी दृढकथन करण्यासाठी (उदाहरणार्थ, "जो वापरकर्ता तुम्हाला ही माहिती पाठवत आहे तो somebody@example.com (ईमेल क्लायंट उघडते) आहे") आपल्याला http://localhost:3000/sp/assertion या URL वर HTTP POST (नवीन टॅबमध्ये उघडते) वापरण्याची आवश्यकता आहे.
export const idpPublicData = {
entityID: `${idpUrl}/metadata`,
signingCert: idpCert,
wantAuthnRequestsSigned: false,
singleSignOnService: [{
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
Location: `${idpUrl}/login`
}],
singleLogoutService: [{
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
Location: `${idpUrl}/logout`
}],
}
ओळख प्रदात्याचा सार्वजनिक डेटाही असाच असतो. हे निर्दिष्ट करते की वापरकर्त्याला लॉग इन करण्यासाठी तुम्ही http://localhost:3001/idp/login वर POST करता आणि वापरकर्त्याला लॉग आउट करण्यासाठी तुम्ही http://localhost:3001/idp/logout वर POST करता.
src/sp.mts
हा तो कोड आहे जो सेवा प्रदात्याची अंमलबजावणी करतो.
import * as config from "./config.mts"
const fs = await import("fs")
const saml = await import("samlify")
SAML ची अंमलबजावणी करण्यासाठी आपण samlify (नवीन टॅबमध्ये उघडते) लायब्ररी वापरतो.
import * as validator from "@authenio/samlify-node-xmllint"
saml.setSchemaValidator(validator)
samlify लायब्ररीला अशी अपेक्षा असते की XML योग्य आहे, अपेक्षित सार्वजनिक की ने स्वाक्षरी केलेले आहे इत्यादी प्रमाणित करण्यासाठी एक पॅकेज असावे. या उद्देशासाठी आपण @authenio/samlify-node-xmllint (नवीन टॅबमध्ये उघडते) वापरतो.
const express = (await import("express")).default
const spRouter = express.Router()
const app = express()
express (नवीन टॅबमध्ये उघडते) Router (नवीन टॅबमध्ये उघडते) ही एक "मिनी वेब साइट" आहे जी वेब साइटच्या आत माउंट केली जाऊ शकते. या प्रकरणात, आपण सेवा प्रदात्याच्या सर्व व्याख्या एकत्र करण्यासाठी त्याचा वापर करतो.
const spPrivateKey = fs.readFileSync("keys/saml-sp.pem").toString()
const sp = saml.ServiceProvider({
privateKey: spPrivateKey,
...config.spPublicData
})
सेवा प्रदात्याचे स्वतःचे सादरीकरण म्हणजे सर्व सार्वजनिक डेटा आणि माहितीवर स्वाक्षरी करण्यासाठी तो वापरत असलेली खाजगी की.
const idp = saml.IdentityProvider(config.idpPublicData);
सार्वजनिक डेटामध्ये सेवा प्रदात्याला ओळख प्रदात्याबद्दल माहित असणे आवश्यक असलेली प्रत्येक गोष्ट असते.
spRouter.get(`/metadata`,
(req, res) => res.header("Content-Type", "text/xml").send(sp.getMetadata())
)
इतर SAML घटकांसोबत आंतरकार्यक्षमता सक्षम करण्यासाठी, सेवा आणि ओळख प्रदात्यांचा सार्वजनिक डेटा (ज्याला मेटाडेटा म्हणतात) /metadata मध्ये XML स्वरूपात उपलब्ध असावा.
spRouter.post(`/assertion`,
स्वतःची ओळख पटवण्यासाठी ब्राउझरद्वारे अॅक्सेस केले जाणारे हे पेज आहे. दृढकथनामध्ये वापरकर्ता आयडेंटिफायर (येथे आपण ईमेल पत्ता वापरतो) समाविष्ट असतो आणि त्यात अतिरिक्त गुणधर्म समाविष्ट असू शकतात. वरील अनुक्रम आकृतीमधील (sequence diagram) पायरी 7 साठी हा हँडलर आहे.
async (req, res) => {
// console.log(`SAML प्रतिसाद:\n${Buffer.from(req.body.SAMLResponse, 'base64').toString('utf-8')}`)
दृढकथनामध्ये प्रदान केलेला XML डेटा पाहण्यासाठी तुम्ही कमेंट आऊट केलेली कमांड वापरू शकता. तो base64 एन्कोड केलेला (नवीन टॅबमध्ये उघडते) असतो.
try {
const loginResponse = await sp.parseLoginResponse(idp, 'post', req);
ओळख सर्व्हरकडून आलेली लॉगिन विनंती पार्स करा.
res.send(`
<html>
<body>
<h2>Hello ${loginResponse.extract.nameID}</h2>
</body>
</html>
`)
res.send();
वापरकर्त्याला आपल्याला लॉगिन मिळाले आहे हे दाखवण्यासाठी HTML प्रतिसाद पाठवा.
} catch (err) {
console.error('Error processing SAML response:', err);
res.status(400).send('SAML authentication failed');
}
}
)
अयशस्वी झाल्यास वापरकर्त्याला कळवा.
spRouter.get('/login',
जेव्हा ब्राउझर हे पेज मिळवण्याचा प्रयत्न करतो तेव्हा लॉगिन विनंती तयार करा. वरील अनुक्रम आकृतीमधील पायरी 1 साठी हा हँडलर आहे.
async (req, res) => {
const loginRequest = await sp.createLoginRequest(idp, "post")
लॉगिन विनंती पोस्ट करण्यासाठी माहिती मिळवा.
res.send(`
<html>
<body>
<script>
window.onload = function () { document.forms[0].submit(); }
</script>
हे पेज फॉर्म (खाली पहा) आपोआप सबमिट करते. अशा प्रकारे पुनर्निर्देशित होण्यासाठी वापरकर्त्याला काहीही करावे लागत नाही. वरील अनुक्रम आकृतीमधील ही पायरी 2 आहे.
<form method="post" action="${loginRequest.entityEndpoint}">
loginRequest.entityEndpoint वर पोस्ट करा (ओळख प्रदाता एंडपॉईंटची URL).
<input type="hidden" name="${loginRequest.type}" value="${loginRequest.context}" />
इनपुटचे नाव loginRequest.type (SAMLRequest) आहे. त्या क्षेत्रासाठी सामग्री loginRequest.context आहे, जे पुन्हा base64 एन्कोड केलेले XML आहे.
</form>
</body>
</html>
`)
}
)
app.use(express.urlencoded({extended: true}))
हे मिडलवेअर (नवीन टॅबमध्ये उघडते) HTTP विनंतीचा (नवीन टॅबमध्ये उघडते) मुख्य भाग (body) वाचते. डीफॉल्टनुसार express त्याकडे दुर्लक्ष करते, कारण बहुतांश विनंत्यांना त्याची आवश्यकता नसते. आपल्याला त्याची आवश्यकता आहे कारण POST मुख्य भाग वापरते.
app.use(`/${config.spDir}`, spRouter)
सेवा प्रदाता डिरेक्टरीमध्ये (/sp) राऊटर माउंट करा.
app.get("/", (req, res) => {
res.send(`
<html>
<body>
<button onClick="document.location.href='${config.spUrl}/login'">
Click here to log on
</button>
</body>
</html>
`)
})
जर ब्राउझरने रूट डिरेक्टरी मिळवण्याचा प्रयत्न केला, तर त्याला लॉगिन पेजची लिंक द्या.
app.listen(config.spPort, () => {
console.log(`service provider is running on http://${config.spHostname}:${config.spPort}`)
})
या express अॅप्लिकेशनसह spPort ऐका.
src/idp.mts
हा ओळख प्रदाता आहे. तो सेवा प्रदात्यासारखाच आहे, खालील स्पष्टीकरणे वेगळ्या असलेल्या भागांसाठी आहेत.
const xmlParser = new (await import("fast-xml-parser")).XMLParser(
{
ignoreAttributes: false, // गुणधर्म जतन करा
attributeNamePrefix: "@_", // गुणधर्मांसाठी उपसर्ग
}
)
आपल्याला सेवा प्रदात्याकडून प्राप्त होणारी XML विनंती वाचणे आणि समजून घेणे आवश्यक आहे.
const getLoginPage = requestId => `
हे फंक्शन आपोआप सबमिट होणाऱ्या फॉर्मसह पेज तयार करते जे वरील अनुक्रम आकृतीच्या पायरी 4 मध्ये परत केले जाते.
<html>
<head>
<title>Login page</title>
</head>
<body>
<h2>Login page</h2>
<form method="post" action="./loginSubmitted">
<input type="hidden" name="requestId" value="${requestId}" />
Email address: <input name="email" />
<br />
<button type="Submit">
Login to the service provider
</button>
आपण सेवा प्रदात्याला दोन क्षेत्रे पाठवतो:
requestIdज्याला आपण प्रतिसाद देत आहोत.- वापरकर्ता आयडेंटिफायर (सध्या आपण वापरकर्त्याने दिलेला ईमेल पत्ता वापरतो).
</form>
</body>
</html>
const idpRouter = express.Router()
idpRouter.post("/loginSubmitted", async (req, res) => {
const loginResponse = await idp.createLoginResponse(
वरील अनुक्रम आकृतीच्या पायरी 5 साठी हा हँडलर आहे. idp.createLoginResponse (नवीन टॅबमध्ये उघडते) लॉगिन प्रतिसाद तयार करते.
sp,
{
authnContextClassRef: 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport',
audience: sp.entityID,
प्रेक्षक (audience) हा सेवा प्रदाता आहे.
extract: {
request: {
id: req.body.requestId
}
},
विनंतीमधून काढलेली माहिती. विनंतीमध्ये आपण ज्या एका पॅरामीटरची काळजी घेतो तो म्हणजे requestId, जो सेवा प्रदात्याला विनंत्या आणि त्यांचे प्रतिसाद जुळवण्यास मदत करतो.
signingKey: { privateKey: idpPrivateKey, publicKey: config.idpCert } // स्वाक्षरी सुनिश्चित करा
प्रतिसादावर स्वाक्षरी करण्यासाठी डेटा असण्यासाठी आपल्याला signingKey ची आवश्यकता आहे. सेवा प्रदाता स्वाक्षरी नसलेल्या विनंत्यांवर विश्वास ठेवत नाही.
},
"post",
{
email: req.body.email
हे वापरकर्त्याच्या माहितीचे क्षेत्र आहे जे आपण सेवा प्रदात्याला परत पाठवतो.
}
);
res.send(`
<html>
<body>
<script>
window.onload = function () { document.forms[0].submit(); }
</script>
<form method="post" action="${loginResponse.entityEndpoint}">
<input type="hidden" name="${loginResponse.type}" value="${loginResponse.context}" />
</form>
</body>
</html>
`)
})
पुन्हा, आपोआप सबमिट होणारा फॉर्म वापरा. वरील अनुक्रम आकृतीमधील ही पायरी 6 आहे.
// लॉगिन विनंत्यांसाठी IdP एंडपॉइंट
idpRouter.post(`/login`,
हा तो एंडपॉईंट आहे जो सेवा प्रदात्याकडून लॉगिन विनंती प्राप्त करतो. वरील अनुक्रम आकृतीच्या पायरी 3 चा हा हँडलर आहे.
async (req, res) => {
try {
// पर्यायी मार्ग कारण मला parseLoginRequest कार्यान्वित करता आले नाही.
// const loginRequest = await idp.parseLoginRequest(sp, 'post', req)
const samlRequest = xmlParser.parse(Buffer.from(req.body.SAMLRequest, 'base64').toString('utf-8'))
res.send(getLoginPage(samlRequest["samlp:AuthnRequest"]["@_ID"]))
प्रमाणीकरण विनंतीचा ID वाचण्यासाठी आपण idp.parseLoginRequest (नवीन टॅबमध्ये उघडते) वापरण्यास सक्षम असावे. तथापि, मी ते कार्यान्वित करू शकलो नाही आणि त्यावर जास्त वेळ घालवणे योग्य नव्हते म्हणून मी फक्त एक सामान्य-उद्देशीय XML पार्सर (नवीन टॅबमध्ये उघडते) वापरतो. आपल्याला आवश्यक असलेली माहिती <samlp:AuthnRequest> टॅगमधील ID गुणधर्म आहे, जो XML च्या शीर्ष स्तरावर आहे.
इथेरियम स्वाक्षऱ्या वापरणे
आता आपण सेवा प्रदात्याला वापरकर्त्याची ओळख पाठवू शकतो, पुढची पायरी म्हणजे वापरकर्त्याची ओळख विश्वासार्ह पद्धतीने मिळवणे. Viem आपल्याला वापरकर्त्याच्या पत्त्यासाठी फक्त वॉलेटला विचारण्याची अनुमती देते, परंतु याचा अर्थ माहितीसाठी ब्राउझरला विचारणे असा होतो. आपले ब्राउझरवर नियंत्रण नसते, त्यामुळे आपण त्याकडून मिळणाऱ्या प्रतिसादावर आपोआप विश्वास ठेवू शकत नाही.
त्याऐवजी, IdP ब्राउझरला स्वाक्षरी करण्यासाठी एक स्ट्रिंग पाठवणार आहे. जर ब्राउझरमधील वॉलेट या स्ट्रिंगवर स्वाक्षरी करत असेल, तर याचा अर्थ असा की तो खरोखरच तो पत्ता आहे (म्हणजेच, त्याला त्या पत्त्याशी संबंधित खाजगी की माहित आहे).
हे कृतीत पाहण्यासाठी, विद्यमान IdP आणि SP थांबवा आणि या कमांड्स चालवा:
git checkout eth-signatures
pnpm install
pnpm start
त्यानंतर SP वर (नवीन टॅबमध्ये उघडते) ब्राउझ करा आणि सूचनांचे पालन करा.
लक्षात घ्या की या टप्प्यावर आपल्याला इथेरियम पत्त्यावरून ईमेल पत्ता कसा मिळवायचा हे माहित नाही, त्यामुळे त्याऐवजी आपण SP ला <ethereum address>@bad.email.address कळवतो.
सविस्तर स्पष्टीकरण
मागील आकृतीमधील पायऱ्या 4-5 मध्ये बदल आहेत.
आपण बदललेली एकमेव फाईल idp.mts आहे. येथे बदललेले भाग आहेत.
import { v4 as uuidv4 } from 'uuid'
import { verifyMessage } from 'viem'
आपल्याला या दोन अतिरिक्त लायब्ररींची आवश्यकता आहे. आपण नॉन्स (नवीन टॅबमध्ये उघडते) मूल्य तयार करण्यासाठी uuid (नवीन टॅबमध्ये उघडते) वापरतो. मूल्य स्वतः महत्त्वाचे नसते, फक्त ते एकदाच वापरले जाते ही वस्तुस्थिती महत्त्वाची असते.
viem (नवीन टॅबमध्ये उघडते) लायब्ररी आपल्याला इथेरियम व्याख्या वापरू देते. येथे स्वाक्षरी खरोखरच वैध आहे हे पडताळण्यासाठी आपल्याला त्याची आवश्यकता आहे.
const loginPrompt = "To access the service provider, sign this nonce: "
वॉलेट वापरकर्त्याला संदेशावर स्वाक्षरी करण्यासाठी परवानगी मागतो. केवळ नॉन्स असलेला संदेश वापरकर्त्यांना गोंधळात टाकू शकतो, म्हणून आपण हा प्रॉम्प्ट समाविष्ट करतो.
// requestIDs येथे ठेवा
let nonces = {}
विनंतीला प्रतिसाद देण्यास सक्षम होण्यासाठी आपल्याला विनंतीच्या माहितीची आवश्यकता आहे. आपण ती विनंतीसोबत पाठवू शकतो (पायरी 4), आणि ती परत मिळवू शकतो (पायरी 5). तथापि, आपण ब्राउझरकडून मिळणाऱ्या माहितीवर विश्वास ठेवू शकत नाही, जे संभाव्यतः प्रतिकूल वापरकर्त्याच्या नियंत्रणाखाली असते. त्यामुळे नॉन्सला की म्हणून वापरून ती येथे साठवणे अधिक चांगले आहे.
लक्षात घ्या की साधेपणासाठी आपण येथे हे व्हेरिएबल म्हणून करत आहोत. तथापि, याचे अनेक तोटे आहेत:
- आपण डिनायल ऑफ सर्व्हिस (denial of service) हल्ल्यास असुरक्षित आहोत. एखादा दुर्भावनापूर्ण वापरकर्ता अनेक वेळा लॉग इन करण्याचा प्रयत्न करू शकतो, ज्यामुळे आपली मेमरी भरू शकते.
- जर IdP प्रक्रिया रीस्टार्ट करण्याची आवश्यकता असेल, तर आपण विद्यमान मूल्ये गमावतो.
- आपण एकाधिक प्रक्रियांमध्ये लोड बॅलन्स करू शकत नाही, कारण प्रत्येकाचे स्वतःचे व्हेरिएबल असेल.
प्रॉडक्शन सिस्टमवर आपण डेटाबेस वापरू आणि काही प्रकारची एक्सपायरी यंत्रणा लागू करू.
const getSignaturePage = requestId => {
const nonce = uuidv4()
nonces[nonce] = requestId
एक नॉन्स तयार करा आणि भविष्यातील वापरासाठी requestId साठवा.
return `
<html>
<head>
<script type="module">
जेव्हा पेज लोड होते तेव्हा हे JavaScript आपोआप कार्यान्वित होते.
import { createWalletClient, custom, getAddress } from 'https://esm.sh/viem'
आपल्याला viem मधील अनेक फंक्शन्सची आवश्यकता आहे.
if (!window.ethereum) {
alert("Please install MetaMask or a compatible wallet and then reload")
}
जर ब्राउझरवर वॉलेट असेल तरच आपण काम करू शकतो.
const [account] = await window.ethereum.request({method: 'eth_requestAccounts'})
वॉलेटकडून खात्यांच्या सूचीची विनंती करा (window.ethereum). असे गृहीत धरा की किमान एक खाते आहे आणि फक्त पहिले खाते साठवा.
const walletClient = createWalletClient({
account,
transport: custom(window.ethereum)
})
ब्राउझर वॉलेटशी संवाद साधण्यासाठी एक वॉलेट क्लायंट (नवीन टॅबमध्ये उघडते) तयार करा.
window.goodSignature = () => {
walletClient.signMessage({
message: "${loginPrompt}${nonce}"
वापरकर्त्याला संदेशावर स्वाक्षरी करण्यास सांगा. कारण हे संपूर्ण HTML एका टेम्पलेट स्ट्रिंगमध्ये (नवीन टॅबमध्ये उघडते) आहे, आपण idp प्रक्रियेत परिभाषित केलेले व्हेरिएबल्स वापरू शकतो. अनुक्रम आकृतीमधील ही पायरी 4.5 आहे.
}).then(signature => {
const path= "/${config.idpDir}/signature/${nonce}/" + account + "/" + signature
window.location.href = path
})
}
/idp/signature/<nonce>/<address>/<signature> वर पुनर्निर्देशित करा. अनुक्रम आकृतीमधील ही पायरी 5 आहे.
window.badSignature = () => {
const path= "/${config.idpDir}/signature/${nonce}/" +
getAddress("0x" + "BAD060A7".padEnd(40, "0")) +
"/0x" + "BAD0516".padStart(130, "0")
window.location.href = path
}
स्वाक्षरी ब्राउझरद्वारे परत पाठविली जाते, जी संभाव्यतः दुर्भावनापूर्ण असू शकते (तुम्हाला ब्राउझरमध्ये फक्त http://localhost:3001/idp/signature/bad-nonce/bad-address/bad-signature उघडण्यापासून रोखण्यासाठी काहीही नाही). म्हणून, IdP प्रक्रिया चुकीच्या स्वाक्षऱ्या योग्यरित्या हाताळते की नाही हे पडताळणे महत्त्वाचे आहे.
</script>
</head>
<body>
<h2>Please sign</h2>
<button onClick="window.goodSignature()">
Submit a good (valid) signature
</button>
<br/>
<button onClick="window.badSignature()">
Submit a bad (invalid) signature
</button>
</body>
</html>
`
}
उर्वरित फक्त मानक HTML आहे.
idpRouter.get("/signature/:nonce/:account/:signature", async (req, res) => {
अनुक्रम आकृतीमधील पायरी 5 साठी हा हँडलर आहे.
const requestId = nonces[req.params.nonce]
if (requestId === undefined) {
res.send("Bad nonce")
return ;
}
nonces[req.params.nonce] = undefined
विनंती ID मिळवा आणि तो पुन्हा वापरला जाऊ शकत नाही याची खात्री करण्यासाठी nonces मधून नॉन्स हटवा.
try {
स्वाक्षरी अवैध असण्याचे अनेक मार्ग असल्यामुळे, कोणत्याही त्रुटी पकडण्यासाठी आपण याला try ... catch ब्लॉकमध्ये गुंडाळतो.
const validSignature = await verifyMessage({
address: req.params.account,
message: `${loginPrompt}${req.params.nonce}`,
signature: req.params.signature
})
अनुक्रम आकृतीमधील पायरी 5.5 ची अंमलबजावणी करण्यासाठी verifyMessage (नवीन टॅबमध्ये उघडते) वापरा.
if (!validSignature)
throw("Bad signature")
} catch (err) {
res.send("Error:" + err)
return ;
}
एका छोट्या बदलाचा अपवाद वगळता, उर्वरित हँडलर आपण यापूर्वी /loginSubmitted हँडलरमध्ये जे केले आहे त्याच्या समतुल्य आहे.
const loginResponse = await idp.createLoginResponse(
.
.
.
{
email: req.params.account + "@bad.email.address"
}
);
आपल्याकडे प्रत्यक्ष ईमेल पत्ता नाही (तो आपल्याला पुढील विभागात मिळेल), त्यामुळे सध्या आपण इथेरियम पत्ता परत करतो आणि तो ईमेल पत्ता नाही असे स्पष्टपणे चिन्हांकित करतो.
// लॉगिन विनंत्यांसाठी IdP एंडपॉइंट
idpRouter.post(`/login`,
async (req, res) => {
try {
// पर्यायी मार्ग कारण मला parseLoginRequest कार्यान्वित करता आले नाही.
// const loginRequest = await idp.parseLoginRequest(sp, 'post', req)
const samlRequest = xmlParser.parse(Buffer.from(req.body.SAMLRequest, 'base64').toString('utf-8'))
res.send(getSignaturePage(samlRequest["samlp:AuthnRequest"]["@_ID"]))
} catch (err) {
console.error('Error processing SAML response:', err);
res.status(400).send('SAML authentication failed');
}
}
)
पायरी 3 हँडलरमध्ये getLoginPage ऐवजी आता getSignaturePage वापरा.
ईमेल पत्ता मिळवणे
पुढची पायरी म्हणजे सेवा प्रदात्याने विनंती केलेला आयडेंटिफायर, ईमेल पत्ता मिळवणे. ते करण्यासाठी, आपण इथेरियम साक्षांकन सेवा (Ethereum Attestation Service - EAS) (नवीन टॅबमध्ये उघडते) वापरतो.
साक्षांकने मिळवण्याचा सर्वात सोपा मार्ग म्हणजे GraphQL API (नवीन टॅबमध्ये उघडते) वापरणे. आपण ही क्वेरी वापरतो:
query GetAttestationsByRecipient {
attestations(
where: {
recipient: { equals: "${getAddress(ethAddr)}" }
schemaId: { equals: "0xfa2eff59a916e3cc3246f9aec5e0ca00874ae9d09e4678e5016006f07622f977" }
}
take: 1
) {
data
id
attester
}
}
या schemaId (नवीन टॅबमध्ये उघडते) मध्ये फक्त एक ई-मेल पत्ता समाविष्ट आहे. ही क्वेरी या स्कीमाच्या साक्षांकनांची विनंती करते. साक्षांकनाच्या विषयाला recipient म्हटले जाते. तो नेहमी एक इथेरियम पत्ता असतो.
चेतावणी: आपण येथे ज्या प्रकारे साक्षांकने मिळवत आहोत त्यात दोन सुरक्षा समस्या आहेत.
-
आपण API एंडपॉईंट,
https://optimism.easscan.org/graphqlवर जात आहोत, जो एक केंद्रित घटक आहे. आपणidगुणधर्म मिळवू शकतो आणि नंतर साक्षांकन खरे आहे की नाही हे पडताळण्यासाठी ऑनचेन लुकअप करू शकतो, परंतु API एंडपॉईंट तरीही आपल्याला त्यांच्याबद्दल न सांगता साक्षांकने सेन्सॉर करू शकतो.ही समस्या सोडवणे अशक्य नाही, आपण आपला स्वतःचा GraphQL एंडपॉईंट चालवू शकतो आणि चेन नोंदींमधून (logs) साक्षांकने मिळवू शकतो, परंतु आपल्या उद्देशांसाठी ते खूप जास्त आहे.
-
आपण साक्षांकनकर्त्याची ओळख पाहत नाही. कोणीही आपल्याला चुकीची माहिती देऊ शकतो. वास्तविक जगातील अंमलबजावणीमध्ये आपल्याकडे विश्वासार्ह साक्षांकनकर्त्यांचा एक संच असेल आणि आपण केवळ त्यांचीच साक्षांकने पाहू.
हे कृतीत पाहण्यासाठी, विद्यमान IdP आणि SP थांबवा आणि या कमांड्स चालवा:
git checkout email-address
pnpm install
pnpm start
त्यानंतर तुमचा ई-मेल पत्ता द्या. तुमच्याकडे ते करण्याचे दोन मार्ग आहेत:
-
खाजगी की वापरून वॉलेट इम्पोर्ट करा आणि
0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80ही चाचणी खाजगी की वापरा. -
तुमच्या स्वतःच्या ई-मेल पत्त्यासाठी साक्षांकन जोडा:
-
साक्षांकन एक्सप्लोररमधील स्कीमावर (नवीन टॅबमध्ये उघडते) ब्राउझ करा.
-
Attest with Schema वर क्लिक करा.
-
प्राप्तकर्ता म्हणून तुमचा इथेरियम पत्ता, ईमेल पत्ता म्हणून तुमचा ई-मेल पत्ता प्रविष्ट करा आणि Onchain निवडा. त्यानंतर Make Attestation वर क्लिक करा.
-
तुमच्या वॉलेटमध्ये व्यवहार मंजूर करा. गॅससाठी पैसे देण्यासाठी तुम्हाला ऑप्टिमिझम् ब्लॉकचेनवर (नवीन टॅबमध्ये उघडते) काही ETH ची आवश्यकता असेल.
-
कोणत्याही प्रकारे, तुम्ही हे केल्यानंतर http://localhost:3000 (नवीन टॅबमध्ये उघडते) वर ब्राउझ करा आणि सूचनांचे पालन करा. जर तुम्ही चाचणी खाजगी की इम्पोर्ट केली असेल, तर तुम्हाला मिळणारा ई-मेल test_addr_0@example.com असेल. जर तुम्ही तुमचा स्वतःचा पत्ता वापरला असेल, तर तुम्ही जे साक्षांकन केले असेल तेच असावे.
सविस्तर स्पष्टीकरण
नवीन पायऱ्या म्हणजे GraphQL संवाद, पायऱ्या 5.6 आणि 5.7 आहेत.
पुन्हा, येथे idp.mts चे बदललेले भाग आहेत.
import { GraphQLClient } from 'graphql-request'
import { SchemaEncoder } from '@ethereum-attestation-service/eas-sdk'
आपल्याला आवश्यक असलेल्या लायब्ररी इम्पोर्ट करा.
const graphqlEndpointUrl = "https://optimism.easscan.org/graphql"
प्रत्येक ब्लॉकचेनसाठी एक वेगळा एंडपॉईंट (नवीन टॅबमध्ये उघडते) आहे.
const graphqlClient = new GraphQLClient(graphqlEndpointUrl, { fetch })
एक नवीन GraphQLClient क्लायंट तयार करा जो आपण एंडपॉईंटला क्वेरी करण्यासाठी वापरू शकतो.
const graphqlSchema = 'string emailAddress'
const graphqlEncoder = new SchemaEncoder(graphqlSchema)
GraphQL आपल्याला फक्त बाइट्ससह एक अपारदर्शक डेटा ऑब्जेक्ट देते. ते समजून घेण्यासाठी आपल्याला स्कीमाची आवश्यकता आहे.
const ethereumAddressToEmail = async ethAddr => {
इथेरियम पत्त्यावरून ई-मेल पत्ता मिळवण्यासाठी एक फंक्शन.
const query = `
query GetAttestationsByRecipient {
ही एक GraphQL क्वेरी आहे.
attestations(
आपण साक्षांकने शोधत आहोत.
where: {
recipient: { equals: "${getAddress(ethAddr)}" }
schemaId: { equals: "0xfa2eff59a916e3cc3246f9aec5e0ca00874ae9d09e4678e5016006f07622f977" }
}
आपल्याला हवी असलेली साक्षांकने आपल्या स्कीमामधील आहेत, जिथे प्राप्तकर्ता getAddress(ethAddr) आहे. getAddress (नवीन टॅबमध्ये उघडते) फंक्शन आपल्या पत्त्यामध्ये योग्य चेकसम (checksum) (नवीन टॅबमध्ये उघडते) असल्याची खात्री करते. हे आवश्यक आहे कारण GraphQL केस-सेन्सिटिव्ह (case-significant) आहे. "0xBAD060A7", "0xBad060A7" आणि "0xbad060a7" ही वेगवेगळी मूल्ये आहेत.
take: 1
आपल्याला कितीही साक्षांकने सापडली तरीही, आपल्याला फक्त पहिलेच हवे आहे.
) {
data
id
attester
}
}`
आपल्याला प्राप्त करायची असलेली क्षेत्रे.
attester: ज्या पत्त्याने साक्षांकन सबमिट केले आहे. सामान्यतः साक्षांकनावर विश्वास ठेवायचा की नाही हे ठरवण्यासाठी याचा वापर केला जातो.id: साक्षांकन ID. GraphQL क्वेरीमधील माहिती योग्य आहे हे पडताळण्यासाठी तुम्ही हे मूल्य ऑनचेन साक्षांकन वाचण्यासाठी (नवीन टॅबमध्ये उघडते) वापरू शकता.data: स्कीमा डेटा (या प्रकरणात, ई-मेल पत्ता).
const queryResult = await graphqlClient.request(query)
if (queryResult.attestations.length == 0)
return "no_address@available.is"
जर कोणतेही साक्षांकन नसेल, तर असे मूल्य परत करा जे स्पष्टपणे चुकीचे आहे, परंतु जे सेवा प्रदात्याला वैध वाटेल.
const attestationDataFields = graphqlEncoder.decodeData(queryResult.attestations[0].data)
return attestationDataFields[0].value.value
}
जर मूल्य असेल, तर डेटा डिकोड करण्यासाठी decodeData वापरा. आपल्याला ते प्रदान करत असलेल्या मेटाडेटाची आवश्यकता नाही, फक्त मूल्याचीच आवश्यकता आहे.
const loginResponse = await idp.createLoginResponse(
sp,
{
.
.
.
},
"post",
{
email: await ethereumAddressToEmail(req.params.account)
}
);
ई-मेल पत्ता मिळवण्यासाठी नवीन फंक्शन वापरा.
विकेंद्रीकरणाचे काय?
या कॉन्फिगरेशनमध्ये वापरकर्ते ते नसलेले कोणीतरी असल्याचे भासवू शकत नाहीत, जोपर्यंत आपण इथेरियम ते ई-मेल पत्ता मॅपिंगसाठी विश्वासार्ह साक्षांकनकर्त्यांवर अवलंबून असतो. तथापि, आपला ओळख प्रदाता अद्याप एक केंद्रित घटक आहे. ज्याच्याकडे ओळख प्रदात्याची खाजगी की आहे तो सेवा प्रदात्याला चुकीची माहिती पाठवू शकतो.
मल्टी-पार्टी कॉम्प्युटेशन (MPC) (नवीन टॅबमध्ये उघडते) वापरून यावर उपाय असू शकतो. मला आशा आहे की मी भविष्यातील ट्युटोरिअलमध्ये याबद्दल लिहीन.
निष्कर्ष
इथेरियम स्वाक्षऱ्यांसारख्या लॉग ऑन मानकाचा अवलंब करताना कोंबडी आणि अंड्याच्या समस्येचा (chicken and egg problem) सामना करावा लागतो. सेवा प्रदात्यांना शक्य तितक्या विस्तृत बाजारपेठेला आकर्षित करायचे असते. वापरकर्त्यांना त्यांच्या लॉग ऑन मानकाला सपोर्ट करण्याबद्दल काळजी न करता सेवा अॅक्सेस करण्यास सक्षम व्हायचे असते. इथेरियम IdP सारखे अॅडॉप्टर तयार केल्याने आपल्याला या अडथळ्यावर मात करण्यास मदत होऊ शकते.





