Pular para o conteúdo principal

Usando Ethereum para autenticação web2

web2
autenticação
eas
Iniciante
Ori Pomerantz
30 de abril de 2025
20 minutos de leitura

Introdução

SAML (opens in a new tab) é um padrão usado na web2 para permitir que um provedor de identidade (IdP) (opens in a new tab) forneça informações do usuário para provedores de serviço (SP) (opens in a new tab).

Neste tutorial, você aprenderá como integrar assinaturas do Ethereum com SAML para permitir que os usuários usem suas carteiras Ethereum para se autenticarem em serviços da web2 que ainda não suportam o Ethereum nativamente.

Observe que este tutorial foi escrito para dois públicos distintos:

  • Pessoas do Ethereum que entendem o Ethereum e precisam aprender sobre SAML
  • Pessoas da Web2 que entendem de SAML e autenticação da web2 e precisam aprender sobre Ethereum

Como resultado, ele conterá muito material introdutório que você já conhece. Sinta-se à vontade para pulá-lo.

SAML para pessoas do Ethereum

SAML é um protocolo centralizado. Um provedor de serviço (SP) só aceita afirmações (como "este é o meu usuário João, ele deve ter permissões para fazer A, B e C") de um provedor de identidade (IdP) se tiver uma relação de confiança preexistente com ele, ou com a autoridade certificadora (opens in a new tab) que assinou o certificado daquele IdP.

Por exemplo, o SP pode ser uma agência de viagens que presta serviços de viagens a empresas, e o IdP pode ser o site interno de uma empresa. Quando os funcionários precisam reservar uma viagem de negócios, a agência de viagens os envia para autenticação pela empresa antes de permitir que eles realmente reservem a viagem.

Passo a passo do processo SAML

Esta é a forma como as três entidades, o navegador, o SP e o IdP, negociam o acesso. O SP não precisa saber nada sobre o usuário que está usando o navegador com antecedência, apenas confiar no IdP.

Ethereum para pessoas do SAML

Ethereum é um sistema descentralizado.

Logon do Ethereum

Os usuários possuem uma chave privada (geralmente mantida em uma extensão do navegador). A partir da chave privada, você pode derivar uma chave pública e, a partir dela, um endereço de 20 bytes. Quando os usuários precisam fazer login em um sistema, eles são solicitados a assinar uma mensagem com um nonce (um valor de uso único). O servidor pode verificar se a assinatura foi criada por esse endereço.

Obtendo dados extras de atestados

A assinatura verifica apenas o endereço do Ethereum. Para obter outros atributos do usuário, você normalmente usa atestados (opens in a new tab). Um atestado normalmente tem estes campos:

  • Atestador, o endereço que fez o atestado
  • Destinatário, o endereço ao qual o atestado se aplica
  • Dados, os dados que estão sendo atestados, como nome, permissões, etc.
  • Esquema, o ID do esquema usado para interpretar os dados.

Devido à natureza descentralizada do Ethereum, qualquer usuário pode fazer atestados. A identidade do atestador é importante para identificar quais atestados consideramos confiáveis.

Configuração

O primeiro passo é ter um SP SAML e um IdP SAML comunicando-se entre si.

  1. Baixe o software. O software de exemplo para este artigo está no github (opens in a new tab). Diferentes estágios são armazenados em diferentes branches, para este estágio você quer saml-only

    1git clone https://github.com/qbzzt/250420-saml-ethereum -b saml-only
    2cd 250420-saml-ethereum
    3pnpm install
  2. Crie chaves com certificados autoassinados. Isso significa que a chave é sua própria autoridade de certificação e precisa ser importada manualmente para o provedor de serviços. Consulte a documentação do OpenSSL (opens in a new tab) para obter mais informações.

    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. Inicie os servidores (tanto o SP quanto o IdP)

    1pnpm start
  4. Acesse o SP na URL http://localhost:3000/ (opens in a new tab) e clique no botão para ser redirecionado para o IdP (porta 3001).

  5. Forneça ao IdP seu endereço de e-mail e clique em Login no provedor de serviços. Veja que você é redirecionado de volta para o provedor de serviços (porta 3000) e que ele o reconhece pelo seu endereço de e-mail.

