Přeskočit na hlavní obsah

Použití Etherea pro web2 autentizaci

web2
ověření
eas
Začátečník
Ori Pomerantz
30. dubna 2025
18 minuta čtení

Úvod

SAML (opens in a new tab) je standard používaný na web2, který umožňuje poskytovateli identity (IdP) (opens in a new tab) poskytovat informace o uživateli poskytovatelům služeb (SP) (opens in a new tab).

V tomto tutoriálu se dozvíte, jak integrovat podpisy Etherea se SAML, abyste uživatelům umožnili používat jejich peněženky Ethereum k vlastní autentizaci u služeb web2, které ještě nativně nepodporují Ethereum.

Upozorňujeme, že tento tutoriál je napsán pro dvě samostatné skupiny čtenářů:

  • Lidi z komunity Etherea, kteří Ethereu rozumí a potřebují se naučit SAML
  • Lidi z web2, kteří rozumí SAML a web2 autentizaci a potřebují se naučit o Ethereu

V důsledku toho bude obsahovat spoustu úvodního materiálu, který již znáte. Klidně ho přeskočte.

SAML pro lidi z komunity Etherea

SAML je centralizovaný protokol. Poskytovatel služeb (SP) přijímá tvrzení (například „toto je můj uživatel John, měl by mít oprávnění provádět A, B a C“) od poskytovatele identity (IdP) pouze pokud s ním má již existující vztah důvěry, nebo s certifikační autoritou (opens in a new tab), která podepsala certifikát daného IdP.

Například SP může být cestovní kancelář poskytující cestovní služby firmám a IdP může být interní webová stránka firmy. Když si zaměstnanci potřebují rezervovat služební cestu, cestovní kancelář je odešle k autentizaci firmou, než jim skutečně umožní cestu rezervovat.

Proces SAML krok za krokem

Tímto způsobem si tři entity, prohlížeč, SP a IdP, vyjednávají přístup. SP nemusí předem vědět nic o uživateli používajícím prohlížeč, stačí, když důvěřuje IdP.

Ethereum pro lidi znalé SAML

Ethereum je decentralizovaný systém.

Přihlášení přes Ethereum

Uživatelé mají privátní klíč (obvykle uložený v rozšíření prohlížeče). Z privátního klíče můžete odvodit veřejný klíč a z něj 20bajtovou adresu. Když se uživatelé potřebují přihlásit do systému, jsou požádáni o podepsání zprávy s hodnotou nonce (jednorázově použitelná hodnota). Server může ověřit, že podpis byl vytvořen touto adresou.

Získávání dodatečných dat z atestací

Podpis pouze ověřuje adresu Etherea. Chcete-li získat další atributy uživatele, obvykle používáte atestace (opens in a new tab). Atestace má obvykle tato pole:

  • Atestátor, adresa, která provedla atestaci
  • Příjemce, adresa, které se atestace týká
  • Data, atestovaná data, jako je jméno, oprávnění atd.
  • Schéma, ID schématu použitého k interpretaci dat.

Vzhledem k decentralizované povaze Etherea může atestace provádět kterýkoli uživatel. Identita atestátora je důležitá pro identifikaci atestací, které považujeme za spolehlivé.

Nastavení

Prvním krokem je zajistit komunikaci mezi SAML SP a SAML IdP.

  1. Stáhněte si software. Vzorový software pro tento článek je na GitHubu (opens in a new tab). Různé fáze jsou uloženy v různých větvích, pro tuto fázi chcete saml-only

    1git clone https://github.com/qbzzt/250420-saml-ethereum -b saml-only
    2cd 250420-saml-ethereum
    3pnpm install
  2. Vytvořte klíče s certifikáty s vlastním podpisem (self-signed). To znamená, že klíč je svou vlastní certifikační autoritou a je třeba jej ručně importovat do poskytovatele služeb. Více informací naleznete v dokumentaci 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. Spusťte servery (SP i IdP)

    1pnpm start
  4. Přejděte na adresu URL SP http://localhost:3000/ (opens in a new tab) a kliknutím na tlačítko budete přesměrováni na IdP (port 3001).

  5. Poskytněte IdP svou e-mailovou adresu a klikněte na Přihlásit se k poskytovateli služeb. Uvidíte, že budete přesměrováni zpět k poskytovateli služeb (port 3000) a že vás pozná podle vaší e-mailové adresy.

