Utilizzare Ethereum per l'autenticazione web2
Introduzione
SAML (opens in a new tab) è uno standard utilizzato nel web2 per consentire a un provider di identità (IdP) (opens in a new tab) di fornire informazioni sull'utente ai provider di servizi (SP) (opens in a new tab).
In questo tutorial imparerai come integrare le firme di Ethereum con SAML per consentire agli utenti di utilizzare i propri portafogli Ethereum per autenticarsi ai servizi web2 che non supportano ancora nativamente Ethereum.
Nota che questo tutorial è scritto per due tipi di pubblico distinti:
- Utenti di Ethereum che comprendono Ethereum e hanno bisogno di imparare SAML
- Utenti del web2 che comprendono SAML e l'autenticazione web2 e hanno bisogno di imparare Ethereum
Di conseguenza, conterrà molto materiale introduttivo che potresti già conoscere. Sentiti libero di saltarlo.
SAML per gli utenti di Ethereum
SAML è un protocollo centralizzato. Un provider di servizi (SP) accetta asserzioni (come "questo è il mio utente John, dovrebbe avere i permessi per fare A, B e C") da un provider di identità (IdP) solo se ha una relazione di fiducia preesistente con esso, o con l'autorità di certificazione (opens in a new tab) che ha firmato il certificato di quell'IdP.
Ad esempio, l'SP può essere un'agenzia di viaggi che fornisce servizi di viaggio alle aziende, e l'IdP può essere il sito web interno di un'azienda. Quando i dipendenti devono prenotare un viaggio di lavoro, l'agenzia di viaggi li invia per l'autenticazione da parte dell'azienda prima di consentire loro di prenotare effettivamente il viaggio.
Questo è il modo in cui le tre entità, il browser, l'SP e l'IdP, negoziano l'accesso. L'SP non ha bisogno di sapere nulla in anticipo sull'utente che utilizza il browser, deve solo fidarsi dell'IdP.
Ethereum per gli utenti di SAML
Ethereum è un sistema decentralizzato.
Gli utenti hanno una chiave privata (tipicamente conservata in un'estensione del browser). Dalla chiave privata è possibile derivare una chiave pubblica, e da questa un indirizzo di 20 byte. Quando gli utenti devono accedere a un sistema, viene loro richiesto di firmare un messaggio con un nonce (un valore monouso). Il server può verificare che la firma sia stata creata da quell'indirizzo.
La firma verifica solo l'indirizzo di Ethereum. Per ottenere altri attributi dell'utente, in genere si utilizzano le attestazioni (opens in a new tab). Un'attestazione ha tipicamente questi campi:
- Attestatore, l'indirizzo che ha effettuato l'attestazione
- Destinatario, l'indirizzo a cui si applica l'attestazione
- Dati, i dati attestati, come nome, permessi, ecc.
- Schema, l'ID dello schema utilizzato per interpretare i dati.
A causa della natura decentralizzata di Ethereum, qualsiasi utente può effettuare attestazioni. L'identità dell'attestatore è importante per identificare quali attestazioni consideriamo affidabili.
Configurazione
Il primo passo è far comunicare tra loro un SP SAML e un IdP SAML.
-
Scarica il software. Il software di esempio per questo articolo è su github (opens in a new tab). Le diverse fasi sono memorizzate in rami diversi, per questa fase ti serve
saml-only1git clone https://github.com/qbzzt/250420-saml-ethereum -b saml-only2cd 250420-saml-ethereum3pnpm install
122. Crea chiavi con certificati autofirmati. Questo significa che la chiave è la propria autorità di certificazione e deve essere importata manualmente nel provider di servizi. Consulta [la documentazione di OpenSSL](https://docs.openssl.org/master/man1/openssl-req/) per maggiori informazioni. 34 ```sh5 mkdir keys6 cd keys7 openssl req -new -x509 -days 365 -nodes -sha256 -out saml-sp.crt -keyout saml-sp.pem -subj /CN=sp/8 openssl req -new -x509 -days 365 -nodes -sha256 -out saml-idp.crt -keyout saml-idp.pem -subj /CN=idp/9 cd ..Mostra tutto-
Avvia i server (sia SP che IdP)
1pnpm start
124. Naviga verso l'SP all'URL [http://localhost:3000/](http://localhost:3000/) e fai clic sul pulsante per essere reindirizzato all'IdP (porta 3001).345. Fornisci all'IdP il tuo indirizzo e-mail e fai clic su **Login to the service provider**. Vedrai che verrai reindirizzato di nuovo al provider di servizi (porta 3000) e che ti riconoscerà tramite il tuo indirizzo e-mail.56### Spiegazione dettagliata78Questo è ciò che accade, passo dopo passo:9101112#### src/config.mts1314Questo file contiene la configurazione sia per il Provider di Identità che per il Provider di Servizi. Normalmente queste due sarebbero entità diverse, ma qui possiamo condividere il codice per semplicità.1516```typescript17const fs = await import("fs")1819const protocol="http"Mostra tuttoPer ora stiamo solo testando, quindi va bene usare HTTP.
1export const spCert = fs.readFileSync("keys/saml-sp.crt").toString()2export const idpCert = fs.readFileSync("keys/saml-idp.crt").toString()Leggi le chiavi pubbliche, che sono normalmente disponibili per entrambi i componenti (e considerate affidabili direttamente, o firmate da un'autorità di certificazione fidata).
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}`Mostra tuttoGli URL per entrambi i componenti.
1export const spPublicData = {I dati pubblici per il provider di servizi.
1 entityID: `${spUrl}/metadata`,Per convenzione, in SAML l'entityID è l'URL in cui sono disponibili i metadati dell'entità. Questi metadati corrispondono ai dati pubblici qui presenti, tranne per il fatto che sono in formato 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 }Mostra tuttoLa definizione più importante per i nostri scopi è l'assertionConsumerServer. Significa che per asserire qualcosa (ad esempio, "l'utente che ti invia queste informazioni è somebody@example.com (opens email client)") al provider di servizi dobbiamo usare HTTP POST (opens in a new tab) all'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 }Mostra tuttoI dati pubblici per il provider di identità sono simili. Specificano che per far accedere un utente si effettua un POST a http://localhost:3001/idp/login e per disconnettere un utente si effettua un POST a http://localhost:3001/idp/logout.
src/sp.mts
Questo è il codice che implementa un provider di servizi.
1import * as config from "./config.mts"2const fs = await import("fs")3const saml = await import("samlify")Usiamo la libreria samlify (opens in a new tab) per implementare SAML.
1import * as validator from "@authenio/samlify-node-xmllint"2saml.setSchemaValidator(validator)La libreria samlify si aspetta di avere un pacchetto che convalidi che l'XML sia corretto, firmato con la chiave pubblica prevista, ecc. Usiamo @authenio/samlify-node-xmllint (opens in a new tab) per questo scopo.
1const express = (await import("express")).default2const spRouter = express.Router()3const app = express()Un Router (opens in a new tab) di express (opens in a new tab) è un "mini sito web" che può essere montato all'interno di un sito web. In questo caso, lo usiamo per raggruppare tutte le definizioni del provider di servizi.
1const spPrivateKey = fs.readFileSync("keys/saml-sp.pem").toString()23const sp = saml.ServiceProvider({4 privateKey: spPrivateKey, 5 ...config.spPublicData6})La rappresentazione che il provider di servizi ha di se stesso è costituita da tutti i dati pubblici e dalla chiave privata che utilizza per firmare le informazioni.
1const idp = saml.IdentityProvider(config.idpPublicData);I dati pubblici contengono tutto ciò che il provider di servizi deve sapere sul provider di identità.
1spRouter.get(`/metadata`, 2 (req, res) => res.header("Content-Type", "text/xml").send(sp.getMetadata())3)Per consentire l'interoperabilità con altri componenti SAML, i provider di servizi e di identità dovrebbero avere i loro dati pubblici (chiamati metadati) disponibili in formato XML in /metadata.
1spRouter.post(`/assertion`,Questa è la pagina a cui accede il browser per identificarsi. L'asserzione include l'identificatore dell'utente (qui usiamo l'indirizzo e-mail) e può includere attributi aggiuntivi. Questo è il gestore per il passaggio 7 nel diagramma di sequenza sopra.
1 async (req, res) => {2 // console.log(`Risposta SAML:\n${Buffer.from(req.body.SAMLResponse, 'base64').toString('utf-8')}`)Puoi usare il comando commentato per vedere i dati XML forniti nell'asserzione. È codificato in base64 (opens in a new tab).
1 try {2 const loginResponse = await sp.parseLoginResponse(idp, 'post', req);Analizza la richiesta di accesso dal server di identità.
1 res.send(`2 <html>3 <body>4 <h2>Hello ${loginResponse.extract.nameID}</h2>5 </body>6 </html>7 `)8 res.send();Invia una risposta HTML, solo per mostrare all'utente che abbiamo ricevuto l'accesso.
1 } catch (err) {2 console.error('Error processing SAML response:', err);3 res.status(400).send('SAML authentication failed');4 }5 }6)Informa l'utente in caso di fallimento.
1spRouter.get('/login',Crea una richiesta di accesso quando il browser tenta di ottenere questa pagina. Questo è il gestore per il passaggio 1 nel diagramma di sequenza sopra.
1 async (req, res) => {2 const loginRequest = await sp.createLoginRequest(idp, "post")Ottieni le informazioni per inviare una richiesta di accesso.
1 res.send(`2 <html>3 <body>4 <script>5 window.onload = function () { document.forms[0].submit(); } 6 </script>Questa pagina invia il modulo (vedi sotto) automaticamente. In questo modo l'utente non deve fare nulla per essere reindirizzato. Questo è il passaggio 2 nel diagramma di sequenza sopra.
1 <form method="post" action="${loginRequest.entityEndpoint}">Invia a loginRequest.entityEndpoint (l'URL dell'endpoint del provider di identità).
1 <input type="hidden" name="${loginRequest.type}" value="${loginRequest.context}" />Il nome dell'input è loginRequest.type (SAMLRequest). Il contenuto per quel campo è loginRequest.context, che è di nuovo XML codificato in base64.
1 </form>2 </body>3 </html>4 `) 5 }6)78app.use(express.urlencoded({extended: true}))Questo middleware (opens in a new tab) legge il corpo della richiesta HTTP (opens in a new tab). Per impostazione predefinita express lo ignora, perché la maggior parte delle richieste non lo richiede. Ne abbiamo bisogno perché POST utilizza il corpo.
1app.use(`/${config.spDir}`, spRouter)Monta il router nella directory del provider di servizi (/sp).
1app.get("/", (req, res) => {2 res.send(`3 <html>4 <body>5 <button onClick="document.location.href='${config.spUrl}/login'">6 Click here to log on7 </button>8 </body>9 </html>10 `)11})Mostra tuttoSe un browser tenta di ottenere la directory principale, forniscigli un link alla pagina di accesso.
1app.listen(config.spPort, () => {2 console.log(`service provider is running on http://${config.spHostname}:${config.spPort}`)3})Ascolta la spPort con questa applicazione express.
src/idp.mts
Questo è il provider di identità. È molto simile al provider di servizi, le spiegazioni di seguito riguardano le parti che sono diverse.
1const xmlParser = new (await import("fast-xml-parser")).XMLParser(2 {3 ignoreAttributes: false, // Preserva gli attributi4 attributeNamePrefix: "@_", // Prefisso per gli attributi5 }6)Dobbiamo leggere e comprendere la richiesta XML che riceviamo dal provider di servizi.
1const getLoginPage = requestId => `Questa funzione crea la pagina con il modulo inviato automaticamente che viene restituita nel passaggio 4 del diagramma di sequenza sopra.
1<html>2 <head>3 <title>Login page</title>4 </head>5 <body>6 <h2>Login page</h2>7 <form method="post" action="./loginSubmitted">8 <input type="hidden" name="requestId" value="${requestId}" />9 Email address: <input name="email" />10 <br />11 <button type="Submit">12 Login to the service provider13 </button>Mostra tuttoCi sono due campi che inviamo al provider di servizi:
- Il
requestIda cui stiamo rispondendo. - L'identificatore dell'utente (per ora usiamo l'indirizzo e-mail fornito dall'utente).
1 </form>2 </body>3</html>45const idpRouter = express.Router()67idpRouter.post("/loginSubmitted", async (req, res) => {8 const loginResponse = await idp.createLoginResponse(Questo è il gestore per il passaggio 5 del diagramma di sequenza sopra. idp.createLoginResponse (opens in a new tab) crea la risposta di accesso.
1 sp, 2 {3 authnContextClassRef: 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport',4 audience: sp.entityID,Il pubblico è il provider di servizi.
1 extract: {2 request: {3 id: req.body.requestId4 }5 },Informazioni estratte dalla richiesta. L'unico parametro che ci interessa nella richiesta è il requestId, che consente al provider di servizi di far corrispondere le richieste e le relative risposte.
1 signingKey: { privateKey: idpPrivateKey, publicKey: config.idpCert } // Assicura la firmaAbbiamo bisogno di signingKey per avere i dati per firmare la risposta. Il provider di servizi non si fida delle richieste non firmate.
1 },2 "post",3 {4 email: req.body.emailQuesto è il campo con le informazioni sull'utente che inviamo di nuovo al provider di servizi.
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})Mostra tuttoAncora una volta, usa un modulo inviato automaticamente. Questo è il passaggio 6 del diagramma di sequenza sopra.
12// Endpoint IdP per le richieste di login3idpRouter.post(`/login`,Questo è l'endpoint che riceve una richiesta di accesso dal provider di servizi. Questo è il gestore del passaggio 3 del diagramma di sequenza sopra.
1 async (req, res) => {2 try {3 // Soluzione alternativa perché non sono riuscito a far funzionare 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"]))Dovremmo essere in grado di usare idp.parseLoginRequest (opens in a new tab) per leggere l'ID della richiesta di autenticazione. Tuttavia, non sono riuscito a farlo funzionare e non valeva la pena dedicarci molto tempo, quindi uso semplicemente un parser XML di uso generale (opens in a new tab). L'informazione di cui abbiamo bisogno è l'attributo ID all'interno del tag <samlp:AuthnRequest>, che si trova al livello superiore dell'XML.
Utilizzare le firme di Ethereum
Ora che possiamo inviare un'identità utente al provider di servizi, il passo successivo è ottenere l'identità utente in modo affidabile. Viem ci consente di chiedere semplicemente al portafoglio l'indirizzo dell'utente, ma questo significa chiedere le informazioni al browser. Non controlliamo il browser, quindi non possiamo fidarci automaticamente della risposta che ne otteniamo.
Invece, l'IdP invierà al browser una stringa da firmare. Se il portafoglio nel browser firma questa stringa, significa che è davvero quell'indirizzo (cioè, conosce la chiave privata che corrisponde all'indirizzo).
Per vederlo in azione, ferma l'IdP e l'SP esistenti ed esegui questi comandi:
1git checkout eth-signatures2pnpm install3pnpm startQuindi naviga verso l'SP (opens in a new tab) e segui le indicazioni.
Nota che a questo punto non sappiamo come ottenere l'indirizzo e-mail dall'indirizzo di Ethereum, quindi riportiamo invece <ethereum address>@bad.email.address all'SP.
Spiegazione dettagliata
Le modifiche sono nei passaggi 4-5 del diagramma precedente.
L'unico file che abbiamo modificato è idp.mts. Ecco le parti modificate.
1import { v4 as uuidv4 } from 'uuid'2import { verifyMessage } from 'viem'Abbiamo bisogno di queste due librerie aggiuntive. Usiamo uuid (opens in a new tab) per creare il valore del nonce (opens in a new tab). Il valore in sé non ha importanza, solo il fatto che venga utilizzato una sola volta.
La libreria viem (opens in a new tab) ci consente di utilizzare le definizioni di Ethereum. Qui ne abbiamo bisogno per verificare che la firma sia effettivamente valida.
1const loginPrompt = "To access the service provider, sign this nonce: "Il portafoglio chiede all'utente il permesso di firmare il messaggio. Un messaggio che è solo un nonce potrebbe confondere gli utenti, quindi includiamo questo prompt.
1// Mantieni qui i requestID2let nonces = {}Abbiamo bisogno delle informazioni della richiesta per potervi rispondere. Potremmo inviarle con la richiesta (passaggio 4) e riceverle indietro (passaggio 5). Tuttavia, non possiamo fidarci delle informazioni che otteniamo dal browser, che è sotto il controllo di un utente potenzialmente ostile. Quindi è meglio memorizzarle qui, con il nonce come chiave.
Nota che lo stiamo facendo qui come variabile per motivi di semplicità. Tuttavia, questo presenta diversi svantaggi:
- Siamo vulnerabili a un attacco denial of service. Un utente malintenzionato potrebbe tentare di accedere più volte, riempiendo la nostra memoria.
- Se il processo IdP deve essere riavviato, perdiamo i valori esistenti.
- Non possiamo bilanciare il carico su più processi, perché ognuno avrebbe la propria variabile.
Su un sistema di produzione useremmo un database e implementeremmo un qualche tipo di meccanismo di scadenza.
1const getSignaturePage = requestId => {2 const nonce = uuidv4()3 nonces[nonce] = requestIdCrea un nonce e memorizza il requestId per un uso futuro.
1 return `2<html>3 <head>4 <script type="module">Questo JavaScript viene eseguito automaticamente quando la pagina viene caricata.
1 import { createWalletClient, custom, getAddress } from 'https://esm.sh/viem'Abbiamo bisogno di diverse funzioni da viem.
1 if (!window.ethereum) {2 alert("Please install MetaMask or a compatible wallet and then reload")3 }Possiamo funzionare solo se c'è un portafoglio sul browser.
1 const [account] = await window.ethereum.request({method: 'eth_requestAccounts'})Richiedi l'elenco degli account dal portafoglio (window.ethereum). Supponi che ce ne sia almeno uno e memorizza solo il primo.
1 const walletClient = createWalletClient({2 account,3 transport: custom(window.ethereum)4 })Crea un client del portafoglio (opens in a new tab) per interagire con il portafoglio del browser.
1 window.goodSignature = () => {2 walletClient.signMessage({3 message: "${loginPrompt}${nonce}"Chiedi all'utente di firmare un messaggio. Poiché tutto questo HTML si trova in una stringa di template (opens in a new tab), possiamo usare le variabili definite nel processo idp. Questo è il passaggio 4.5 nel diagramma di sequenza.
1 }).then(signature => {2 const path= "/${config.idpDir}/signature/${nonce}/" + account + "/" + signature3 window.location.href = path4 })5 }Reindirizza a /idp/signature/<nonce>/<address>/<signature>. Questo è il passaggio 5 nel diagramma di sequenza.
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 }La firma viene rispedita dal browser, che è potenzialmente dannoso (non c'è nulla che ti impedisca di aprire semplicemente http://localhost:3001/idp/signature/bad-nonce/bad-address/bad-signature nel browser). Pertanto, è importante verificare che il processo IdP gestisca correttamente le firme errate.
1 </script>2 </head>3 <body>4 <h2>Please sign</h2>5 <button onClick="window.goodSignature()">6 Submit a good (valid) signature7 </button>8 <br/>9 <button onClick="window.badSignature()">10 Submit a bad (invalid) signature11 </button>12 </body>13</html> 14`15}Mostra tuttoIl resto è solo HTML standard.
1idpRouter.get("/signature/:nonce/:account/:signature", async (req, res) => {Questo è il gestore per il passaggio 5 nel diagramma di sequenza.
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] = undefinedOttieni l'ID della richiesta ed elimina il nonce da nonces per assicurarti che non possa essere riutilizzato.
1 try {Poiché ci sono così tanti modi in cui la firma può essere non valida, racchiudiamo questo in un blocco try ... catch per intercettare eventuali errori generati.
1 const validSignature = await verifyMessage({2 address: req.params.account,3 message: `${loginPrompt}${req.params.nonce}`,4 signature: req.params.signature5 })Usa verifyMessage (opens in a new tab) per implementare il passaggio 5.5 nel diagramma di sequenza.
1 if (!validSignature)2 throw("Bad signature")3 } catch (err) {4 res.send("Error:" + err)5 return ;6 }Il resto del gestore è equivalente a quello che abbiamo fatto in precedenza nel gestore /loginSubmitted, ad eccezione di una piccola modifica.
1 const loginResponse = await idp.createLoginResponse(2 .3 .4 .5 {6 email: req.params.account + "@bad.email.address"7 }8 );Non abbiamo l'indirizzo e-mail effettivo (lo otterremo nella prossima sezione), quindi per ora restituiamo l'indirizzo di Ethereum e lo contrassegniamo chiaramente come non un indirizzo e-mail.
1// Endpoint IdP per le richieste di login2idpRouter.post(`/login`,3 async (req, res) => {4 try {5 // Soluzione alternativa perché non sono riuscito a far funzionare 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)Mostra tuttoInvece di getLoginPage, ora usa getSignaturePage nel gestore del passaggio 3.
Ottenere l'indirizzo e-mail
Il passo successivo è ottenere l'indirizzo e-mail, l'identificatore richiesto dal provider di servizi. Per farlo, usiamo l'Ethereum Attestation Service (EAS) (opens in a new tab).
Il modo più semplice per ottenere le attestazioni è usare l'API GraphQL (opens in a new tab). Usiamo questa query:
1query GetAttestationsByRecipient {2 attestations(3 where: { 4 recipient: { equals: "${getAddress(ethAddr)}" }5 schemaId: { equals: "0xfa2eff59a916e3cc3246f9aec5e0ca00874ae9d09e4678e5016006f07622f977" }6 }7 take: 18 ) { 9 data10 id11 attester12 }13}Mostra tuttoQuesto schemaId (opens in a new tab) include solo un indirizzo e-mail. Questa query richiede le attestazioni di questo schema. Il soggetto dell'attestazione è chiamato recipient (destinatario). È sempre un indirizzo di Ethereum.
Attenzione: Il modo in cui stiamo ottenendo le attestazioni qui presenta due problemi di sicurezza.
-
Stiamo andando all'endpoint dell'API,
https://optimism.easscan.org/graphql, che è un componente centralizzato. Possiamo ottenere l'attributoide poi fare una ricerca on-chain per verificare che un'attestazione sia reale, ma l'endpoint dell'API può comunque censurare le attestazioni non informandoci su di esse.Questo problema non è impossibile da risolvere, potremmo eseguire il nostro endpoint GraphQL e ottenere le attestazioni dai log della catena, ma questo è eccessivo per i nostri scopi.
-
Non guardiamo l'identità dell'attestatore. Chiunque può fornirci informazioni false. In un'implementazione nel mondo reale avremmo un insieme di attestatori fidati e guarderemmo solo le loro attestazioni.
Per vederlo in azione, ferma l'IdP e l'SP esistenti ed esegui questi comandi:
1git checkout email-address2pnpm install3pnpm startQuindi fornisci il tuo indirizzo e-mail. Hai due modi per farlo:
-
Importa un portafoglio usando una chiave privata e usa la chiave privata di test
0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80. -
Aggiungi un'attestazione per il tuo indirizzo e-mail:
-
Naviga verso lo schema nell'esploratore di attestazioni (opens in a new tab).
-
Fai clic su Attest with Schema.
-
Inserisci il tuo indirizzo di Ethereum come destinatario, il tuo indirizzo e-mail come email address e seleziona Onchain. Quindi fai clic su Make Attestation.
-
Approva la transazione nel tuo portafoglio. Avrai bisogno di un po' di ETH sulla Blockchain di Optimism (opens in a new tab) per pagare il gas.
-
In ogni caso, dopo averlo fatto naviga verso http://localhost:3000 (opens in a new tab) e segui le indicazioni. Se hai importato la chiave privata di test, l'e-mail che ricevi è test_addr_0@example.com. Se hai usato il tuo indirizzo, dovrebbe essere quello che hai attestato.
Spiegazione dettagliata
I nuovi passaggi sono la comunicazione GraphQL, passaggi 5.6 e 5.7.
Ancora una volta, ecco le parti modificate di idp.mts.
1import { GraphQLClient } from 'graphql-request'2import { SchemaEncoder } from '@ethereum-attestation-service/eas-sdk'Importa le librerie di cui abbiamo bisogno.
1const graphqlEndpointUrl = "https://optimism.easscan.org/graphql"C'è un endpoint separato per ogni blockchain (opens in a new tab).
1const graphqlClient = new GraphQLClient(graphqlEndpointUrl, { fetch })Crea un nuovo client GraphQLClient che possiamo usare per interrogare l'endpoint.
1const graphqlSchema = 'string emailAddress'2const graphqlEncoder = new SchemaEncoder(graphqlSchema)GraphQL ci fornisce solo un oggetto dati opaco con byte. Per capirlo abbiamo bisogno dello schema.
1const ethereumAddressToEmail = async ethAddr => {Una funzione per passare da un indirizzo di Ethereum a un indirizzo e-mail.
1 const query = `2 query GetAttestationsByRecipient {Questa è una query GraphQL.
1 attestations(Stiamo cercando attestazioni.
1 where: { 2 recipient: { equals: "${getAddress(ethAddr)}" }3 schemaId: { equals: "0xfa2eff59a916e3cc3246f9aec5e0ca00874ae9d09e4678e5016006f07622f977" }4 }Le attestazioni che vogliamo sono quelle nel nostro schema, in cui il destinatario è getAddress(ethAddr). La funzione getAddress (opens in a new tab) si assicura che il nostro indirizzo abbia il checksum (opens in a new tab) corretto. Questo è necessario perché GraphQL fa distinzione tra maiuscole e minuscole. "0xBAD060A7", "0xBad060A7" e "0xbad060a7" sono valori diversi.
1 take: 1Indipendentemente da quante attestazioni troviamo, vogliamo solo la prima.
1 ) {2 data3 id4 attester5 }6 }`I campi che vogliamo ricevere.
attester: L'indirizzo che ha inviato l'attestazione. Normalmente questo viene utilizzato per decidere se fidarsi o meno dell'attestazione.id: L'ID dell'attestazione. Puoi usare questo valore per leggere l'attestazione on-chain (opens in a new tab) per verificare che le informazioni dalla query GraphQL siano corrette.data: I dati dello schema (in questo caso, l'indirizzo e-mail).
1 const queryResult = await graphqlClient.request(query)23 if (queryResult.attestations.length == 0)4 return "no_address@available.is"Se non c'è alcuna attestazione, restituisci un valore che è ovviamente errato, ma che apparirebbe valido al provider di servizi.
1 const attestationDataFields = graphqlEncoder.decodeData(queryResult.attestations[0].data)2 return attestationDataFields[0].value.value3}Se c'è un valore, usa decodeData per decodificare i dati. Non abbiamo bisogno dei metadati che fornisce, solo del valore stesso.
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 );Mostra tuttoUsa la nuova funzione per ottenere l'indirizzo e-mail.
E la decentralizzazione?
In questa configurazione gli utenti non possono fingere di essere qualcuno che non sono, a patto di affidarci ad attestatori fidati per la mappatura dall'indirizzo di Ethereum all'indirizzo e-mail. Tuttavia, il nostro provider di identità è ancora un componente centralizzato. Chiunque abbia la chiave privata del provider di identità può inviare informazioni false al provider di servizi.
Potrebbe esserci una soluzione che utilizza il calcolo multiparte (MPC) (opens in a new tab). Spero di scriverne in un tutorial futuro.
Conclusione
L'adozione di uno standard di accesso, come le firme di Ethereum, affronta il problema dell'uovo e della gallina. I provider di servizi vogliono attrarre il mercato più ampio possibile. Gli utenti vogliono poter accedere ai servizi senza doversi preoccupare di supportare il loro standard di accesso. La creazione di adattatori, come un IdP di Ethereum, può aiutarci a superare questo ostacolo.
Vedi qui per altri miei lavori (opens in a new tab).
Ultimo aggiornamento della pagina: 23 novembre 2025