Explicação detalhada

Isto é o que acontece, passo a passo:

Logon SAML normal sem Ethereum

src/config.mts

Este arquivo contém a configuração tanto para o Provedor de Identidade quanto para o Provedor de Serviços. Normalmente, essas duas seriam entidades diferentes, mas aqui podemos compartilhar código para simplificar.

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

Por enquanto estamos apenas testando, então não há problema em usar HTTP.

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

Leia as chaves públicas, que normalmente estão disponíveis para ambos os componentes (e são confiáveis diretamente ou assinadas por uma autoridade de certificação confiável).

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}`
Exibir tudo

As URLs para ambos os componentes.

1export const spPublicData = {

Os dados públicos para o provedor de serviços.

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

Por convenção, em SAML, o entityID é a URL onde os metadados da entidade estão disponíveis. Esses metadados correspondem aos dados públicos aqui, exceto que estão em 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 }
Exibir tudo

A definição mais importante para nossos propósitos é o assertionConsumerServer. Isso significa que para afirmar algo (por exemplo, "o usuário que lhe envia esta informação é somebody@example.com (opens email client)") para o provedor de serviços, precisamos usar HTTP POST (opens in a new tab) para a 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 }
Exibir tudo

Os dados públicos do provedor de identidade são semelhantes. Ele especifica que, para fazer login de um usuário, você envia um POST para http://localhost:3001/idp/login e para fazer logout de um usuário, você envia um POST para http://localhost:3001/idp/logout.

src/sp.mts

Este é o código que implementa um provedor de serviços.

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

Usamos a biblioteca samlify (opens in a new tab) para implementar o SAML.

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

A biblioteca samlify espera ter um pacote que valide se o XML está correto, assinado com a chave pública esperada, etc. Usamos @authenio/samlify-node-xmllint (opens in a new tab) para essa finalidade.

1const express = (await import("express")).default
2const spRouter = express.Router()
3const app = express()

Um Router (opens in a new tab) do express (opens in a new tab) é um "mini site" que pode ser montado dentro de um site. Nesse caso, nós o usamos para agrupar todas as definições do provedor de serviços.

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

A representação do próprio provedor de serviços de si mesmo são todos os dados públicos e a chave privada que ele usa para assinar informações.

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

Os dados públicos contêm tudo o que o provedor de serviços precisa saber sobre o provedor de identidade.

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

Para permitir a interoperabilidade com outros componentes SAML, os provedores de serviços e de identidade devem ter seus dados públicos (chamados de metadados) disponíveis em formato XML em /metadata.

1spRouter.post(`/assertion`,

Esta é a página acessada pelo navegador para se identificar. A afirmação inclui o identificador do usuário (aqui usamos o endereço de e-mail) e pode incluir atributos adicionais. Este é o manipulador para a etapa 7 no diagrama de sequência acima.

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

Você pode usar o comando comentado para ver os dados XML fornecidos na asserção. Ele é codificado em base64 (opens in a new tab).

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

Analise a solicitação de login do servidor de identidade.

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

Envie uma resposta HTML, apenas para mostrar ao usuário que recebemos o login.

1 } catch (err) {
2 console.error('Error processing SAML response:', err);
3 res.status(400).send('SAML authentication failed');
4 }
5 }
6)

Informe o usuário em caso de falha.

1spRouter.get('/login',

Crie uma solicitação de login quando o navegador tentar obter esta página. Este é o manipulador para a etapa 1 no diagrama de sequência acima.

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

Obtenha as informações para postar uma solicitação de login.

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

Esta página envia o formulário (veja abaixo) automaticamente. Dessa forma, o usuário não precisa fazer nada para ser redirecionado. Esta é a etapa 2 no diagrama de sequência acima.

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

Poste para loginRequest.entityEndpoint (a URL do ponto de extremidade do provedor de identidade).

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

O nome da entrada é loginRequest.type (SAMLRequest). O conteúdo para esse campo é loginRequest.context, que é novamente XML codificado em base64.

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

Este middleware (opens in a new tab) lê o corpo da requisição HTTP (opens in a new tab). Por padrão, o express o ignora, porque a maioria das requisições não o exige. Nós precisamos dele porque o POST usa o corpo.

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

Monte o roteador no diretório do provedor de serviços (/sp).

1app.get("/", (req, res) => {
2 res.send(`
3 <html>
4 <body>
5 <button onClick="document.location.href='${config.spUrl}/login'">
6 Clique aqui para fazer logon
7 </button>
8 </body>
9 </html>
10 `)
11})
Exibir tudo

Se um navegador tentar obter o diretório raiz, forneça a ele um link para a página de login.

1app.listen(config.spPort, () => {
2 console.log(`service provider is running on http://${config.spHostname}:${config.spPort}`)
3})