Podrobné vysvětlení

Toto se děje krok za krokem:

Běžné přihlášení SAML bez Etherea

src/config.mts

Tento soubor obsahuje konfiguraci jak pro poskytovatele identity, tak pro poskytovatele služeb. Obvykle by se jednalo o různé entity, ale zde můžeme pro zjednodušení sdílet kód.

1const fs = await import("fs")
2
3const protocol="http"

Zatím jen testujeme, takže je v pořádku používat HTTP.

1export const spCert = fs.readFileSync("keys/saml-sp.crt").toString()
2export const idpCert = fs.readFileSync("keys/saml-idp.crt").toString()

Načtení veřejných klíčů, které jsou normálně dostupné oběma komponentám (a jsou buď přímo důvěryhodné, nebo podepsané důvěryhodnou certifikační autoritou).

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}`
Zobrazit vše

Adresy URL pro obě komponenty.

1export const spPublicData = {

Veřejná data pro poskytovatele služeb.

1 entityID: `${spUrl}/metadata`,

Podle konvence je v SAML entityID adresa URL, na které jsou dostupná metadata entity. Tato metadata odpovídají zdejším veřejným datům, s výjimkou toho, že jsou ve formátu 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 }
Zobrazit vše

Nejdůležitější definicí pro naše účely je assertionConsumerServer. To znamená, že abychom poskytovateli služeb mohli něco potvrdit (například, že „uživatel, který vám posílá tyto informace, je nekdo@example.com (opens email client)“), musíme použít HTTP POST (opens in a new tab) na 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 }
Zobrazit vše

Veřejná data pro poskytovatele identity jsou podobná. Určuje, že pro přihlášení uživatele je třeba odeslat POST na http://localhost:3001/idp/login a pro odhlášení uživatele POST na http://localhost:3001/idp/logout.

src/sp.mts

Toto je kód, který implementuje poskytovatele služeb.

1import * as config from "./config.mts"
2const fs = await import("fs")
3const saml = await import("samlify")

K implementaci SAML používáme knihovnu samlify (opens in a new tab).

1import * as validator from "@authenio/samlify-node-xmllint"
2saml.setSchemaValidator(validator)

Knihovna samlify očekává balíček, který ověří, že XML je správné, podepsané očekávaným veřejným klíčem atd. Pro tento účel používáme @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) v expressu (opens in a new tab) je „mini webová stránka“, kterou lze připojit k webové stránce. V tomto případě jej používáme ke seskupení všech definic poskytovatele služeb dohromady.

1const spPrivateKey = fs.readFileSync("keys/saml-sp.pem").toString()
2
3const sp = saml.ServiceProvider({
4 privateKey: spPrivateKey,
5 ...config.spPublicData
6})

Vlastní reprezentace poskytovatele služeb se skládá ze všech veřejných dat a privátního klíče, který používá k podepisování informací.

1const idp = saml.IdentityProvider(config.idpPublicData);

Veřejná data obsahují vše, co poskytovatel služeb potřebuje vědět o poskytovateli identity.

1spRouter.get(`/metadata`,
2 (req, res) => res.header("Content-Type", "text/xml").send(sp.getMetadata())
3)

Pro zajištění interoperability s ostatními komponentami SAML by poskytovatelé služeb a identity měli mít svá veřejná data (nazývaná metadata) dostupná ve formátu XML na adrese /metadata.

1spRouter.post(`/assertion`,

Toto je stránka, na kterou prohlížeč přistupuje, aby se identifikoval. Tvrzení obsahuje identifikátor uživatele (zde používáme e-mailovou adresu) a může obsahovat další atributy. Toto je obslužný program pro krok 7 ve výše uvedeném sekvenčním diagramu.

1 async (req, res) => {
2 // console.log(`SAML response:\n${Buffer.from(req.body.SAMLResponse, 'base64').toString('utf-8')}`)

Pomocí zakomentovaného příkazu můžete zobrazit data XML poskytnutá v tvrzení. Je kódován v base64 (opens in a new tab).

1 try {
2 const loginResponse = await sp.parseLoginResponse(idp, 'post', req);

Zpracujte požadavek na přihlášení od serveru identity.

1 res.send(`
2 <html>
3 <body>
4 <h2>Dobrý den, ${loginResponse.extract.nameID}</h2>
5 </body>
6 </html>
7 `)
8 res.send();

Odešlete odpověď HTML, abyste uživateli ukázali, že jsme obdrželi přihlášení.

1 } catch (err) {
2 console.error('Chyba při zpracování odpovědi SAML:', err);
3 res.status(400).send('Ověření SAML se nezdařilo');
4 }
5 }
6)

V případě selhání informujte uživatele.

1spRouter.get('/login',

Vytvořte požadavek na přihlášení, když se prohlížeč pokusí o přístup na tuto stránku. Toto je obslužný program pro krok 1 ve výše uvedeném sekvenčním diagramu.

1 async (req, res) => {
2 const loginRequest = await sp.createLoginRequest(idp, "post")

Získejte informace pro odeslání požadavku na přihlášení.

1 res.send(`
2 <html>
3 <body>
4 <script>
5 window.onload = function () { document.forms[0].submit(); }
6 </script>

