Перейти до основного контенту

Використання Ethereum для автентифікації web2

web2
автентифікація
eas
Початківець
Ori Pomerantz
30 квітня 2025 р.
18 читається за хвилину

Вступ

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 — внутрішнім вебсайтом компанії. Коли співробітникам потрібно забронювати ділову поїздку, туристична агенція відправляє їх для автентифікації компанією, перш ніж дозволити їм фактично забронювати поїздку.

Покроковий процес SAML

Таким чином три сутності, браузер, SP та IdP, домовляються про доступ. SP не потрібно нічого знати про користувача, що використовує браузер, заздалегідь, лише довіряти IdP.

Ethereum для людей з SAML

Ethereum — це децентралізована система.

Вхід в Ethereum

Користувачі мають приватний ключ (зазвичай зберігається в розширенні браузера). З приватного ключа ви можете отримати публічний ключ, а з нього — 20-байтну адресу. Коли користувачам потрібно увійти в систему, їм пропонується підписати повідомлення за допомогою nonce (одноразового значення). Сервер може перевірити, чи був підпис створений цією адресою.

Отримання додаткових даних з атестацій

Підпис лише підтверджує адресу Ethereum. Щоб отримати інші атрибути користувача, зазвичай використовуються атестації (opens in a new tab). Атестація зазвичай має такі поля:

  • Атестатор, адреса, яка зробила атестацію
  • Одержувач, адреса, до якої застосовується атестація
  • Дані, дані, що засвідчуються, такі як ім'я, дозволи тощо.
  • Схема, ідентифікатор схеми, що використовується для інтерпретації даних.

Через децентралізовану природу Ethereum будь-який користувач може робити атестації. Особа атестатора важлива для визначення, які атестації ми вважаємо надійними.

Налаштування

Перший крок — це наявність SAML SP та SAML IdP, що взаємодіють між собою.

  1. Завантажте програмне забезпечення. Приклад програмного забезпечення для цієї статті знаходиться на GitHub (opens in a new tab). Різні етапи зберігаються в різних гілках, для цього етапу вам потрібна saml-only

    1git clone https://github.com/qbzzt/250420-saml-ethereum -b saml-only
    2cd 250420-saml-ethereum
    3pnpm install
  2. Створіть ключі з самопідписаними сертифікатами. Це означає, що ключ є власним центром сертифікації, і його потрібно імпортувати вручну до постачальника послуг. Дивіться документацію OpenSSL (opens in a new tab) для отримання додаткової інформації.

    1mkdir keys
    2cd keys
    3openssl 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 ..
  3. Запустіть сервери (і SP, і IdP)

    1pnpm start
  4. Перейдіть до SP за URL-адресою http://localhost:3000/ (opens in a new tab) і натисніть кнопку, щоб бути перенаправленим до IdP (порт 3001).

  5. Надайте IdP свою адресу електронної пошти та натисніть Увійти до постачальника послуг. Переконайтеся, що вас перенаправили назад до постачальника послуг (порт 3000) і що він розпізнає вас за вашою адресою електронної пошти.

Детальне пояснення

Ось що відбувається крок за кроком:

Звичайний вхід SAML без Ethereum

src/config.mts

Цей файл містить конфігурацію як для постачальника посвідчень, так і для постачальника послуг. Зазвичай це дві різні сутності, але тут ми можемо спільно використовувати код для простоти.

1const fs = await import("fs")
2
3const 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 = 3000
2export const spHostname = "localhost"
3export const spDir = "sp"
4
5export const idpPort = 3001
6export const idpHostname = "localhost"
7export const idpDir = "idp"
8
9export 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")).default
2const 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()
2
3const sp = saml.ServiceProvider({
4 privateKey: spPrivateKey,
5 ...config.spPublicData
6})

Власне представлення постачальника послуг — це всі публічні дані та приватний ключ, який він використовує для підписання інформації.

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)
7
8app.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>
Показати все

Ми надсилаємо постачальнику послуг два поля:

  1. requestId, на який ми відповідаємо.
  2. Ідентифікатор користувача (зараз ми використовуємо адресу електронної пошти, яку надає користувач).
1 </form>
2 </body>
3</html>
4
5const idpRouter = express.Router()
6
7idpRouter.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.requestId
4 }
5 },

Інформація, витягнута із запиту. Єдиний параметр, який нас цікавить у запиті, це requestId, який дозволяє постачальнику послуг зіставляти запити та їх відповіді.

