Використання Ethereum для автентифікації web2
Вступ
SAML (opens in a new tab) — це стандарт, що використовується у web2, щоб дозволити постачальнику посвідчень (IdP) (opens in a new tab) надавати інформацію про користувача для постачальників послуг (SP) (opens in a new tab).
У цьому посібнику ви дізнаєтеся, як інтегрувати підписи Ethereum із SAML, щоб дозволити користувачам використовувати свої гаманці Ethereum для автентифікації у службах web2, які ще не підтримують Ethereum.
Зверніть увагу, що цей посібник написаний для двох окремих аудиторій:
- Люди з Ethereum, які розуміють Ethereum і потребують вивчення SAML
- Люди з Web2, які розуміють SAML і автентифікацію web2 і потребують вивчення Ethereum
У результаті він міститиме багато вступного матеріалу, який ви вже знаєте. Не соромтеся пропустити його.
SAML для людей з Ethereum
SAML — це централізований протокол. Постачальник послуг (SP) приймає твердження (наприклад, "це мій користувач Іван, він повинен мати дозволи на виконання A, B та C") від постачальника посвідчень (IdP) лише якщо він має попередні довірчі відносини або з ним, або з центром сертифікації (opens in a new tab), який підписав сертифікат цього IdP.
Наприклад, SP може бути туристичною агенцією, що надає туристичні послуги компаніям, а IdP — внутрішнім вебсайтом компанії. Коли співробітникам потрібно забронювати ділову поїздку, туристична агенція відправляє їх для автентифікації компанією, перш ніж дозволити їм фактично забронювати поїздку.
Таким чином три сутності, браузер, SP та IdP, домовляються про доступ. SP не потрібно нічого знати про користувача, що використовує браузер, заздалегідь, лише довіряти IdP.
Ethereum для людей з SAML
Ethereum — це децентралізована система.
Користувачі мають приватний ключ (зазвичай зберігається в розширенні браузера). З приватного ключа ви можете отримати публічний ключ, а з нього — 20-байтну адресу. Коли користувачам потрібно увійти в систему, їм пропонується підписати повідомлення за допомогою nonce (одноразового значення). Сервер може перевірити, чи був підпис створений цією адресою.
Підпис лише підтверджує адресу Ethereum. Щоб отримати інші атрибути користувача, зазвичай використовуються атестації (opens in a new tab). Атестація зазвичай має такі поля:
- Атестатор, адреса, яка зробила атестацію
- Одержувач, адреса, до якої застосовується атестація
- Дані, дані, що засвідчуються, такі як ім'я, дозволи тощо.
- Схема, ідентифікатор схеми, що використовується для інтерпретації даних.
Через децентралізовану природу Ethereum будь-який користувач може робити атестації. Особа атестатора важлива для визначення, які атестації ми вважаємо надійними.
Налаштування
Перший крок — це наявність SAML SP та SAML IdP, що взаємодіють між собою.
-
Завантажте програмне забезпечення. Приклад програмного забезпечення для цієї статті знаходиться на GitHub (opens in a new tab). Різні етапи зберігаються в різних гілках, для цього етапу вам потрібна
saml-only1git 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 -
Перейдіть до SP за URL-адресою http://localhost:3000/ (opens in a new tab) і натисніть кнопку, щоб бути перенаправленим до 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. Це означає, що для того, щоб щось підтвердити (наприклад, "користувач, який надсилає вам цю інформацію, є someone@example.com (opens email client)") постачальнику послуг, нам потрібно використовувати HTTP POST (opens in a new tab) на URL-адресу http://localhost:3000/sp/assertion.
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 }Показати всеПублічні дані для постачальника посвідчень схожі. Він вказує, що для входу користувача ви робите POST-запит на http://localhost:3001/idp/login, а для виходу користувача — POST-запит на http://localhost:3001/idp/logout.
src/sp.mts
Це код, який реалізує постачальника послуг.
1import * as config from "./config.mts"2const fs = await import("fs")3const saml = await import("samlify")Ми використовуємо бібліотеку samlify (opens in a new tab) для реалізації SAML.
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()Router (opens in a new tab) з express (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, постачальники послуг та посвідчень повинні мати свої публічні дані (які називаються метаданими), доступні у форматі XML за адресою /metadata.
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>Привіт ${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). За замовчуванням express ігнорує його, оскільки більшість запитів не потребують цього. Нам це потрібно, оскільки 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 Натисніть тут, щоб увійти7 </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 за допомогою цієї програми express.
src/idp.mts
Це постачальник посвідчень. Він дуже схожий на постачальника послуг, пояснення нижче стосуються тих частин, які відрізняються.
1const xmlParser = new (await import("fast-xml-parser")).XMLParser(2 {3 ignoreAttributes: false, // Зберегти атрибути4 attributeNamePrefix: "@_", // Префікс для атрибутів5 }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 } // Забезпечити підписанняНам потрібен 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 для запитів на вхід3idpRouter.post(`/login`,Це кінцева точка, яка отримує запит на вхід від постачальника послуг. Це обробник для кроку 3 у наведеній вище діаграмі послідовності.
1 async (req, res) => {2 try {3 // Обхідний шлях, оскільки не вдалося змусити parseLoginRequest працювати.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"]))Ми повинні мати можливість використовувати idp.parseLoginRequest (opens in a new tab) для зчитування ідентифікатора запиту на автентифікацію. Однак мені не вдалося змусити його працювати, і не варто було витрачати на це багато часу, тому я просто використовую XML-парсер загального призначення (opens in a new tab). Інформація, яка нам потрібна, це атрибут ID всередині тегу <samlp:AuthnRequest>, який знаходиться на верхньому рівні XML.
Використання підписів Ethereum
Тепер, коли ми можемо надіслати посвідчення користувача постачальнику послуг, наступним кроком є отримання посвідчення користувача надійним способом. Viem дозволяє нам просто запитати у гаманця адресу користувача, але це означає запит інформації у браузера. Ми не контролюємо браузер, тому не можемо автоматично довіряти відповіді, яку отримуємо від нього.
Натомість IdP надсилатиме браузеру рядок для підписання. Якщо гаманець у браузері підписує цей рядок, це означає, що це дійсно та сама адреса (тобто, він знає приватний ключ, що відповідає цій адресі).
Щоб побачити це в дії, зупиніть існуючі IdP та SP і виконайте ці команди:
1git checkout eth-signatures2pnpm install3pnpm startПотім перейдіть до SP (opens in a new tab) і дотримуйтесь інструкцій.
Зверніть увагу, що на цьому етапі ми не знаємо, як отримати адресу електронної пошти з адреси Ethereum, тому замість цього ми повідомляємо <ethereum address>@bad.email.address до SP.
Детальне пояснення
Зміни стосуються кроків 4-5 у попередній діаграмі.
Єдиний файл, який ми змінили, це idp.mts. Ось змінені частини.
1import { v4 as uuidv4 } from 'uuid'2import { verifyMessage } from 'viem'Нам потрібні ці дві додаткові бібліотеки. Ми використовуємо uuid (opens in a new tab) для створення значення nonce (opens in a new tab). Саме значення не має значення, важливий лише той факт, що воно використовується лише один раз.
Бібліотека viem (opens in a new tab) дозволяє нам використовувати визначення Ethereum. Тут нам це потрібно, щоб перевірити, чи дійсно підпис дійсний.
1const loginPrompt = "Щоб отримати доступ до постачальника послуг, підпишіть цей nonce: "Гаманець запитує у користувача дозвіл на підписання повідомлення. Повідомлення, яке є просто nonce, може збити користувачів з пантелику, тому ми включаємо цей запит.
1// Зберігайте requestID тут2let nonces = {}Нам потрібна інформація про запит, щоб мати можливість відповісти на нього. Ми могли б надіслати її разом із запитом (крок 4) і отримати назад (крок 5). Однак ми не можемо довіряти інформації, яку отримуємо від браузера, який перебуває під контролем потенційно ворожого користувача. Тому краще зберігати її тут, з nonce як ключем.
Зверніть увагу, що ми робимо це тут як змінну для простоти. Однак це має кілька недоліків:
- Ми вразливі до атаки типу «відмова в обслуговуванні». Зловмисний користувач може спробувати увійти кілька разів, заповнюючи нашу пам'ять.
- Якщо процес IdP потрібно перезапустити, ми втратимо існуючі значення.
- Ми не можемо балансувати навантаження між кількома процесами, оскільки кожен матиме власну змінну.
У виробничій системі ми б використовували базу даних і реалізували певний механізм закінчення терміну дії.
1const getSignaturePage = requestId => {2 const nonce = uuidv4()3 nonces[nonce] = requestIdСтворіть nonce і збережіть requestId для подальшого використання.
1 return `2<html>3 <head>4 <script type="module">Цей JavaScript виконується автоматично при завантаженні сторінки.
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 запиту та видаліть nonce з 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 })Використовуйте verifyMessage (opens in a new tab), щоб реалізувати крок 5.5 у діаграмі послідовності.
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 );У нас немає фактичної адреси електронної пошти (ми отримаємо її в наступному розділі), тому поки що ми повертаємо адресу Ethereum і чітко позначаємо її як не адресу електронної пошти.
1// Кінцева точка IdP для запитів на вхід2idpRouter.post(`/login`,3 async (req, res) => {4 try {5 // Обхідний шлях, оскільки не вдалося змусити parseLoginRequest працювати.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 тепер використовуйте getSignaturePage в обробнику кроку 3.
Отримання адреси електронної пошти
Наступним кроком є отримання адреси електронної пошти, ідентифікатора, який запитує постачальник послуг. Для цього ми використовуємо Ethereum Attestation Service (EAS) (opens in a new tab).
Найпростіший спосіб отримати атестації — це використовувати GraphQL 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) включає лише адресу електронної пошти. Цей запит просить атестації цієї схеми. Суб'єкт атестації називається одержувачем. Це завжди адреса Ethereum.
Попередження: спосіб, яким ми отримуємо атестації, має дві проблеми з безпекою.
-
Ми звертаємося до кінцевої точки API,
https://optimism.easscan.org/graphql, яка є централізованим компонентом. Ми можемо отримати атрибутid, а потім виконати пошук ончейн, щоб перевірити, чи є атестація справжньою, але кінцева точка API все ще може цензурувати атестації, не повідомляючи нам про них.Цю проблему не неможливо вирішити, ми могли б запустити власну кінцеву точку GraphQL і отримати атестації з журналів ланцюга, але це надмірно для наших цілей.
-
Ми не дивимося на особу атестатора. Будь-хто може надати нам неправдиву інформацію. У реальній реалізації ми б мали набір довірених атестаторів і дивилися б тільки на їхні атестації.
Щоб побачити це в дії, зупиніть існуючі IdP та SP і виконайте ці команди:
1git checkout email-address2pnpm install3pnpm startПотім надайте свою адресу електронної пошти. У вас є два способи зробити це:
-
Імпортуйте гаманець за допомогою приватного ключа та використовуйте тестовий приватний ключ
0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80. -
Додайте атестацію для вашої власної адреси електронної пошти:
-
Перейдіть до схеми в оглядачі атестацій (opens in a new tab).
-
Натисніть Атестувати за схемою.
-
Введіть свою адресу Ethereum як одержувача, свою адресу електронної пошти як адресу електронної пошти та виберіть Ончейн. Потім натисніть Зробити атестацію.
-
Підтвердьте транзакцію у своєму гаманці. Вам знадобиться трохи ETH на блокчейні Optimism (opens in a new tab) для оплати газу.
-
У будь-якому випадку, після цього перейдіть за адресою http://localhost:3000 (opens in a new tab) і дотримуйтесь інструкцій. Якщо ви імпортували тестовий приватний ключ, адреса електронної пошти, яку ви отримаєте, буде test_addr_0@example.com. Якщо ви використовували власну адресу, вона має бути такою, яку ви атестували.
Детальне пояснення
Нові кроки — це комунікація GraphQL, кроки 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)GraphQL надає нам лише непрозорий об'єкт даних з байтами. Щоб зрозуміти його, нам потрібна схема.
1const ethereumAddressToEmail = async ethAddr => {Функція для переходу від адреси Ethereum до адреси електронної пошти.
1 const query = `2 query GetAttestationsByRecipient {Це запит GraphQL.
1 атестації(Ми шукаємо атестації.
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). Це необхідно, оскільки GraphQL чутливий до регістру. "0xBAD060A7", "0xBad060A7", та "0xbad060a7" є різними значеннями.
1 take: 1Незалежно від того, скільки атестацій ми знайдемо, нам потрібна лише перша.
1 ) {2 data3 id4 attester5 }6 }`Поля, які ми хочемо отримати.
attester: Адреса, яка подала атестацію. Зазвичай це використовується для вирішення, чи довіряти атестації.id: Ідентифікатор атестації. Ви можете використовувати це значення для читання атестації ончейн (opens in a new tab), щоб перевірити, чи правильна інформація з запиту GraphQL.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 );Показати всеВикористовуйте нову функцію для отримання адреси електронної пошти.
А як щодо децентралізації?
У цій конфігурації користувачі не можуть видавати себе за когось іншого, доки ми покладаємося на надійних атестаторів для зіставлення адреси Ethereum з адресою електронної пошти. Однак наш постачальник посвідчень все ще є централізованим компонентом. Той, хто має приватний ключ постачальника посвідчень, може надсилати неправдиву інформацію постачальнику послуг.
Можливо, існує рішення з використанням багатосторонніх обчислень (MPC) (opens in a new tab). Сподіваюся написати про це в майбутньому посібнику.
Висновок
Прийняття стандарту входу, такого як підписи Ethereum, стикається з проблемою курки та яйця. Постачальники послуг хочуть залучити якомога ширший ринок. Користувачі хочуть мати доступ до послуг, не турбуючись про підтримку свого стандарту входу. Створення адаптерів, таких як Ethereum IdP, може допомогти нам подолати цю перешкоду.
Більше моїх робіт дивіться тут (opens in a new tab).
Останні оновлення сторінки: 23 листопада 2025 р.