Escute a spPort com este aplicativo express.

src/idp.mts

Este é o provedor de identidade. É muito semelhante ao provedor de serviços, as explicações abaixo são para as partes que são diferentes.

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

Precisamos ler e entender a solicitação XML que recebemos do provedor de serviços.

1const getLoginPage = requestId => `

Esta função cria a página com o formulário enviado automaticamente que é retornado na etapa 4 do diagrama de sequência acima.

1<html>
2 <head>
3 <title>Página de login</title>
4 </head>
5 <body>
6 <h2>Página de login</h2>
7 <form method="post" action="./loginSubmitted">
8 <input type="hidden" name="requestId" value="${requestId}" />
9 Endereço de e-mail: <input name="email" />
10 <br />
11 <button type="Submit">
12 Fazer login no provedor de serviços
13 </button>
Exibir tudo

Há dois campos que enviamos ao provedor de serviços:

  1. O requestId ao qual estamos respondendo.
  2. O identificador do usuário (usamos o endereço de e-mail que o usuário fornece por enquanto).
1 </form>
2 </body>
3</html>
4
5const idpRouter = express.Router()
6
7idpRouter.post("/loginSubmitted", async (req, res) => {
8 const loginResponse = await idp.createLoginResponse(

Este é o manipulador para a etapa 5 no diagrama de sequência acima. idp.createLoginResponse (opens in a new tab) cria a resposta de login.

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

O público é o provedor de serviços.

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

Informações extraídas da solicitação. O único parâmetro que nos interessa na solicitação é o requestId, que permite ao provedor de serviços corresponder as solicitações e suas respostas.

1 signingKey: { privateKey: idpPrivateKey, publicKey: config.idpCert } // Ensure signing

Precisamos que o signingKey tenha os dados para assinar a resposta. O provedor de serviços não confia em solicitações não assinadas.

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

Este é o campo com as informações do usuário que enviamos de volta ao provedor de serviços.

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})
Exibir tudo

Novamente, use um formulário enviado automaticamente. Esta é a etapa 6 do diagrama de sequência acima.

1
2// Ponto de extremidade do IdP para solicitações de login
3idpRouter.post(`/login`,

Este é o ponto de extremidade que recebe uma solicitação de login do provedor de serviços. Este é o manipulador da etapa 3 do diagrama de sequência acima.

1 async (req, res) => {
2 try {
3 // Workaround because I couldn't get parseLoginRequest to work.
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"]))

Deveríamos ser capazes de usar idp.parseLoginRequest (opens in a new tab) para ler o ID da solicitação de autenticação. No entanto, não consegui fazê-lo funcionar e não valia a pena gastar muito tempo nisso, então eu apenas uso um analisador de XML de propósito geral (opens in a new tab). A informação que precisamos é o atributo ID dentro da tag <samlp:AuthnRequest>, que está no nível superior do XML.

Usando assinaturas do Ethereum

Agora que podemos enviar uma identidade de usuário para o provedor de serviços, o próximo passo é obter a identidade do usuário de uma maneira confiável. O Viem nos permite apenas solicitar à carteira o endereço do usuário, mas isso significa solicitar a informação ao navegador. Nós não controlamos o navegador, então não podemos confiar automaticamente na resposta que recebemos dele.

Em vez disso, o IdP enviará ao navegador uma string para assinar. Se a carteira no navegador assinar esta string, significa que é realmente aquele endereço (ou seja, ele conhece a chave privada que corresponde ao endereço).

Para ver isso em ação, pare o IdP e o SP existentes e execute estes comandos:

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

Em seguida, acesse o SP (opens in a new tab) e siga as instruções.

Note que, neste momento, não sabemos como obter o endereço de e-mail a partir do endereço do Ethereum, então, em vez disso, relatamos <endereço do ethereum>@bad.email.address para o SP.

Explicação detalhada

As alterações estão nas etapas 4-5 no diagrama anterior.

SAML com uma assinatura do Ethereum

O único arquivo que alteramos foi idp.mts. Aqui estão as partes alteradas.

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

Precisamos dessas duas bibliotecas adicionais. Usamos uuid (opens in a new tab) para criar o valor nonce (opens in a new tab). O valor em si não importa, apenas o fato de ser usado uma única vez.

A biblioteca viem (opens in a new tab) nos permite usar definições do Ethereum. Aqui precisamos dela para verificar se a assinatura é realmente válida.

1const loginPrompt = "Para acessar o provedor de serviços, assine este nonce: "

A carteira pede permissão ao usuário para assinar a mensagem. Uma mensagem que é apenas um nonce pode confundir os usuários, por isso incluímos esta solicitação.

1// Manter requestIDs aqui
2let nonces = {}

Precisamos das informações da solicitação para poder respondê-la. Poderíamos enviá-lo com a solicitação (etapa 4) e recebê-lo de volta (etapa 5). No entanto, não podemos confiar nas informações que obtemos do navegador, que está sob o controle de um usuário potencialmente hostil. Portanto, é melhor armazená-lo aqui, com o nonce como chave.

Observe que estamos fazendo isso aqui como uma variável para simplificar. No entanto, isso tem várias desvantagens:

  • Estamos vulneráveis a um ataque de negação de serviço. Um usuário mal-intencionado pode tentar fazer login várias vezes, enchendo nossa memória.
  • Se o processo do IdP precisar ser reiniciado, perdemos os valores existentes.
  • Não podemos balancear a carga entre vários processos, porque cada um teria sua própria variável.

Em um sistema de produção, usaríamos um banco de dados e implementaríamos algum tipo de mecanismo de expiração.

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

Crie um nonce e armazene o requestId para uso futuro.

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

Este JavaScript é executado automaticamente quando a página é carregada.

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

Precisamos de várias funções do viem.

1 if (!window.ethereum) {
2 alert("Por favor, instale a MetaMask ou uma carteira compatível e depois recarregue")
3 }

Só podemos trabalhar se houver uma carteira no navegador.

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

Solicite a lista de contas da carteira (window.ethereum). Suponha que haja pelo menos uma e armazene apenas a primeira.

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

Crie um cliente de carteira (opens in a new tab) para interagir com a carteira do navegador.

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

Peça ao usuário para assinar uma mensagem. Como todo este HTML está em uma string de modelo (opens in a new tab), podemos usar variáveis definidas no processo idp. Esta é a etapa 4.5 no diagrama de sequência.

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

Redirecionar para /idp/signature/<nonce>/<address>/<signature>. Esta é a etapa 5 no diagrama de sequência.

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 }

A assinatura é enviada de volta pelo navegador, que é potencialmente mal-intencionado (não há nada para impedi-lo de simplesmente abrir http://localhost:3001/idp/signature/bad-nonce/bad-address/bad-signature no navegador). Portanto, é importante verificar se o processo IdP lida corretamente com assinaturas inválidas.

1 </script>
2 </head>
3 <body>
4 <h2>Por favor, assine</h2>
5 <button onClick="window.goodSignature()">
6 Enviar uma assinatura boa (válida)
7 </button>
8 <br/>
9 <button onClick="window.badSignature()">
10 Enviar uma assinatura ruim (inválida)
11 </button>
12 </body>
13</html>
14`
15}
Exibir tudo