1 signingKey: { privateKey: idpPrivateKey, publicKey: config.idpCert } // Забезпечити підписання

Нам потрібен signingKey, щоб мати дані для підписання відповіді. Постачальник послуг не довіряє непідписаним запитам.

1 },
2 "post",
3 {
4 email: req.body.email

Це поле з інформацією про користувача, яку ми надсилаємо назад постачальнику послуг.

1 }
2 );
3
4 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 у наведеній вище діаграмі послідовності.

1
2// Кінцева точка 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-signatures
2pnpm install
3pnpm start

Потім перейдіть до SP (opens in a new tab) і дотримуйтесь інструкцій.

Зверніть увагу, що на цьому етапі ми не знаємо, як отримати адресу електронної пошти з адреси Ethereum, тому замість цього ми повідомляємо <ethereum address>@bad.email.address до SP.

Детальне пояснення

Зміни стосуються кроків 4-5 у попередній діаграмі.

SAML з підписом Ethereum

Єдиний файл, який ми змінили, це 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 + "/" + signature
3 window.location.href = path
4 })
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 = path
6 }

Підпис надсилається назад браузером, який є потенційно зловмисним (ніщо не заважає вам просто відкрити 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.signature
5 })

Використовуйте 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: 1
8 ) {
9 data
10 id
11 attester
12 }
13}
Показати все

Цей schemaId (opens in a new tab) включає лише адресу електронної пошти. Цей запит просить атестації цієї схеми. Суб'єкт атестації називається одержувачем. Це завжди адреса Ethereum.

Попередження: спосіб, яким ми отримуємо атестації, має дві проблеми з безпекою.

  • Ми звертаємося до кінцевої точки API, https://optimism.easscan.org/graphql, яка є централізованим компонентом. Ми можемо отримати атрибут id, а потім виконати пошук ончейн, щоб перевірити, чи є атестація справжньою, але кінцева точка API все ще може цензурувати атестації, не повідомляючи нам про них.

    Цю проблему не неможливо вирішити, ми могли б запустити власну кінцеву точку GraphQL і отримати атестації з журналів ланцюга, але це надмірно для наших цілей.

  • Ми не дивимося на особу атестатора. Будь-хто може надати нам неправдиву інформацію. У реальній реалізації ми б мали набір довірених атестаторів і дивилися б тільки на їхні атестації.

Щоб побачити це в дії, зупиніть існуючі IdP та SP і виконайте ці команди:

1git checkout email-address
2pnpm install
3pnpm start

Потім надайте свою адресу електронної пошти. У вас є два способи зробити це:

  • Імпортуйте гаманець за допомогою приватного ключа та використовуйте тестовий приватний ключ 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80.

  • Додайте атестацію для вашої власної адреси електронної пошти:

    1. Перейдіть до схеми в оглядачі атестацій (opens in a new tab).

    2. Натисніть Атестувати за схемою.

    3. Введіть свою адресу Ethereum як одержувача, свою адресу електронної пошти як адресу електронної пошти та виберіть Ончейн. Потім натисніть Зробити атестацію.

    4. Підтвердьте транзакцію у своєму гаманці. Вам знадобиться трохи ETH на блокчейні Optimism (opens in a new tab) для оплати газу.

У будь-якому випадку, після цього перейдіть за адресою http://localhost:3000 (opens in a new tab) і дотримуйтесь інструкцій. Якщо ви імпортували тестовий приватний ключ, адреса електронної пошти, яку ви отримаєте, буде test_addr_0@example.com. Якщо ви використовували власну адресу, вона має бути такою, яку ви атестували.

Детальне пояснення

Перехід від адреси Ethereum до електронної пошти

Нові кроки — це комунікація 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 data
3 id
4 attester
5 }
6 }`

Поля, які ми хочемо отримати.

  • attester: Адреса, яка подала атестацію. Зазвичай це використовується для вирішення, чи довіряти атестації.
  • id: Ідентифікатор атестації. Ви можете використовувати це значення для читання атестації ончейн (opens in a new tab), щоб перевірити, чи правильна інформація з запиту GraphQL.
  • data: Дані схеми (у цьому випадку адреса електронної пошти).
1 const queryResult = await graphqlClient.request(query)
2
3 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.value
3}

Якщо є значення, використовуйте 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 р.

Чи була ця інструкція корисною?