ویب۲ کی توثیق کے لیے ایتھیریم کا استعمال
تعارف
SAML (opens in a new tab) ویب۲ پر استعمال ہونے والا ایک معیار ہے جو ایک شناختی فراہم کنندہ (IdP) (opens in a new tab) کو [سروس فراہم کنندگان (SP)](https://en.wikipedia.org/wiki/Service_provider_(SAML) (opens in a new tab) کے لیے صارف کی معلومات فراہم کرنے کی اجازت دیتا ہے۔
اس ٹیوٹوریل میں آپ سیکھیں گے کہ ایتھیریم دستخطوں کو SAML کے ساتھ کیسے مربوط کیا جائے تاکہ صارفین اپنے ایتھیریم والیٹس کا استعمال کرتے ہوئے خود کو ان ویب۲ سروسز پر توثیق کر سکیں جو ابھی تک مقامی طور پر ایتھیریم کو سپورٹ نہیں کرتیں۔
نوٹ کریں کہ یہ ٹیوٹوریل دو الگ الگ سامعین کے لیے لکھا گیا ہے:
- ایتھیریم کے لوگ جو ایتھیریم کو سمجھتے ہیں اور انہیں SAML سیکھنے کی ضرورت ہے
- ویب۲ کے لوگ جو SAML اور ویب۲ توثیق کو سمجھتے ہیں اور انہیں ایتھیریم سیکھنے کی ضرورت ہے
نتیجتاً، اس میں بہت سا ایسا تعارفی مواد شامل ہوگا جو آپ پہلے سے جانتے ہیں۔ آپ اسے بلا جھجھک چھوڑ سکتے ہیں۔
ایتھیریم کے لوگوں کے لیے SAML
SAML ایک مرکزی پروٹوکول ہے۔ ایک سروس فراہم کنندہ (SP) کسی شناختی فراہم کنندہ (IdP) سے دعوے (جیسے "یہ میرا صارف جان ہے، اسے A، B، اور C کرنے کی اجازت ہونی چاہیے") صرف اسی صورت میں قبول کرتا ہے جب اس کا اس کے ساتھ، یا اس سرٹیفکیٹ اتھارٹی (opens in a new tab) کے ساتھ پہلے سے موجود اعتماد کا رشتہ ہو جس نے اس IdP کے سرٹیفکیٹ پر دستخط کیے ہوں۔
مثال کے طور پر، SP ایک ٹریول ایجنسی ہو سکتی ہے جو کمپنیوں کو سفری خدمات فراہم کرتی ہے، اور IdP کسی کمپنی کی اندرونی ویب سائٹ ہو سکتی ہے۔ جب ملازمین کو کاروباری سفر بک کرنے کی ضرورت ہوتی ہے، تو ٹریول ایجنسی انہیں اصل میں سفر بک کرنے کی اجازت دینے سے پہلے کمپنی کے ذریعے توثیق کے لیے بھیجتی ہے۔
یہ وہ طریقہ ہے جس سے تینوں ادارے، براؤزر، SP، اور IdP، رسائی کے لیے بات چیت کرتے ہیں۔ SP کو براؤزر استعمال کرنے والے صارف کے بارے میں پہلے سے کچھ جاننے کی ضرورت نہیں ہوتی، بس IdP پر اعتماد کرنا ہوتا ہے۔
SAML کے لوگوں کے لیے ایتھیریم
ایتھیریم ایک لامركزی نظام ہے۔
صارفین کے پاس ایک نجی کلید ہوتی ہے (جو عام طور پر براؤزر ایکسٹینشن میں رکھی جاتی ہے)۔ نجی کلید سے آپ ایک عوامی کلید اخذ کر سکتے ہیں، اور اس سے ایک 20-byte پتہ حاصل کر سکتے ہیں۔ جب صارفین کو کسی سسٹم میں لاگ ان کرنے کی ضرورت ہوتی ہے، تو ان سے ایک نانس (ایک بار استعمال ہونے والی قدر) کے ساتھ ایک پیغام پر دستخط کرنے کی درخواست کی جاتی ہے۔ سرور اس بات کی تصدیق کر سکتا ہے کہ دستخط اسی پتہ کے ذریعے بنائے گئے تھے۔
دستخط صرف ایتھیریم پتہ کی تصدیق کرتا ہے۔ صارف کی دیگر خصوصیات حاصل کرنے کے لیے، آپ عام طور پر تصدیقات (opens in a new tab) کا استعمال کرتے ہیں۔ ایک تصدیق میں عام طور پر یہ فیلڈز ہوتے ہیں:
- تصدیق کنندہ (Attestor)، وہ پتہ جس نے تصدیق کی
- وصول کنندہ (Recipient)، وہ پتہ جس پر تصدیق لاگو ہوتی ہے
- ڈیٹا (Data)، وہ ڈیٹا جس کی تصدیق کی جا رہی ہے، جیسے نام، اجازتیں، وغیرہ۔
- اسکیما (Schema)، اس اسکیما کی ID جو ڈیٹا کی تشریح کے لیے استعمال ہوتی ہے۔
ایتھیریم کی لامركزی نوعیت کی وجہ سے، کوئی بھی صارف تصدیقات کر سکتا ہے۔ تصدیق کنندہ کی شناخت یہ جاننے کے لیے اہم ہے کہ ہم کن تصدیقات کو قابل اعتماد سمجھتے ہیں۔
سیٹ اپ
پہلا قدم یہ ہے کہ ایک SAML SP اور ایک SAML IdP آپس میں بات چیت کر رہے ہوں۔
-
سافٹ ویئر ڈاؤن لوڈ کریں۔ اس مضمون کے لیے نمونہ سافٹ ویئر GitHub پر (opens in a new tab) موجود ہے۔ مختلف مراحل مختلف برانچز میں محفوظ ہیں، اس مرحلے کے لیے آپ کو
saml-onlyدرکار ہے۔git clone https://github.com/qbzzt/250420-saml-ethereum -b saml-only cd 250420-saml-ethereum pnpm install -
سیلف سائنڈ سرٹیفکیٹس کے ساتھ کلیدیں بنائیں۔ اس کا مطلب ہے کہ کلید خود اپنی سرٹیفکیٹ اتھارٹی ہے، اور اسے سروس فراہم کنندہ میں دستی طور پر امپورٹ کرنے کی ضرورت ہے۔ مزید معلومات کے لیے OpenSSL کی دستاویزات (opens in a new tab) دیکھیں۔
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 -
URL http://localhost:3000/ (opens in a new tab) پر SP کو براؤز کریں اور IdP (پورٹ 3001) پر ری ڈائریکٹ ہونے کے لیے بٹن پر کلک کریں۔
-
IdP کو اپنا ای میل پتہ فراہم کریں اور سروس فراہم کنندہ میں لاگ ان کریں پر کلک کریں۔ دیکھیں کہ آپ واپس سروس فراہم کنندہ (پورٹ 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}`
دونوں اجزاء کے لیے URLs۔
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 (opens email client) ہے") ہمیں URL http://localhost:3000/sp/assertion پر HTTP POST (opens in a new tab) استعمال کرنے کی ضرورت ہے۔
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 (opens in a new tab) لائبریری کا استعمال کرتے ہیں۔
import * as validator from "@authenio/samlify-node-xmllint"
saml.setSchemaValidator(validator)
samlify لائبریری توقع کرتی ہے کہ ایک پیکیج اس بات کی توثیق کرے گا کہ XML درست ہے، متوقع عوامی کلید کے ساتھ دستخط شدہ ہے، وغیرہ۔ ہم اس مقصد کے لیے @authenio/samlify-node-xmllint (opens in a new tab) کا استعمال کرتے ہیں۔
const express = (await import("express")).default
const spRouter = express.Router()
const app = express()
ایک express (opens in a new tab) Router (opens in a new tab) ایک "منی ویب سائٹ" ہے جسے کسی ویب سائٹ کے اندر ماؤنٹ کیا جا سکتا ہے۔ اس صورت میں، ہم اسے سروس فراہم کنندہ کی تمام تعریفوں کو ایک ساتھ گروپ کرنے کے لیے استعمال کرتے ہیں۔
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`,
یہ وہ صفحہ ہے جس تک براؤزر اپنی شناخت کے لیے رسائی حاصل کرتا ہے۔ دعوے میں صارف کا شناخت کنندہ شامل ہوتا ہے (یہاں ہم ای میل پتہ استعمال کرتے ہیں)، اور اس میں اضافی خصوصیات شامل ہو سکتی ہیں۔ یہ اوپر دی گئی ترتیب کے خاکہ میں مرحلہ 7 کا ہینڈلر ہے۔
async (req, res) => {
// console.log(`SAML response:\n${Buffer.from(req.body.SAMLResponse, 'base64').toString('utf-8')}`)
آپ دعوے میں فراہم کردہ XML ڈیٹا دیکھنے کے لیے کمنٹ آؤٹ کی گئی کمانڈ استعمال کر سکتے ہیں۔ یہ base64 انکوڈ شدہ (opens in a new tab) ہے۔
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 ہے، جو کہ دوبارہ XML ہے جو base64 انکوڈ شدہ ہے۔
</form>
</body>
</html>
`)
}
)
app.use(express.urlencoded({extended: true}))
یہ مڈل ویئر (opens in a new tab) HTTP درخواست (opens in a new tab) کی باڈی کو پڑھتا ہے۔ پہلے سے طے شدہ طور پر ایکسپریس اسے نظر انداز کر دیتا ہے، کیونکہ زیادہ تر درخواستوں کو اس کی ضرورت نہیں ہوتی۔ ہمیں اس کی ضرورت ہے کیونکہ 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}`)
})
اس ایکسپریس ایپلیکیشن کے ساتھ 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 (opens in a new tab) لاگ ان کا جواب بناتا ہے۔
sp,
{
authnContextClassRef: 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport',
audience: sp.entityID,
سامعین سروس فراہم کنندہ ہیں۔
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 (opens in a new tab) استعمال کرنے کے قابل ہونا چاہیے۔ تاہم، میں اسے کام کرنے کے قابل نہیں بنا سکا اور اس پر زیادہ وقت صرف کرنا مناسب نہیں تھا اس لیے میں نے صرف ایک عام مقصد کا XML پارسر (opens in a new tab) استعمال کیا۔ ہمیں جس معلومات کی ضرورت ہے وہ <samlp:AuthnRequest> ٹیگ کے اندر ID وصف ہے، جو XML کی اوپری سطح پر ہے۔
ایتھیریم دستخطوں کا استعمال
اب جب کہ ہم سروس فراہم کنندہ کو صارف کی شناخت بھیج سکتے ہیں، اگلا قدم قابل اعتماد طریقے سے صارف کی شناخت حاصل کرنا ہے۔ Viem ہمیں صرف والیٹ سے صارف کا پتہ مانگنے کی اجازت دیتا ہے، لیکن اس کا مطلب براؤزر سے معلومات مانگنا ہے۔ ہم براؤزر کو کنٹرول نہیں کرتے، اس لیے ہم اس سے ملنے والے جواب پر خود بخود اعتماد نہیں کر سکتے۔
اس کے بجائے، IdP براؤزر کو دستخط کرنے کے لیے ایک سٹرنگ بھیجنے والا ہے۔ اگر براؤزر میں موجود والیٹ اس سٹرنگ پر دستخط کرتا ہے، تو اس کا مطلب ہے کہ یہ واقعی وہی پتہ ہے (یعنی، یہ اس نجی کلید کو جانتا ہے جو اس پتہ سے مطابقت رکھتی ہے)۔
اسے عملی طور پر دیکھنے کے لیے، موجودہ IdP اور SP کو روکیں اور یہ کمانڈز چلائیں:
git checkout eth-signatures
pnpm install
pnpm start
پھر SP پر (opens in a new tab) براؤز کریں اور ہدایات پر عمل کریں۔
نوٹ کریں کہ اس مقام پر ہم نہیں جانتے کہ ایتھیریم پتہ سے ای میل پتہ کیسے حاصل کیا جائے، اس لیے اس کے بجائے ہم SP کو <ethereum address>@bad.email.address رپورٹ کرتے ہیں۔
تفصیلی وضاحت
تبدیلیاں پچھلے خاکہ میں مراحل 4-5 میں ہیں۔
ہم نے صرف idp.mts فائل کو تبدیل کیا ہے۔ یہاں تبدیل شدہ حصے ہیں۔
import { v4 as uuidv4 } from 'uuid'
import { verifyMessage } from 'viem'
ہمیں ان دو اضافی لائبریریوں کی ضرورت ہے۔ ہم نانس (opens in a new tab) کی قدر بنانے کے لیے uuid (opens in a new tab) کا استعمال کرتے ہیں۔ قدر بذات خود اہمیت نہیں رکھتی، صرف یہ حقیقت اہم ہے کہ اسے صرف ایک بار استعمال کیا جاتا ہے۔
viem (opens in a new tab) لائبریری ہمیں ایتھیریم کی تعریفیں استعمال کرنے دیتی ہے۔ یہاں ہمیں اس بات کی تصدیق کرنے کے لیے اس کی ضرورت ہے کہ دستخط واقعی درست ہے۔
const loginPrompt = "To access the service provider, sign this nonce: "
والیٹ صارف سے پیغام پر دستخط کرنے کی اجازت مانگتا ہے۔ ایک پیغام جو صرف ایک نانس ہو، صارفین کو الجھا سکتا ہے، اس لیے ہم یہ پرامپٹ شامل کرتے ہیں۔
// requestIDs کو یہاں رکھیں
let nonces = {}
ہمیں درخواست کی معلومات کی ضرورت ہے تاکہ ہم اس کا جواب دے سکیں۔ ہم اسے درخواست کے ساتھ بھیج سکتے ہیں (مرحلہ 4)، اور اسے واپس وصول کر سکتے ہیں (مرحلہ 5)۔ تاہم، ہم براؤزر سے ملنے والی معلومات پر اعتماد نہیں کر سکتے، جو ممکنہ طور پر کسی مخالف صارف کے کنٹرول میں ہوتا ہے۔ اس لیے اسے یہاں نانس کو کلید کے طور پر استعمال کرتے ہوئے محفوظ کرنا بہتر ہے۔
نوٹ کریں کہ ہم سادگی کی خاطر اسے یہاں ایک متغیر کے طور پر کر رہے ہیں۔ تاہم، اس کے کئی نقصانات ہیں:
- ہم ڈینائل آف سروس (DoS) حملے کا شکار ہو سکتے ہیں۔ ایک بدنیتی پر مبنی صارف ہماری میموری کو بھرتے ہوئے کئی بار لاگ ان کرنے کی کوشش کر سکتا ہے۔
- اگر 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)
})
براؤزر والیٹ کے ساتھ تعامل کرنے کے لیے ایک والیٹ کلائنٹ (opens in a new tab) بنائیں۔
window.goodSignature = () => {
walletClient.signMessage({
message: "${loginPrompt}${nonce}"
صارف سے پیغام پر دستخط کرنے کو کہیں۔ چونکہ یہ پورا HTML ایک ٹیمپلیٹ سٹرنگ (opens in a new tab) میں ہے، اس لیے ہم 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 (opens in a new tab) کا استعمال کریں۔
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');
}
}
)
getLoginPage کے بجائے، اب مرحلہ 3 کے ہینڈلر میں getSignaturePage استعمال کریں۔
ای میل پتہ حاصل کرنا
اگلا قدم ای میل پتہ حاصل کرنا ہے، جو سروس فراہم کنندہ کی طرف سے درخواست کردہ شناخت کنندہ ہے۔ ایسا کرنے کے لیے، ہم ایتھیریم اٹیسٹیشن سروس (EAS) (opens in a new tab) کا استعمال کرتے ہیں۔
تصدیقات حاصل کرنے کا سب سے آسان طریقہ GraphQL API (opens in a new tab) کا استعمال کرنا ہے۔ ہم یہ استفسار (query) استعمال کرتے ہیں:
query GetAttestationsByRecipient {
attestations(
where: {
recipient: { equals: "${getAddress(ethAddr)}" }
schemaId: { equals: "0xfa2eff59a916e3cc3246f9aec5e0ca00874ae9d09e4678e5016006f07622f977" }
}
take: 1
) {
data
id
attester
}
}
اس schemaId (opens in a new tab) میں صرف ایک ای میل پتہ شامل ہے۔ یہ استفسار اس اسکیما کی تصدیقات طلب کرتا ہے۔ تصدیق کے موضوع کو recipient کہا جاتا ہے۔ یہ ہمیشہ ایک ایتھیریم پتہ ہوتا ہے۔
انتباہ: ہم یہاں جس طرح سے تصدیقات حاصل کر رہے ہیں اس میں دو حفاظتی مسائل ہیں۔
-
ہم API اینڈ پوائنٹ،
https://optimism.easscan.org/graphqlپر جا رہے ہیں، جو ایک مرکزی جزو ہے۔ ہمidوصف حاصل کر سکتے ہیں اور پھر یہ تصدیق کرنے کے لیے آن چین تلاش کر سکتے ہیں کہ آیا کوئی تصدیق اصلی ہے، لیکن API اینڈ پوائنٹ اب بھی ہمیں ان کے بارے میں نہ بتا کر تصدیقات کو سنسر کر سکتا ہے۔اس مسئلے کو حل کرنا ناممکن نہیں ہے، ہم اپنا GraphQL اینڈ پوائنٹ چلا سکتے ہیں اور چین لاگز سے تصدیقات حاصل کر سکتے ہیں، لیکن یہ ہمارے مقاصد کے لیے ضرورت سے زیادہ ہے۔
-
ہم تصدیق کنندہ کی شناخت نہیں دیکھتے۔ کوئی بھی ہمیں غلط معلومات فراہم کر سکتا ہے۔ حقیقی دنیا کے نفاذ میں ہمارے پاس قابل اعتماد تصدیق کنندگان کا ایک مجموعہ ہوگا اور ہم صرف ان کی تصدیقات کو دیکھیں گے۔
اسے عملی طور پر دیکھنے کے لیے، موجودہ IdP اور SP کو روکیں اور یہ کمانڈز چلائیں:
git checkout email-address
pnpm install
pnpm start
پھر اپنا ای میل پتہ فراہم کریں۔ آپ کے پاس ایسا کرنے کے دو طریقے ہیں:
-
نجی کلید کا استعمال کرتے ہوئے ایک والیٹ امپورٹ کریں، اور ٹیسٹنگ نجی کلید
0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80استعمال کریں۔ -
اپنے ای میل پتہ کے لیے ایک تصدیق شامل کریں:
-
اٹیسٹیشن ایکسپلورر میں اسکیما (opens in a new tab) پر براؤز کریں۔
-
Attest with Schema پر کلک کریں۔
-
وصول کنندہ کے طور پر اپنا ایتھیریم پتہ، ای میل پتہ کے طور پر اپنا ای میل پتہ درج کریں، اور آن چین منتخب کریں۔ پھر Make Attestation پر کلک کریں۔
-
اپنے والیٹ میں ٹرانزیکشن کو منظور کریں۔ گیس کی ادائیگی کے لیے آپ کو آپٹیمزم بلاک چین (opens in a new tab) پر کچھ ETH کی ضرورت ہوگی۔
-
کسی بھی طرح، ایسا کرنے کے بعد http://localhost:3000 (opens in a new tab) پر براؤز کریں اور ہدایات پر عمل کریں۔ اگر آپ نے ٹیسٹنگ نجی کلید امپورٹ کی ہے، تو آپ کو موصول ہونے والا ای میل 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"
ہر بلاک چین کے لیے ایک الگ اینڈ پوائنٹ (opens in a new tab) موجود ہے۔
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 (opens in a new tab) فنکشن اس بات کو یقینی بناتا ہے کہ ہمارے پتہ کا چیک سم (checksum) (opens in a new tab) درست ہے۔ یہ ضروری ہے کیونکہ GraphQL کیس-حساس (case-significant) ہے۔ "0xBAD060A7"، "0xBad060A7"، اور "0xbad060a7" مختلف اقدار ہیں۔
take: 1
اس بات سے قطع نظر کہ ہمیں کتنی تصدیقات ملتی ہیں، ہمیں صرف پہلی والی چاہیے۔
) {
data
id
attester
}
}`
وہ فیلڈز جو ہم وصول کرنا چاہتے ہیں۔
attester: وہ پتہ جس نے تصدیق جمع کرائی۔ عام طور پر یہ فیصلہ کرنے کے لیے استعمال ہوتا ہے کہ آیا تصدیق پر اعتماد کیا جائے یا نہیں۔id: تصدیق کی ID۔ آپ اس قدر کو آن چین تصدیق پڑھنے (opens in a new tab) کے لیے استعمال کر سکتے ہیں تاکہ یہ تصدیق کی جا سکے کہ 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) (opens in a new tab) کا استعمال کرتے ہوئے اس کا کوئی حل ہو سکتا ہے۔ مجھے امید ہے کہ میں مستقبل کے کسی ٹیوٹوریل میں اس کے بارے میں لکھوں گا۔
نتیجہ
لاگ آن معیار کو اپنانے، جیسے کہ ایتھیریم دستخط، کو مرغی اور انڈے کے مسئلے کا سامنا ہے۔ سروس فراہم کنندگان وسیع ترین ممکنہ مارکیٹ کو راغب کرنا چاہتے ہیں۔ صارفین چاہتے ہیں کہ وہ اپنے لاگ آن معیار کو سپورٹ کرنے کی فکر کیے بغیر خدمات تک رسائی حاصل کر سکیں۔ ایڈاپٹرز بنانا، جیسے کہ ایک ایتھیریم IdP، ہمیں اس رکاوٹ پر قابو پانے میں مدد کر سکتا ہے۔