Tato stránka automaticky odešle formulář (viz níže). Díky tomu uživatel nemusí pro přesměrování nic dělat. Toto je krok 2 ve výše uvedeném sekvenčním diagramu.

1 <form method="post" action="${loginRequest.entityEndpoint}">

Odešlete POST na loginRequest.entityEndpoint (adresa URL koncového bodu poskytovatele identity).

1 <input type="hidden" name="${loginRequest.type}" value="${loginRequest.context}" />

Název vstupu je loginRequest.type (SAMLRequest). Obsah tohoto pole je loginRequest.context, což je opět XML kódované v base64.

1 </form>
2 </body>
3 </html>
4 `)
5 }
6)
7
8app.use(express.urlencoded({extended: true}))

Tento middleware (opens in a new tab) čte tělo požadavku HTTP (opens in a new tab). Ve výchozím nastavení ho Express ignoruje, protože většina požadavků ho nevyžaduje. Potřebujeme ho, protože POST používá tělo.

1app.use(`/${config.spDir}`, spRouter)

Připojte router do adresáře poskytovatele služeb (/sp).

1app.get("/", (req, res) => {
2 res.send(`
3 <html>
4 <body>
5 <button onClick="document.location.href='${config.spUrl}/login'">
6 Klikněte zde pro přihlášení
7 </button>
8 </body>
9 </html>
10 `)
11})
Zobrazit vše

Pokud se prohlížeč pokusí získat kořenový adresář, poskytněte mu odkaz na přihlašovací stránku.

1app.listen(config.spPort, () => {
2 console.log(`poskytovatel služeb běží na http://${config.spHostname}:${config.spPort}`)
3})

Naslouchejte na portu spPort pomocí této aplikace Express.

src/idp.mts

Toto je poskytovatel identity. Je velmi podobný poskytovateli služeb, níže uvedená vysvětlení se týkají částí, které se liší.

1const xmlParser = new (await import("fast-xml-parser")).XMLParser(
2 {
3 ignoreAttributes: false, // Zachovat atributy
4 attributeNamePrefix: "@_", // Prefix pro atributy
5 }
6)

Musíme si přečíst a porozumět požadavku XML, který obdržíme od poskytovatele služeb.

