Использование 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) принимает утверждения (например, "это мой пользователь John, у него должны быть разрешения на выполнение 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 -
Перейдите по URL-адресу SP http://localhost:3000/ (opens in a new tab) и нажмите кнопку для перенаправления на IdP (порт 3001).
-
Укажите свой адрес электронной почты в IdP и нажмите Login to the service provider. Убедитесь, что вы были перенаправлены обратно к поставщику услуг (порт 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)") поставщику услуг нам нужно использовать 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, а для выхода — на 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>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}">Отправьте POST-запрос на 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) для чтения ID запроса на аутентификацию. Однако мне не удалось заставить его работать, и не стоило тратить на это много времени, поэтому я просто использую 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, поэтому вместо этого мы сообщаем SP <адрес Ethereum>@bad.email.address.
Подробное объяснение
Изменения касаются шагов 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// Храните здесь requestID2let 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>/<адрес>/<подпись>. Это шаг 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) включает только адрес электронной почты. Этот запрос запрашивает аттестации этой схемы. Субъект аттестации называется recipient. Это всегда адрес Ethereum.
Внимание: способ получения аттестаций здесь имеет две проблемы с безопасностью.
-
Мы обращаемся к конечной точке API,
https://optimism.easscan.org/graphql, которая является централизованным компонентом. Мы можем получить атрибутid, а затем выполнить поиск он-чейн, чтобы проверить, что аттестация реальна, но конечная точка API все еще может подвергать цензуре аттестации, не сообщая нам о них.Эту проблему можно решить: мы могли бы запустить собственную конечную точку GraphQL и получать аттестации из логов цепи, но это излишне для наших целей.
-
Мы не смотрим на личность аттестующего. Любой может предоставить нам ложную информацию. В реальной реализации у нас был бы набор доверенных аттестующих, и мы бы рассматривали только их аттестации.
Чтобы увидеть это в действии, остановите существующие IdP и SP и выполните следующие команды:
1git checkout email-address2pnpm install3pnpm startЗатем укажите свой адрес электронной почты. У вас есть два способа сделать это:
-
Импортируйте кошелек, используя приватный ключ, и используйте тестовый приватный ключ
0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80. -
Добавьте аттестацию для своего собственного адреса электронной почты:
-
Перейдите к схеме в обозревателе аттестаций (opens in a new tab).
-
Нажмите Attest with Schema.
-
Введите свой адрес Ethereum в качестве получателя, свой адрес электронной почты в качестве адреса электронной почты и выберите Onchain. Затем нажмите Make Attestation.
-
Одобрите транзакцию в своем кошельке. Вам понадобится немного 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 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). Это необходимо, поскольку 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 г.





