Uso de Ethereum para autenticación web2
Introducción
SAMLopens in a new tab es un estándar utilizado en web2 que permite a un proveedor de identidad (IdP)opens in a new tab proporcionar información de usuario a proveedores de servicios (SP)opens in a new tab.
En este tutorial aprenderá cómo integrar firmas de Ethereum con SAML para permitir a los usuarios utilizar sus wallets de Ethereum para autenticarse en servicios web2 que aún no soportan Ethereum de forma nativa.
Tenga en cuenta que este tutorial está escrito para dos públicos separados:
- Personas del entorno Ethereum que entienden Ethereum y necesitan aprender sobre SAML
- Personas de web2 que entienden SAML y autenticación web2 y necesitan aprender sobre Ethereum
Como resultado, va a contener mucho material introductorio que usted ya conoce. Si lo desea, puede saltarse esas partes.
SAML para personas de Ethereum
SAML es un protocolo centralizado. Un proveedor de servicios (SP) solo acepta aseveraciones (como "este es mi usuario John, debe tener permisos para hacer A, B y C") de un proveedor de identidad (IdP) si existe una relación de confianza previa ya sea con él, o con la autoridad de certificaciónopens in a new tab que firmó el certificado de ese IdP.
Por ejemplo, el SP puede ser una agencia de viajes que proporciona servicios de viaje a empresas y el IdP puede ser el sitio web interno de una empresa. Cuando los empleados necesitan reservar viajes de negocios, la agencia de viajes los redirige para autenticarse con la empresa antes de permitirles reservar el viaje.
Así es como las tres entidades, el navegador, el SP y el IdP, negocian el acceso. El SP no necesita saber nada sobre el usuario que utiliza el navegador de antemano, solo debe confiar en el IdP.
Ethereum para personas de SAML
Ethereum es un sistema descentralizado.
Los usuarios tienen una clave privada (normalmente guardada en una extensión del navegador). A partir de la clave privada se puede derivar una clave pública, y de esta una dirección de 20 bytes. Cuando los usuarios necesitan iniciar sesión en un sistema, se les solicita firmar un mensaje con un nonce (un valor de un solo uso). El servidor puede verificar que la firma fue creada por esa dirección.
La firma solo verifica la dirección de Ethereum. Para obtener otros atributos del usuario, normalmente se utilizan atestacionesopens in a new tab. Una atestación normalmente tiene estos campos:
- Atestador, la dirección que realizó la atestación
- Destinatario, la dirección a la que se aplica la atestación
- Datos, los datos que se atestiguan, como nombre, permisos, etc.
- Esquema, el ID del esquema empleado para interpretar los datos.
Debido a la naturaleza descentralizada de Ethereum, cualquier usuario puede realizar atestaciones. La identidad del atestador es importante para identificar qué atestaciones consideramos confiables.
Configuración
El primer paso es disponer de un SP SAML y un IdP SAML que se comuniquen entre sí.
-
Descargue el software. El software de ejemplo para este artículo está en githubopens in a new tab. Las distintas etapas están almacenadas en diferentes ramas; para esta etapa debe usar
saml-only1git clone https://github.com/qbzzt/250420-saml-ethereum -b saml-only2cd 250420-saml-ethereum3pnpm install -
Cree claves con certificados autofirmados. Esto significa que la clave es su propia autoridad de certificación y debe importarse manualmente en el proveedor de servicios. Consulte la documentación de OpenSSLopens in a new tab para más información.
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 .. -
Inicie los servidores (tanto SP como IdP)
1pnpm start -
Navegue al SP en la URL http://localhost:3000/opens in a new tab y haga clic en el botón para ser redirigido al IdP (puerto 3001).
-
Proporcione al IdP su dirección de correo electrónico y haga clic en Iniciar sesión en el proveedor de servicios. Verá que es redirigido de vuelta al proveedor de servicios (puerto 3000) y que este lo reconoce por su dirección de correo electrónico.
Explicación detallada
Esto es lo que sucede, paso a paso:
src/config.mts
Este archivo contiene la configuración tanto para el Proveedor de Identidad como para el Proveedor de Servicios. Normalmente serían dos entidades diferentes, pero aquí podemos compartir el código por simplicidad.
1const fs = await import("fs")23const protocol="http"Por ahora solo estamos probando, así que está bien usar HTTP.
1export const spCert = fs.readFileSync("keys/saml-sp.crt").toString()2export const idpCert = fs.readFileSync("keys/saml-idp.crt").toString()Lea las claves públicas, que normalmente están disponibles para ambos componentes (y se confía en ellas directamente, o son firmadas por una autoridad de certificación confiable).
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}`Mostrar todoLas URL de ambos componentes.
1export const spPublicData = {Los datos públicos para el proveedor de servicios.
1 entityID: `${spUrl}/metadata`,Por convención, en SAML el entityID es la URL donde la metadata de la entidad está disponible. Estos metadatos corresponden a los datos públicos aquí, excepto que están en 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 }Mostrar todoLa definición más importante para nuestros propósitos es assertionConsumerServer. Significa que para hacer una afirmación (por ejemplo, "el usuario que le envía esta información es somebody@example.comopens email client") al proveedor de servicios necesitamos usar HTTP POSTopens in a new tab a la 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 }Mostrar todoLos datos públicos para el proveedor de identidad son similares. Especifica que, para iniciar sesión, debe hacer POST a http://localhost:3001/idp/login y para cerrar sesión debe hacer POST a http://localhost:3001/idp/logout.
src/sp.mts
Este es el código que implementa un proveedor de servicios.
1import * as config from "./config.mts"2const fs = await import("fs")3const saml = await import("samlify")Utilizamos la biblioteca samlifyopens in a new tab para implementar SAML.
1import * as validator from "@authenio/samlify-node-xmllint"2saml.setSchemaValidator(validator)La biblioteca samlify espera tener un paquete que valide que el XML es correcto, firmado con la clave pública esperada, etc. Usamos @authenio/samlify-node-xmllintopens in a new tab para este propósito.
1const express = (await import("express")).default2const spRouter = express.Router()3const app = express()Un Routeropens in a new tab de expressopens in a new tab es un "mini sitio web" que puede montarse dentro de un sitio web. En este caso, lo usamos para agrupar todas las definiciones del proveedor de servicios.
1const spPrivateKey = fs.readFileSync("keys/saml-sp.pem").toString()23const sp = saml.ServiceProvider({4 privateKey: spPrivateKey, 5 ...config.spPublicData6})La propia representación del proveedor de servicios consiste en todos los datos públicos y la clave privada que utiliza para firmar información.
1const idp = saml.IdentityProvider(config.idpPublicData);Los datos públicos contienen todo lo que el proveedor de servicios necesita saber sobre el proveedor de identidad.
1spRouter.get(`/metadata`, 2 (req, res) => res.header("Content-Type", "text/xml").send(sp.getMetadata())3)Para facilitar la interoperabilidad con otros componentes SAML, los proveedores de servicios e identidad deben tener sus datos públicos (llamados metadatos) disponibles en formato XML en /metadata.
1spRouter.post(`/assertion`,Esta es la página a la que accede el navegador para identificarse. La afirmación incluye el identificador del usuario (aquí usamos dirección de correo electrónico), y puede incluir atributos adicionales. Este es el manejador para el paso 7 en el diagrama de secuencia anterior.
1 async (req, res) => {2 // console.log(`SAML response:\n${Buffer.from(req.body.SAMLResponse, 'base64').toString('utf-8')}`)Puede usar el comando comentado para ver los datos XML proveídos en la afirmación. Está codificado en base64opens in a new tab.
1 try {2 const loginResponse = await sp.parseLoginResponse(idp, 'post', req);Analice la solicitud de inicio de sesión desde el servidor de identidad.
1 res.send(`2 <html>3 <body>4 <h2>Hola ${loginResponse.extract.nameID}</h2>5 </body>6 </html>7 `)8 res.send();Envíe una respuesta HTML, solo para mostrar al usuario que se recibió el inicio de sesión.
1 } catch (err) {2 console.error('Error processing SAML response:', err);3 res.status(400).send('SAML authentication failed');4 }5 }6)Informe al usuario en caso de fallo.
1spRouter.get('/login',Cree una solicitud de inicio de sesión cuando el navegador intente acceder a esta página. Este es el manejador para el paso 1 en el diagrama de secuencia anterior.
1 async (req, res) => {2 const loginRequest = await sp.createLoginRequest(idp, "post")Obtenga la información para enviar una solicitud de inicio de sesión.
1 res.send(`2 <html>3 <body>4 <script>5 window.onload = function () { document.forms[0].submit(); } 6 </script>Esta página envía el formulario automáticamente (ver abajo). Así, el usuario no tiene que hacer nada para ser redirigido. Este es el paso 2 en el diagrama de secuencia anterior.
1 <form method="post" action="${loginRequest.entityEndpoint}">Realiza un POST a loginRequest.entityEndpoint (la URL del endpoint del proveedor de identidad).
1 <input type="hidden" name="${loginRequest.type}" value="${loginRequest.context}" />El nombre del input es loginRequest.type (SAMLRequest). El contenido de ese campo es loginRequest.context, que es nuevamente XML codificado en base64.
1 </form>2 </body>3 </html>4 `) 5 }6)78app.use(express.urlencoded({extended: true}))Este middlewareopens in a new tab lee el cuerpo de la solicitud HTTPopens in a new tab. Por defecto, express lo ignora porque la mayoría de las solicitudes no lo requieren. Lo necesitamos porque POST sí utiliza el cuerpo.
1app.use(`/${config.spDir}`, spRouter)Monte el router en el directorio del proveedor de servicios (/sp).
1app.get("/", (req, res) => {2 res.send(`3 <html>4 <body>5 <button onClick="document.location.href='${config.spUrl}/login'">6 Haga clic aquí para iniciar sesión7 </button>8 </body>9 </html>10 `)11})Mostrar todoSi un navegador intenta acceder al directorio raíz, proporcione un enlace a la página de inicio de sesión.
1app.listen(config.spPort, () => {2 console.log(`service provider is running on http://${config.spHostname}:${config.spPort}`)3})Escuche en el spPort con esta aplicación express.
src/idp.mts
Este es el proveedor de identidad. Es muy similar al proveedor de servicios; las explicaciones a continuación son para las partes que difieren.
1const xmlParser = new (await import("fast-xml-parser")).XMLParser(2 {3 ignoreAttributes: false, // Preserve attributes4 attributeNamePrefix: "@_", // Prefix for attributes5 }6)Necesitamos leer y comprender la solicitud XML que recibimos del proveedor de servicios.
1const getLoginPage = requestId => `Esta función crea la página con el formulario enviado automáticamente que se devuelve en el paso 4 del diagrama de secuencia anterior.
1<html>2 <head>3 <title>Página de inicio de sesión</title>4 </head>5 <body>6 <h2>Página de inicio de sesión</h2>7 <form method="post" action="./loginSubmitted">8 <input type="hidden" name="requestId" value="${requestId}" />9 Dirección de correo electrónico: <input name="email" />10 <br />11 <button type="Submit">12 Iniciar sesión en el proveedor de servicios13 </button>Mostrar todoHay dos campos que enviamos al proveedor de servicios:
- El
requestIdal que respondemos. - El identificador del usuario (por ahora, la dirección de correo electrónico que indica el usuario).
1 </form>2 </body>3</html>45const idpRouter = express.Router()67idpRouter.post("/loginSubmitted", async (req, res) => {8 const loginResponse = await idp.createLoginResponse,Este es el manejador para el paso 5 en el diagrama de secuencia anterior. idp.createLoginResponseopens in a new tab crea la respuesta de inicio de sesión.
1 sp, 2 {3 authnContextClassRef: 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport',4 audience: sp.entityID,La audiencia es el proveedor de servicios.
1 extract: {2 request: {3 id: req.body.requestId4 }5 },Información extraída de la solicitud. El único parámetro que nos importa en la solicitud es el requestId, que permite al proveedor de servicios emparejar solicitudes y sus respuestas.
1 signingKey: { privateKey: idpPrivateKey, publicKey: config.idpCert } // Ensure signingNecesitamos que signingKey tenga los datos para firmar la respuesta. El proveedor de servicios no confía en solicitudes no firmadas.
1 },2 "post",3 {4 email: req.body.emailEste es el campo con la información del usuario que devolvemos al proveedor de servicios.
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})Mostrar todoNuevamente, use un formulario que se envía automáticamente. Este es el paso 6 del diagrama de secuencia anterior.
12// Punto final de IdP para solicitudes de inicio de sesión3idpRouter.post(`/login`,Este es el punto final que recibe una solicitud de inicio de sesión del proveedor de servicios. Este es el manejador del paso 3 del diagrama de secuencia anterior.
1 async (req, res) => {2 try {3 // Solución alternativa porque no pude hacer funcionar 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"]))Deberíamos poder usar idp.parseLoginRequestopens in a new tab para leer el ID de la solicitud de autenticación. Sin embargo, no pude lograr que funcionara y no valía la pena invertir mucho tiempo en ello, así que utilizo un analizador XML de propósito generalopens in a new tab. La información que necesitamos es el atributo ID dentro de la etiqueta <samlp:AuthnRequest>, que se encuentra al nivel superior del XML.
Usando firmas de Ethereum
Ahora que podemos enviar una identidad de usuario al proveedor de servicios, el siguiente paso es obtener la identidad del usuario de manera confiable. Viem nos permite simplemente solicitar la dirección del usuario a la billetera, pero esto implica pedirle esa información al navegador. No controlamos el navegador, por lo que no podemos confiar automáticamente en la respuesta que recibimos de él.
En su lugar, el IdP va a enviar al navegador una cadena para que la firme. Si la billetera en el navegador firma esta cadena, significa que realmente es esa dirección (es decir, conoce la clave privada que corresponde a la dirección).
Para ver esto en funcionamiento, detén el IdP y SP existentes y ejecuta estos comandos:
1git checkout eth-signatures2pnpm install3pnpm startLuego navegue al SPopens in a new tab y siga las instrucciones.
Tenga en cuenta que en este punto no sabemos cómo obtener la dirección de correo electrónico a partir de la dirección de Ethereum, por lo que en su lugar reportamos <ethereum address>@bad.email.address al SP.
Explicación detallada
Los cambios se encuentran en los pasos 4 y 5 del diagrama anterior.
El único archivo que cambiamos es idp.mts. Aquí están las partes que cambiaron.
1import { v4 as uuidv4 } from 'uuid'2import { verifyMessage } from 'viem'Necesitamos estas dos librerías adicionales. Usamos uuidopens in a new tab para crear el valor nonceopens in a new tab. El valor en sí no importa, solo el hecho de que solo se utilice una vez.
La librería viemopens in a new tab nos permite usar definiciones de Ethereum. Aquí la necesitamos para verificar que la firma sea realmente válida.
1const loginPrompt = "To access the service provider, sign this nonce: "La billetera le pide permiso al usuario para firmar el mensaje. Un mensaje que solo es un nonce podría confundir a los usuarios, por eso incluimos este mensaje.
1// Mantener los requestIDs aquí2let nonces = {}Necesitamos la información de la solicitud para poder responderla. Podríamos enviarla con la solicitud (paso 4), y recibirla de vuelta (paso 5). Sin embargo, no podemos confiar en la información que recibimos del navegador, que está bajo el control de un usuario potencialmente hostil. Por lo tanto, es mejor almacenarla aquí, usando el nonce como clave.
Tenga en cuenta que aquí lo hacemos como una variable por simplicidad. Sin embargo, esto tiene varias desventajas:
- Somos vulnerables a un ataque de denegación de servicio. Un usuario malicioso podría intentar iniciar sesión múltiples veces, llenando nuestra memoria.
- Si el proceso IdP necesita reiniciarse, se perderían los valores existentes.
- No podemos balancear carga entre varios procesos, porque cada uno tendría su propia variable.
En un sistema de producción usaríamos una base de datos e implementaríamos algún mecanismo de expiración.
1const getSignaturePage = requestId => {2 const nonce = uuidv4()3 nonces[nonce] = requestIdCree un nonce y guarde el requestId para su uso futuro.
1 return `2<html>3 <head>4 <script type="module">Este JavaScript se ejecuta automáticamente cuando se carga la página.
1 import { createWalletClient, custom, getAddress } from 'https://esm.sh/viem'Necesitamos varias funciones de viem.
1 if (!window.ethereum) {2 alert("Please install MetaMask or a compatible wallet and then reload")3 }Solo podemos trabajar si hay una billetera en el navegador.
1 const [account] = await window.ethereum.request({method: 'eth_requestAccounts'})Solicite la lista de cuentas a la billetera (window.ethereum). Suponga que hay al menos una, y solo guarde la primera.
1 const walletClient = createWalletClient({2 account,3 transport: custom(window.ethereum)4 })Cree un cliente de billeteraopens in a new tab para interactuar con la billetera del navegador.
1 window.goodSignature = () => {2 walletClient.signMessage({3 message: "${loginPrompt}${nonce}"Solicite al usuario que firme un mensaje. Debido a que todo este HTML está en una plantilla de textoopens in a new tab, podemos usar variables definidas en el proceso idp. Este es el paso 4.5 en el diagrama de secuencia.
1 }).then(signature => {2 const path= "/${config.idpDir}/signature/${nonce}/" + account + "/" + signature3 window.location.href = path4 })5 }Redirija a /idp/signature/<nonce>/<address>/<signature>. Este es el paso 5 en el diagrama de secuencia.
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 es enviada de vuelta por el navegador, que puede ser potencialmente malicioso (nada impide que usted simplemente abra http://localhost:3001/idp/signature/bad-nonce/bad-address/bad-signature en el navegador). Por lo tanto, es importante verificar que el proceso IdP maneje correctamente las firmas incorrectas.
1 </script>2 </head>3 <body>4 <h2>Por favor firme</h2>5 <button onClick="window.goodSignature()">6 Enviar una firma buena (válida)7 </button>8 <br/>9 <button onClick="window.badSignature()">10 Enviar una firma mala (inválida)11 </button>12 </body>13</html> 14`15}Mostrar todoEl resto es simplemente HTML estándar.
1idpRouter.get("/signature/:nonce/:account/:signature", async (req, res) => {Este es el manejador para el paso 5 en el diagrama de secuencia.
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] = undefinedObtenga el ID de la solicitud y borre el nonce de nonces para asegurarse de que no pueda reutilizarse.
1 try {Como hay muchas maneras en que la firma puede ser inválida, envolvemos esto en un bloque try ... catch para capturar cualquier error lanzado.
1 const validSignature = await verifyMessage({2 address: req.params.account,3 message: `${loginPrompt}${req.params.nonce}`,4 signature: req.params.signature5 })Use verifyMessageopens in a new tab para implementar el paso 5.5 en el diagrama de secuencia.
1 if (!validSignature)2 throw("Bad signature")3 } catch (err) {4 res.send("Error:" + err)5 return ;6 }El resto del manejador es equivalente a lo que hicimos en el manejador /loginSubmitted previamente, salvo por un pequeño cambio.
1 const loginResponse = await idp.createLoginResponse(2 .3 .4 .5 {6 email: req.params.account + "@bad.email.address"7 }8 );No tenemos la dirección de correo electrónico real (la obtendremos en la siguiente sección), así que por ahora devolvemos la dirección de Ethereum y la marcamos claramente como que no es una dirección de correo válida.
1// Punto final de IdP para solicitudes de inicio de sesión2idpRouter.post(`/login`,3 async (req, res) => {4 try {5 // Solución alternativa porque no pude hacer funcionar 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)Mostrar todoEn lugar de getLoginPage, ahora use getSignaturePage en el manejador del paso 3.
Obteniendo la dirección de correo electrónico
El siguiente paso es obtener la dirección de correo electrónico, el identificador solicitado por el proveedor de servicios. Para ello, usamos el Ethereum Attestation Service (EAS)opens in a new tab.
La manera más sencilla de obtener atestaciones es usar la API GraphQLopens in a new tab. Usamos esta consulta:
1query GetAttestationsByRecipient {2 attestations(3 where: { 4 recipient: { equals: "${getAddress(ethAddr)}" }5 schemaId: { equals: "0xfa2eff59a916e3cc3246f9aec5e0ca00874ae9d09e4678e5016006f07622f977" }6 }7 take: 18 ) { 9 data10 id11 attester12 }13}Mostrar todoEste schemaIdopens in a new tab incluye solo una dirección de correo electrónico. Esta consulta solicita las atestaciones de este esquema. El sujeto de la atestación se llama recipient. Siempre es una dirección de Ethereum.
Advertencia: la forma en que estamos obteniendo atestaciones aquí tiene dos problemas de seguridad.
-
Estamos accediendo al punto final de la API,
https://optimism.easscan.org/graphql, que es un componente centralizado. Podemos obtener el atributoidy luego hacer una consulta on-chain para comprobar que la atestación es real, pero el punto final de la API aún puede censurar atestaciones al no informarnos sobre ellas.Este problema no es imposible de resolver, podríamos ejecutar nuestro propio punto final GraphQL y obtener las atestaciones de los registros de la cadena, pero eso es excesivo para nuestros fines.
-
No revisamos la identidad del attester. Cualquiera puede enviarnos información falsa. En una implementación real, tendríamos un conjunto de attesters de confianza y solo consideraríamos sus atestaciones.
Para ver esto en funcionamiento, detén el IdP y SP existentes y ejecuta estos comandos:
1git checkout email-address2pnpm install3pnpm startLuego proporcione su dirección de correo electrónico. Tiene dos maneras de hacerlo:
-
Importe una billetera usando una clave privada y use la clave privada de prueba
0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80. -
Agregue una atestación para su propia dirección de correo electrónico:
-
Vaya al esquema en el explorador de atestacionesopens in a new tab.
-
Haga clic en Attest with Schema.
-
Ingrese su dirección de Ethereum como receptor, su correo electrónico como dirección de correo, y seleccione Onchain. Luego haga clic en Make Attestation.
-
Acepte la transacción en su billetera. Necesitará algo de ETH en la cadena de bloques Optimismopens in a new tab para pagar el gas.
-
De cualquier manera, después de hacer esto, navegue a http://localhost:3000opens in a new tab y siga las instrucciones. Si importó la clave privada de prueba, el correo que recibirá es test_addr_0@example.com. Si usó su propia dirección, debería ser la que usted haya atestiguado.
Explicación detallada
Los nuevos pasos son la comunicación GraphQL, pasos 5.6 y 5.7.
Nuevamente, aquí están las partes modificadas de idp.mts.
1import { GraphQLClient } from 'graphql-request'2import { SchemaEncoder } from '@ethereum-attestation-service/eas-sdk'Importe las librerías que necesitamos.
1const graphqlEndpointUrl = "https://optimism.easscan.org/graphql"Hay un punto final separado para cada blockchainopens in a new tab.
1const graphqlClient = new GraphQLClient(graphqlEndpointUrl, { fetch })Cree un nuevo cliente GraphQLClient que podamos usar para consultar el punto final.
1const graphqlSchema = 'string emailAddress'2const graphqlEncoder = new SchemaEncoder(graphqlSchema)GraphQL solo nos da un objeto de datos opaco con bytes. Para interpretarlo necesitamos el esquema.
1const ethereumAddressToEmail = async ethAddr => {Una función para obtener una dirección de correo electrónico a partir de una dirección de Ethereum.
1 const query = `2 query GetAttestationsByRecipient {Esta es una consulta GraphQL.
1 Certificaciones(Estamos buscando atestaciones.
1 where: { 2 recipient: { equals: "${getAddress(ethAddr)}" }3 schemaId: { equals: "0xfa2eff59a916e3cc3246f9aec5e0ca00874ae9d09e4678e5016006f07622f977" }4 }Las atestaciones que queremos son las de nuestro esquema, donde el receptor es getAddress(ethAddr). La función getAddressopens in a new tab se asegura de que nuestra dirección tenga la suma de comprobaciónopens in a new tab correcta. Esto es necesario ya que en GraphQL se diferencia entre mayúsculas y minúsculas. "0xBAD060A7", "0xBad060A7" y "0xbad060a7" son valores diferentes.
1 take: 1Sin importar cuántas atestaciones encontremos, solo queremos la primera.
1 ) {2 data3 id4 attester5 }6 }`Los campos que deseamos recibir.
attester: La dirección que envió la atestación. Normalmente esto se utiliza para decidir si confiar o no en la atestación.id: El ID de la atestación. Puede usar este valor para leer la atestación en la cadenaopens in a new tab y verificar que la información en la consulta de GraphQL sea correcta.data: Los datos del esquema (en este caso, la dirección de correo electrónico).
1 const queryResult = await graphqlClient.request(query)23 if (queryResult.attestations.length == 0)4 return "no_address@available.is"Si no hay ninguna atestación, devuelva un valor obviamente incorrecto, pero que parecería válido para el proveedor de servicios.
1 const attestationDataFields = graphqlEncoder.decodeData(queryResult.attestations[0].data)2 return attestationDataFields[0].value.value3}Si hay un valor, use decodeData para decodificar los datos. No necesitamos los metadatos que proporciona, solo el valor en sí.
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 );Mostrar todoUse la nueva función para obtener la dirección de correo electrónico.
¿Qué hay de la descentralización?
En esta configuración, los usuarios no pueden hacerse pasar por alguien que no son, siempre y cuando confiemos en atestadores de confianza para el mapeo de la dirección de Ethereum a la dirección de correo electrónico. Sin embargo, nuestro proveedor de identidad sigue siendo un componente centralizado. Quien tenga la clave privada del proveedor de identidad puede enviar información falsa al proveedor de servicios.
Puede haber una solución usando cómputo multipartito seguro (MPC)opens in a new tab. Espero poder escribir sobre esto en un futuro tutorial.
Conclusión
La adopción de un estándar de inicio de sesión, como las firmas de Ethereum, enfrenta el problema del huevo y la gallina. Los proveedores de servicios quieren captar el mercado más amplio posible. Los usuarios quieren poder acceder a los servicios sin tener que preocuparse por si se admite su estándar de inicio de sesión. Crear adaptadores, como un IdP de Ethereum, puede ayudarnos a superar este obstáculo.
Vea aquí más de mi trabajoopens in a new tab.
Última actualización de la página: 23 de noviembre de 2025