1const getLoginPage = requestId => `

Tato funkce vytváří stránku s automaticky odesílaným formulářem, který je vrácen v kroku 4 výše uvedeného sekvenčního diagramu.

1<html>
2 <head>
3 <title>Přihlašovací stránka</title>
4 </head>
5 <body>
6 <h2>Přihlašovací stránka</h2>
7 <form method="post" action="./loginSubmitted">
8 <input type="hidden" name="requestId" value="${requestId}" />
9 E-mailová adresa: <input name="email" />
10 <br />
11 <button type="Submit">
12 Přihlásit se k poskytovateli služeb
13 </button>
Zobrazit vše

Poskytovateli služeb posíláme dvě pole:

  1. requestId, na které odpovídáme.
  2. Identifikátor uživatele (prozatím používáme e-mailovou adresu, kterou uživatel poskytne).
1 </form>
2 </body>
3</html>
4
5const idpRouter = express.Router()
6
7idpRouter.post("/loginSubmitted", async (req, res) => {
8 const loginResponse = await idp.createLoginResponse(

Toto je obslužný program pro krok 5 ve výše uvedeném sekvenčním diagramu. idp.createLoginResponse (opens in a new tab) vytváří odpověď na přihlášení.

1 sp,
2 {
3 authnContextClassRef: 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport',
4 audience: sp.entityID,

Cílovou skupinou je poskytovatel služeb.

1 extract: {
2 request: {
3 id: req.body.requestId
4 }
5 },

Informace extrahované z požadavku. Jediný parametr, který nás v požadavku zajímá, je requestId, který umožňuje poskytovateli služeb párovat požadavky a jejich odpovědi.

1 signingKey: { privateKey: idpPrivateKey, publicKey: config.idpCert } // Zajistit podepsání

Potřebujeme signingKey, abychom měli data k podepsání odpovědi. Poskytovatel služeb nedůvěřuje nepodepsaným požadavkům.

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

Toto je pole s informacemi o uživateli, které posíláme zpět poskytovateli služeb.

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})
Zobrazit vše

Opět použijte automaticky odesílaný formulář. Toto je krok 6 ve výše uvedeném sekvenčním diagramu.

1
2// Koncový bod IdP pro požadavky na přihlášení
3idpRouter.post(`/login`,

Toto je koncový bod, který přijímá požadavek na přihlášení od poskytovatele služeb. Toto je obslužný program pro krok 3 ve výše uvedeném sekvenčním diagramu.

1 async (req, res) => {
2 try {
3 // Dočasné řešení, protože se mi nepodařilo zprovoznit 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"]))

Měli bychom být schopni použít idp.parseLoginRequest (opens in a new tab) ke čtení ID požadavku na ověření. Nepodařilo se mi to však zprovoznit a nestálo za to tím trávit spoustu času, tak jsem prostě použil univerzální XML parser (opens in a new tab). Informace, kterou potřebujeme, je atribut ID uvnitř značky <samlp:AuthnRequest>, která je na nejvyšší úrovni XML.

Použití podpisů Etherea

Nyní, když můžeme odeslat identitu uživatele poskytovateli služeb, dalším krokem je získat identitu uživatele důvěryhodným způsobem. Viem nám umožňuje jednoduše požádat peněženku o adresu uživatele, ale to znamená žádat o informace prohlížeč. Prohlížeč nemáme pod kontrolou, takže nemůžeme automaticky důvěřovat odpovědi, kterou od něj dostaneme.

Místo toho IdP pošle prohlížeči řetězec k podepsání. Pokud peněženka v prohlížeči tento řetězec podepíše, znamená to, že se skutečně jedná o tuto adresu (tj. zná privátní klíč, který adrese odpovídá).

Chcete-li to vidět v akci, zastavte stávající IdP a SP a spusťte tyto příkazy:

1git checkout eth-signatures
2pnpm install
3pnpm start

Poté přejděte na SP (opens in a new tab) a postupujte podle pokynů.

Všimněte si, že v tomto okamžiku nevíme, jak získat e-mailovou adresu z adresy Etherea, takže místo toho hlásíme SP <adresa etherea>@bad.email.address.

Podrobné vysvětlení

Změny jsou v krocích 4–5 v předchozím diagramu.

SAML s podpisem Etherea

Jediný soubor, který jsme změnili, je idp.mts. Zde jsou změněné části.

1import { v4 as uuidv4 } from 'uuid'
2import { verifyMessage } from 'viem'

Potřebujeme tyto dvě další knihovny. K vytvoření hodnoty nonce (opens in a new tab) používáme uuid (opens in a new tab). Na samotné hodnotě nezáleží, jen na tom, že je použita pouze jednou.

Knihovna viem (opens in a new tab) nám umožňuje používat definice Etherea. Zde ji potřebujeme k ověření, že podpis je skutečně platný.

1const loginPrompt = "Pro přístup k poskytovateli služeb podepište tuto hodnotu nonce: "

Peněženka požádá uživatele o povolení podepsat zprávu. Zpráva, která je pouze hodnotou nonce, by mohla uživatele zmást, proto uvádíme tuto výzvu.

1// Zde si ponechte requestID
2let nonces = {}

Potřebujeme informace o požadavku, abychom na něj mohli odpovědět. Mohli bychom ho poslat s požadavkem (krok 4) a přijmout ho zpět (krok 5). Nemůžeme však důvěřovat informacím, které získáváme z prohlížeče, který je pod kontrolou potenciálně nepřátelského uživatele. Je tedy lepší jej uložit zde s hodnotou nonce jako klíčem.

Všimněte si, že pro zjednodušení to zde děláme jako proměnnou. To má však několik nevýhod:

  • Jsme zranitelní vůči útoku typu odepření služby (denial of service). Uživatel se zlými úmysly by se mohl pokusit přihlásit vícekrát a zaplnit tak naši paměť.
  • Pokud je třeba proces IdP restartovat, ztratíme stávající hodnoty.
  • Nemůžeme rozkládat zátěž mezi více procesů, protože každý by měl svou vlastní proměnnou.

Na produkčním systému bychom použili databázi a implementovali nějaký mechanismus pro vypršení platnosti.

1const getSignaturePage = requestId => {
2 const nonce = uuidv4()
3 nonces[nonce] = requestId

Vytvořte hodnotu nonce a uložte requestId pro budoucí použití.

1 return `
2<html>
3 <head>
4 <script type="module">