O resto é apenas HTML padrão.

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

Este é o manipulador para a etapa 5 no diagrama de sequência.

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

Obtenha o ID da solicitação e exclua o nonce de nonces para garantir que ele não possa ser reutilizado.

1 try {

Como há muitas maneiras pelas quais a assinatura pode ser inválida, envolvemos isso em um bloco try ... catch para capturar quaisquer erros lançados.

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

Use verifyMessage (opens in a new tab) para implementar a etapa 5.5 no diagrama de sequência.

1 if (!validSignature)
2 throw("Bad signature")
3 } catch (err) {
4 res.send("Error:" + err)
5 return ;
6 }

O restante do manipulador é equivalente ao que fizemos no manipulador /loginSubmitted anteriormente, exceto por uma pequena alteração.

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

Não temos o endereço de e-mail real (o obteremos na próxima seção), então, por enquanto, retornamos o endereço do Ethereum e o marcamos claramente como não sendo um endereço de e-mail.

1// Ponto de extremidade do IdP para solicitações de login
2idpRouter.post(`/login`,
3 async (req, res) => {
4 try {
5 // Solução alternativa porque não consegui fazer o parseLoginRequest funcionar.
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"]))

Em vez de getLoginPage, agora use getSignaturePage no manipulador da etapa 3.

Obtendo o endereço de e-mail

O próximo passo é obter o endereço de e-mail, o identificador solicitado pelo provedor de serviços. Para fazer isso, usamos o Ethereum Attestation Service (EAS) (opens in a new tab).

A maneira mais fácil de obter atestados é usar a API GraphQL (opens 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: 1
8 ) {
9 data
10 id
11 attester
12 }
13}
Exibir tudo

Este schemaId (opens in a new tab) inclui apenas um endereço de e-mail. Esta consulta solicita atestados deste esquema. O sujeito do atestado é chamado de destinatário. É sempre um endereço do Ethereum.

Aviso: a maneira como estamos obtendo os atestados aqui tem dois problemas de segurança.

  • Estamos indo para o ponto de extremidade da API, https://optimism.easscan.org/graphql, que é um componente centralizado. Podemos obter o atributo id e, em seguida, fazer uma pesquisa na cadeia para verificar se um atestado é real, mas o ponto de extremidade da API ainda pode censurar atestados ao não nos informar sobre eles.

    Este problema não é impossível de resolver, poderíamos executar nosso próprio ponto de extremidade GraphQL e obter os atestados dos registros da cadeia, mas isso é excessivo para nossos propósitos.

  • Nós não olhamos para a identidade do atestador. Qualquer um pode nos fornecer informações falsas. Em uma implementação do mundo real, teríamos um conjunto de atestadores confiáveis e olharíamos apenas para seus atestados.

Para ver isso em ação, pare o IdP e o SP existentes e execute estes comandos:

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

Em seguida, forneça seu endereço de e-mail. Você tem duas maneiras de fazer isso:

  • Importe uma carteira usando uma chave privada e use a chave privada de teste 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80.

  • Adicione um atestado para seu próprio endereço de e-mail:

    1. Navegue até o esquema no explorador de atestados (opens in a new tab).

    2. Clique em Atestar com Esquema.

    3. Insira seu endereço Ethereum como destinatário, seu endereço de e-mail como endereço de e-mail e selecione Onchain. Em seguida, clique em Fazer Atestado.

    4. Aprove a transação na sua carteira. Você precisará de um pouco de ETH na Blockchain da Optimism (opens in a new tab) para pagar pelo gás.

De qualquer forma, depois de fazer isso, navegue até http://localhost:3000 (opens in a new tab) e siga as instruções. Se você importou a chave privada de teste, o e-mail que você recebe é test_addr_0@example.com. Se você usou seu próprio endereço, deve ser o que você atestou.

Explicação detalhada

Obtendo do endereço Ethereum para o e-mail

Os novos passos são a comunicação GraphQL, passos 5.6 e 5.7.

Novamente, aqui estão as partes alteradas de idp.mts.

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

Importe as bibliotecas que precisamos.

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

Existe um endpoint separado para cada cadeia de blocos (opens in a new tab).

1Crie um novo cliente `GraphQLClient` que podemos usar para consultar o ponto de extremidade.

Para entendê-lo, precisamos do esquema.

1Uma função para ir de um endereço Ethereum para um endereço de e-mail.

Esta é uma consulta GraphQL. attestations(

1Estamos procurando por atestados.
1 where: {
2 recipient: { equals: "${getAddress(ethAddr)}" }
3 schemaId: { equals: "0xfa2eff59a916e3cc3246f9aec5e0ca00874ae9d09e4678e5016006f07622f977" }
4 }
1Os atestados que queremos são aqueles em nosso esquema, onde o destinatário é `getAddress(ethAddr)`.

A função getAddress (opens in a new tab) garante que nosso endereço tenha o checksum (opens in a new tab) correto.

1 atestações(

Isto é necessário porque GraphQL diferencia maiúsculas e minúsculas.

1"0xBAD060A7", "0xBad060A7", and "0xbad060a7" são valores diferentes.
1 take: 1

Independentemente de quantos atestados encontrarmos, queremos apenas o primeiro.

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

Os campos que queremos receber.

  • attester: O endereço que enviou o atestado. Normalmente, isso é usado para decidir se confia no atestado ou não.
  • id: O ID do atestado. Você pode usar este valor para ler o atestado on-chain (opens in a new tab) para verificar se a informação da consulta GraphQL está correta.
  • data: Os dados do esquema (neste caso, o endereço de e-mail).
1 const queryResult = await graphqlClient.request(query)
2
3 if (queryResult.attestations.length == 0)
4 return "no_address@available.is"

Se não houver atestado, retorne um valor que seja obviamente incorreto, mas que pareça válido para o provedor de serviços.

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

Se houver um valor, use decodeData para decodificar os dados. Não precisamos dos metadados que ele fornece, apenas do valor em si.

1const loginResponse = await idp.createLoginResponse(
2sp,
3{
4.
5.
6.
7},
8"post",
9{
10email: await ethereumAddressToEmail(req.params.account)
11}
12);
Exibir tudo
1Use a nova função para obter o endereço de e-mail.

E a descentralização? Nesta configuração, os usuários não podem fingir ser alguém que não são, desde que confiemos em atestadores confiáveis para o mapeamento do endereço Ethereum para o endereço de e-mail.

1No entanto, nosso provedor de identidade ainda é um componente centralizado.

Quem tiver a chave privada do provedor de identidade pode enviar informações falsas para o provedor de serviços.

Pode haver uma solução usando computação multipartidária (MPC) (opens in a new tab).

Espero escrever sobre isso em um futuro tutorial. Adoção de um padrão de logon, como assinaturas Ethereum, enfrenta um problema de ovo e galinha. Os provedores de serviços querem atrair o mercado mais amplo possível.

Os usuários querem poder acessar serviços sem ter que se preocupar em suportar seu padrão de logon. A criação de adaptadores, como um IdP do Ethereum, pode nos ajudar a superar esse obstáculo.

Conclusão

Veja aqui mais do meu trabalho (opens in a new tab). Veja aqui mais do meu trabalho (opens in a new tab). Os usuários têm uma chave privada (normalmente mantida em uma extensão de navegador). Criar adaptadores, como um IdP da Ethereum, pode nos ajudar a superar este obstáculo.

Veja aqui mais do meu trabalho (opens in a new tab).

Última atualização da página: 23 de novembro de 2025

Este tutorial foi útil?