Tento JavaScript se spustí automaticky po načtení stránky.

1 import { createWalletClient, custom, getAddress } from 'https://esm.sh/viem'

Potřebujeme několik funkcí z viem.

1 if (!window.ethereum) {
2 alert("Nainstalujte si prosím MetaMask nebo kompatibilní peněženku a poté stránku znovu načtěte")
3 }

Pracovat můžeme pouze v případě, že je v prohlížeči peněženka.

1 const [account] = await window.ethereum.request({method: 'eth_requestAccounts'})

Požádejte o seznam účtů z peněženky (window.ethereum). Předpokládejte, že existuje alespoň jeden, a uložte pouze ten první.

1 const walletClient = createWalletClient({
2 account,
3 transport: custom(window.ethereum)
4 })

Vytvořte klienta peněženky (opens in a new tab) pro interakci s peněženkou prohlížeče.

1 window.goodSignature = () => {
2 walletClient.signMessage({
3 message: "${loginPrompt}${nonce}"

Požádejte uživatele o podepsání zprávy. Protože celé toto HTML je v řetězci šablony (opens in a new tab), můžeme použít proměnné definované v procesu idp. Toto je krok 4.5 v sekvenčním diagramu.

1 }).then(signature => {
2 const path= "/${config.idpDir}/signature/${nonce}/" + account + "/" + signature
3 window.location.href = path
4 })
5 }

Přesměrování na /idp/signature/<nonce>/<adresa>/<podpis>. Toto je krok 5 v sekvenčním diagramu.

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 }

Podpis je odeslán zpět prohlížečem, který může být potenciálně škodlivý (nic vám nebrání v tom, abyste v prohlížeči otevřeli adresu http://localhost:3001/idp/signature/bad-nonce/bad-address/bad-signature). Proto je důležité ověřit, že proces IdP správně zpracovává špatné podpisy.

1 </script>
2 </head>
3 <body>
4 <h2>Prosím podepište</h2>
5 <button onClick="window.goodSignature()">
6 Odeslat dobrý (platný) podpis
7 </button>
8 <br/>
9 <button onClick="window.badSignature()">
10 Odeslat špatný (neplatný) podpis
11 </button>
12 </body>
13</html>
14`
15}
Zobrazit vše

Zbytek je jen standardní HTML.

1idpRouter.get("/signature/:nonce/:account/:signature", async (req, res) => {

Toto je obslužný program pro krok 5 v sekvenčním diagramu.

1 const requestId = nonces[req.params.nonce]
2 if (requestId === undefined) {
3 res.send("Špatná hodnota nonce")
4 return ;
5 }
6
7 nonces[req.params.nonce] = undefined

Získejte ID požadavku a odstraňte hodnotu nonce z nonces, abyste se ujistili, že ji nelze znovu použít.

1 try {

Protože existuje mnoho způsobů, jak může být podpis neplatný, zabalíme to do bloku try ... catch, který zachytí jakékoli vyvolané chyby.

1 const validSignature = await verifyMessage({
2 address: req.params.account,
3 message: `${loginPrompt}${req.params.nonce}`,
4 signature: req.params.signature
5 })

Použijte verifyMessage (opens in a new tab) k implementaci kroku 5.5 v sekvenčním diagramu.

1 if (!validSignature)
2 throw("Špatný podpis")
3 } catch (err) {
4 res.send("Chyba:" + err)
5 return ;
6 }

Zbytek obslužného programu je ekvivalentní tomu, co jsme dělali v obslužném programu /loginSubmitted dříve, s výjimkou jedné malé změny.

1 const loginResponse = await idp.createLoginResponse(
2 .
3 .
4 .
5 {
6 email: req.params.account + "@bad.email.address"
7 }
8 );

Nemáme skutečnou e-mailovou adresu (získáme ji v další části), takže prozatím vracíme adresu Etherea a jasně ji označujeme jako e-mailovou adresu.

1// Koncový bod IdP pro požadavky na přihlášení
2idpRouter.post(`/login`,
3 async (req, res) => {
4 try {
5 // Dočasné řešení, protože se mi nepodařilo zprovoznit 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('Chyba při zpracování odpovědi SAML:', err);
11 res.status(400).send('Ověření SAML se nezdařilo');
12 }
13 }
14)
Zobrazit vše

Místo getLoginPage nyní v obslužném programu kroku 3 použijte getSignaturePage.

Získání e-mailové adresy

Dalším krokem je získání e-mailové adresy, identifikátoru požadovaného poskytovatelem služeb. K tomu používáme službu Ethereum Attestation Service (EAS) (opens in a new tab).

Nejjednodušší způsob, jak získat atestace, je použít GraphQL API (opens in a new tab). Používáme tento dotaz:

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}
Zobrazit vše

Toto schemaId (opens in a new tab) obsahuje pouze e-mailovou adresu. Tento dotaz žádá o atestace tohoto schématu. Subjekt atestace se nazývá recipient. Je to vždy adresa Etherea.

Varování: Způsob, jakým zde získáváme atestace, má dva bezpečnostní problémy.

  • Přistupujeme ke koncovému bodu API https://optimism.easscan.org/graphql, což je centralizovaná komponenta. Můžeme získat atribut id a poté provést ověření na blockchainu, abychom ověřili, že je atestace skutečná, ale koncový bod API může stále cenzurovat atestace tím, že nám o nich neřekne.

    Tento problém není neřešitelný, mohli bychom spustit vlastní koncový bod GraphQL a získat atestace z protokolů řetězce, ale to je pro naše účely zbytečné.

  • Nebereme v úvahu identitu atestátora. Kdokoli nám může poskytnout nepravdivé informace. V reálné implementaci bychom měli sadu důvěryhodných atestátorů a zabývali bychom se pouze jejich atestacemi.

Chcete-li to vidět v akci, zastavte stávající IdP a SP a spusťte tyto příkazy:

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

Poté zadejte svou e-mailovou adresu. Máte dva způsoby, jak to udělat:

  • Importujte peněženku pomocí privátního klíče a použijte testovací privátní klíč 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80.

  • Přidejte atestaci pro svou vlastní e-mailovou adresu:

    1. Přejděte na schéma v průzkumníku atestací (opens in a new tab).

    2. Klikněte na Atestovat se schématem.

    3. Zadejte svou adresu Etherea jako příjemce, svou e-mailovou adresu jako e-mailovou adresu a vyberte Onchain. Poté klikněte na Provést atestaci.

    4. Schvalte transakci ve své peněžence. K zaplacení za palivo budete potřebovat nějaké ETH na blockchainu Optimism (opens in a new tab).

Ať tak či onak, poté přejděte na http://localhost:3000 (opens in a new tab) a postupujte podle pokynů. Pokud jste importovali testovací privátní klíč, e-mail, který obdržíte, je test_addr_0@example.com. Pokud jste použili vlastní adresu, měla by to být ta, kterou jste atestovali.

Podrobné vysvětlení

Získání e-mailu z adresy Etherea

Nové kroky jsou komunikace GraphQL, kroky 5.6 a 5.7.

Opět zde jsou změněné části idp.mts.

1import { GraphQLClient } from 'graphql-request'
2import { SchemaEncoder } from '@ethereum-attestation-service/eas-sdk'

Importujte knihovny, které potřebujeme.

1const graphqlEndpointUrl = "https://optimism.easscan.org/graphql"

Pro každý blockchain existuje samostatný koncový bod (opens in a new tab).

1const graphqlClient = new GraphQLClient(graphqlEndpointUrl, { fetch })

Vytvořte nového klienta GraphQLClient, kterého můžeme použít pro dotazování koncového bodu.

1const graphqlSchema = 'string emailAddress'
2const graphqlEncoder = new SchemaEncoder(graphqlSchema)

GraphQL nám poskytuje pouze neprůhledný datový objekt s bajty. Abychom mu porozuměli, potřebujeme schéma.

1const ethereumAddressToEmail = async ethAddr => {

Funkce pro získání e-mailové adresy z adresy Etherea.

1 const query = `
2 query GetAttestationsByRecipient {

Toto je dotaz GraphQL.

1 attestations(

Hledáme atestace.

1 where: {
2 recipient: { equals: "${getAddress(ethAddr)}" }
3 schemaId: { equals: "0xfa2eff59a916e3cc3246f9aec5e0ca00874ae9d09e4678e5016006f07622f977" }
4 }

Atestace, které chceme, jsou ty v našem schématu, kde je příjemce getAddress(ethAddr). Funkce getAddress (opens in a new tab) zajišťuje, že naše adresa má správný kontrolní součet (opens in a new tab). To je nutné, protože GraphQL rozlišuje velikost písmen. „0xBAD060A7“, „0xBad060A7“ a „0xbad060a7“ jsou různé hodnoty.

1 take: 1

Bez ohledu na to, kolik atestací najdeme, chceme pouze tu první.

1 ) {
2 data
3 id
4 attester
5 }
6 }`

Pole, která chceme obdržet.

  • attester: Adresa, která atestaci odeslala. Obvykle se používá k rozhodnutí, zda atestaci důvěřovat, či nikoli.
  • id: ID atestace. Tuto hodnotu můžete použít ke čtení atestace na blockchainu (opens in a new tab), abyste ověřili, že informace z dotazu GraphQL jsou správné.
  • data: Data schématu (v tomto případě e-mailová adresa).
1 const queryResult = await graphqlClient.request(query)
2
3 if (queryResult.attestations.length == 0)
4 return "no_address@available.is"

Pokud neexistuje žádná atestace, vraťte hodnotu, která je zjevně nesprávná, ale která by se poskytovateli služeb jevila jako platná.

1 const attestationDataFields = graphqlEncoder.decodeData(queryResult.attestations[0].data)
2 return attestationDataFields[0].value.value
3}

Pokud existuje hodnota, použijte k dekódování dat decodeData. Nepotřebujeme metadata, která poskytuje, pouze samotnou hodnotu.

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 );
Zobrazit vše

Pro získání e-mailové adresy použijte novou funkci.

A co decentralizace?

V této konfiguraci se uživatelé nemohou vydávat za někoho, kým nejsou, pokud se spoléháme na důvěryhodné atestátory pro mapování adres Etherea na e-mailové adresy. Náš poskytovatel identity je však stále centralizovanou komponentou. Kdokoli, kdo má privátní klíč poskytovatele identity, může poskytovateli služeb posílat nepravdivé informace.

Může existovat řešení pomocí více-stranného výpočtu (MPC) (opens in a new tab). Doufám, že o tom napíšu v budoucím tutoriálu.

Závěr

Přijetí standardu pro přihlášení, jako jsou podpisy Etherea, se potýká s problémem slepice a vejce. Poskytovatelé služeb chtějí oslovit co nejširší trh. Uživatelé chtějí mít přístup ke službám, aniž by se museli starat o podporu svého standardu pro přihlášení. Vytváření adaptérů, jako je Ethereum IdP, nám může pomoci překonat tuto překážku.

Více z mé práce najdete zde (opens in a new tab).

Stránka naposledy aktualizována: 23. listopadu 2025

Byl tento tutoriál užitečný?