본문으로 건너뛰기

웹2 인증에 이더리움 사용하기

웹2
인증
eas
초급
오리 포메란츠
2025년 4월 30일
36 분 소요

소개

SAML (opens in a new tab)은 웹2에서 신원 제공자(IdP) (opens in a new tab)가 [서비스 제공자(SP)](https://en.wikipedia.org/wiki/Service_provider_(SAML)에게 (opens in a new tab) 사용자 정보를 제공할 수 있도록 하는 데 사용되는 표준입니다.

이 튜토리얼에서는 이더리움 서명을 SAML과 통합하여, 사용자가 아직 이더리움을 기본적으로 지원하지 않는 웹2 서비스에 이더리움 지갑을 사용해 자신을 인증할 수 있도록 하는 방법을 배웁니다.

이 튜토리얼은 두 가지 다른 대상을 위해 작성되었습니다.

  • 이더리움을 이해하고 있으며 SAML을 배워야 하는 이더리움 사용자
  • SAML과 웹2 인증을 이해하고 있으며 이더리움을 배워야 하는 웹2 사용자

결과적으로, 여러분이 이미 알고 있는 많은 입문 자료가 포함될 것입니다. 자유롭게 건너뛰셔도 됩니다.

이더리움 사용자를 위한 SAML

SAML은 중앙화된 프로토콜입니다. 서비스 제공자(SP)는 신원 제공자(IdP)와 사전에 신뢰 관계가 있거나, 해당 IdP의 인증서에 서명한 인증 기관(certificate authority) (opens in a new tab)과 신뢰 관계가 있는 경우에만 IdP로부터의 단언(예: "이 사용자는 John이며, A, B, C를 수행할 권한이 있어야 합니다")을 수락합니다.

예를 들어, SP는 기업에 여행 서비스를 제공하는 여행사일 수 있고, IdP는 회사의 내부 웹사이트일 수 있습니다. 직원이 출장을 예약해야 할 때, 여행사는 직원이 실제로 여행을 예약하기 전에 회사에서 인증을 받도록 보냅니다.

Step by step SAML process

이것이 브라우저, SP, IdP라는 세 주체가 접근 권한을 협상하는 방식입니다. SP는 브라우저를 사용하는 사용자에 대해 사전에 알 필요가 없으며, 단지 IdP를 신뢰하기만 하면 됩니다.

SAML 사용자를 위한 이더리움

이더리움은 탈중앙화된 시스템입니다.

Ethereum logon

사용자는 개인 키(일반적으로 브라우저 확장 프로그램에 보관됨)를 가지고 있습니다. 개인 키에서 공개키를 도출할 수 있으며, 이를 통해 20바이트 주소를 얻을 수 있습니다. 사용자가 시스템에 로그인해야 할 때, 논스(일회용 값)가 포함된 메시지에 서명하도록 요청받습니다. 서버는 해당 주소에서 서명이 생성되었는지 확인할 수 있습니다.

Getting extra data from attestations

서명은 이더리움 주소만 확인합니다. 다른 사용자 속성을 얻으려면 일반적으로 증명 (opens in a new tab)을 사용합니다. 증명에는 일반적으로 다음 필드가 있습니다.

  • 증명자(Attestor), 증명을 만든 주소
  • 수신자(Recipient), 증명이 적용되는 주소
  • 데이터(Data), 이름, 권한 등 증명되는 데이터
  • 스키마(Schema), 데이터를 해석하는 데 사용되는 스키마의 ID

이더리움의 탈중앙화된 특성 때문에 누구나 증명을 만들 수 있습니다. 우리가 어떤 증명을 신뢰할 수 있는지 식별하기 위해서는 증명자의 신원이 중요합니다.

설정

첫 번째 단계는 SAML SP와 SAML IdP가 서로 통신하도록 하는 것입니다.

  1. 소프트웨어를 다운로드합니다. 이 문서의 샘플 소프트웨어는 GitHub에 있습니다 (opens in a new tab). 단계별로 다른 브랜치에 저장되어 있으며, 이 단계에서는 saml-only 브랜치가 필요합니다.

    git clone https://github.com/qbzzt/250420-saml-ethereum -b saml-only
    cd 250420-saml-ethereum
    pnpm install
    
  2. 자체 서명된 인증서로 키를 생성합니다. 이는 키 자체가 자체 인증 기관임을 의미하며, 서비스 제공자에 수동으로 가져와야 합니다. 자세한 내용은 OpenSSL 문서 (opens in a new tab)를 참조하세요.

    mkdir keys
    cd keys
    openssl req -new -x509 -days 365 -nodes -sha256 -out saml-sp.crt -keyout saml-sp.pem -subj /CN=sp/
    openssl req -new -x509 -days 365 -nodes -sha256 -out saml-idp.crt -keyout saml-idp.pem -subj /CN=idp/
    cd ..
    
  3. 서버(SP 및 IdP 모두)를 시작합니다.

    pnpm start
    
  4. URL http://localhost:3000/ (opens in a new tab)에서 SP로 이동한 후 버튼을 클릭하여 IdP(포트 3001)로 리디렉션됩니다.

  5. IdP에 이메일 주소를 제공하고 Login to the service provider를 클릭합니다. 서비스 제공자(포트 3000)로 다시 리디렉션되고 이메일 주소로 사용자를 인식하는지 확인합니다.

상세 설명

단계별로 일어나는 일은 다음과 같습니다.

Normal SAML logon without Ethereum

src/config.mts

이 파일에는 신원 제공자와 서비스 제공자 모두에 대한 구성이 포함되어 있습니다. 일반적으로 이 둘은 서로 다른 개체이지만, 여기서는 단순성을 위해 코드를 공유할 수 있습니다.

const fs = await import("fs")

const protocol="http"

지금은 테스트 중이므로 HTTP를 사용해도 괜찮습니다.

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

일반적으로 두 구성 요소 모두에서 사용할 수 있는(그리고 직접 신뢰되거나 신뢰할 수 있는 인증 기관이 서명한) 공개키를 읽습니다.

두 구성 요소의 URL입니다.

export const spPublicData = {

서비스 제공자의 공개 데이터입니다.

    entityID: `${spUrl}/metadata`,

관례적으로 SAML에서 entityID는 개체의 메타데이터를 사용할 수 있는 URL입니다. 이 메타데이터는 XML 형식이라는 점을 제외하면 여기의 공개 데이터에 해당합니다.

우리의 목적에 가장 중요한 정의는 assertionConsumerServer입니다. 이는 서비스 제공자에게 무언가를 단언하려면(예: "이 정보를 보내는 사용자는 somebody@example.com (opens email client)입니다") URL http://localhost:3000/sp/assertionHTTP POST (opens in a new tab)를 사용해야 함을 의미합니다.

신원 제공자의 공개 데이터도 비슷합니다. 사용자를 로그인하려면 http://localhost:3001/idp/login로 POST하고, 사용자를 로그아웃하려면 http://localhost:3001/idp/logout로 POST하도록 지정합니다.

src/sp.mts

서비스 제공자를 구현하는 코드입니다.

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

SAML을 구현하기 위해 samlify (opens in a new tab) 라이브러리를 사용합니다.

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

samlify 라이브러리는 XML이 올바른지, 예상되는 공개키로 서명되었는지 등을 검증하는 패키지가 필요합니다. 이를 위해 @authenio/samlify-node-xmllint (opens in a new tab)를 사용합니다.

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

express (opens in a new tab) Router (opens in a new tab)는 웹사이트 내에 마운트할 수 있는 "미니 웹사이트"입니다. 이 경우 모든 서비스 제공자 정의를 함께 그룹화하는 데 사용합니다.

const spPrivateKey = fs.readFileSync("keys/saml-sp.pem").toString()

const sp = saml.ServiceProvider({
  privateKey: spPrivateKey,  
  ...config.spPublicData
})

서비스 제공자 자체의 표현은 모든 공개 데이터와 정보를 서명하는 데 사용하는 개인 키입니다.

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

공개 데이터에는 서비스 제공자가 신원 제공자에 대해 알아야 할 모든 내용이 포함되어 있습니다.

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

다른 SAML 구성 요소와의 상호운용성을 활성화하려면, 서비스 및 신원 제공자는 /metadata에서 공개 데이터(메타데이터라고 함)를 XML 형식으로 사용할 수 있어야 합니다.

spRouter.post(`/assertion`,

이것은 브라우저가 자신을 식별하기 위해 접근하는 페이지입니다. 단언에는 사용자 식별자(여기서는 이메일 주소를 사용)가 포함되며, 추가 속성을 포함할 수 있습니다. 이는 위 시퀀스 다이어그램의 7단계에 대한 핸들러입니다.

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

주석 처리된 명령을 사용하여 단언에 제공된 XML 데이터를 볼 수 있습니다. 이는 base64로 인코딩 (opens in a new tab)되어 있습니다.

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

신원 서버로부터의 로그인 요청을 파싱합니다.

      res.send(`
        <html>
          <body>
            <h2>Hello ${loginResponse.extract.nameID}</h2>
          </body>
        </html>
      `)
      res.send();

사용자에게 로그인을 받았음을 보여주기 위해 HTML 응답을 보냅니다.

    } catch (err) {
      console.error('Error processing SAML response:', err);
      res.status(400).send('SAML authentication failed');
    }
  }
)

실패할 경우 사용자에게 알립니다.

spRouter.get('/login',

브라우저가 이 페이지를 가져오려고 시도할 때 로그인 요청을 생성합니다. 이는 위 시퀀스 다이어그램의 1단계에 대한 핸들러입니다.

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

로그인 요청을 POST할 정보를 가져옵니다.

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

이 페이지는 양식(아래 참조)을 자동으로 제출합니다. 이렇게 하면 사용자가 리디렉션되기 위해 아무것도 할 필요가 없습니다. 이는 위 시퀀스 다이어그램의 2단계입니다.

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

loginRequest.entityEndpoint(신원 제공자 엔드포인트의 URL)로 POST합니다.

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

입력 이름은 loginRequest.type(SAMLRequest)입니다. 해당 필드의 내용은 loginRequest.context이며, 이 역시 base64로 인코딩된 XML입니다.

          </form>
        </body>
      </html>
    `)    
  }
)

app.use(express.urlencoded({extended: true}))

이 미들웨어 (opens in a new tab)HTTP 요청 (opens in a new tab)의 본문을 읽습니다. 대부분의 요청에서는 필요하지 않기 때문에 express는 기본적으로 이를 무시합니다. POST는 본문을 사용하므로 우리는 이것이 필요합니다.

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

서비스 제공자 디렉터리(/sp)에 라우터를 마운트합니다.

브라우저가 루트 디렉터리를 가져오려고 시도하면 로그인 페이지에 대한 링크를 제공합니다.

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

이 express 애플리케이션으로 spPort를 수신 대기합니다.

src/idp.mts

이것은 신원 제공자입니다. 서비스 제공자와 매우 유사하며, 아래 설명은 다른 부분에 대한 것입니다.

const xmlParser = new (await import("fast-xml-parser")).XMLParser(
  {
    ignoreAttributes: false, // 속성 보존
    attributeNamePrefix: "@_", // 속성 접두사
  }
)

서비스 제공자로부터 받은 XML 요청을 읽고 이해해야 합니다.

const getLoginPage = requestId => `

이 함수는 위 시퀀스 다이어그램의 4단계에서 반환되는 자동 제출 양식이 있는 페이지를 생성합니다.

서비스 제공자에게 보내는 두 가지 필드가 있습니다.

  1. 우리가 응답하고 있는 requestId.
  2. 사용자 식별자(지금은 사용자가 제공하는 이메일 주소를 사용합니다).
    </form>
  </body>
</html>

const idpRouter = express.Router()

idpRouter.post("/loginSubmitted", async (req, res) => {
  const loginResponse = await idp.createLoginResponse(

이는 위 시퀀스 다이어그램의 5단계에 대한 핸들러입니다. idp.createLoginResponse (opens in a new tab)는 로그인 응답을 생성합니다.

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

대상(audience)은 서비스 제공자입니다.

      extract: {
        request: {
          id: req.body.requestId
        }
      },

요청에서 추출된 정보입니다. 요청에서 우리가 신경 쓰는 유일한 매개변수는 서비스 제공자가 요청과 응답을 일치시킬 수 있게 해주는 requestId입니다.

      signingKey: { privateKey: idpPrivateKey, publicKey: config.idpCert }  // 서명 보장

응답에 서명할 데이터를 가지려면 signingKey가 필요합니다. 서비스 제공자는 서명되지 않은 요청을 신뢰하지 않습니다.

    },
    "post",
    {
      email: req.body.email

이것은 서비스 제공자에게 다시 보내는 사용자 정보가 포함된 필드입니다.

다시 한 번, 자동 제출 양식을 사용합니다. 이는 위 시퀀스 다이어그램의 6단계입니다.


// 로그인 요청을 위한 IdP 엔드포인트
idpRouter.post(`/login`,

이것은 서비스 제공자로부터 로그인 요청을 받는 엔드포인트입니다. 이는 위 시퀀스 다이어그램의 3단계 핸들러입니다.

  async (req, res) => {
    try {
      // parseLoginRequest가 작동하지 않아 적용한 임시 방편입니다.
      // const loginRequest = await idp.parseLoginRequest(sp, 'post', req)
      const samlRequest = xmlParser.parse(Buffer.from(req.body.SAMLRequest, 'base64').toString('utf-8'))
      res.send(getLoginPage(samlRequest["samlp:AuthnRequest"]["@_ID"]))

인증 요청의 ID를 읽기 위해 idp.parseLoginRequest (opens in a new tab)를 사용할 수 있어야 합니다. 하지만 작동하게 만들 수 없었고 많은 시간을 할애할 가치가 없었기 때문에 그냥 범용 XML 파서 (opens in a new tab)를 사용했습니다. 우리에게 필요한 정보는 XML의 최상위 수준에 있는 <samlp:AuthnRequest> 태그 안의 ID 속성입니다.

이더리움 서명 사용하기

이제 서비스 제공자에게 사용자 신원을 보낼 수 있으므로, 다음 단계는 신뢰할 수 있는 방식으로 사용자 신원을 얻는 것입니다. Viem을 사용하면 지갑에 사용자 주소를 요청할 수 있지만, 이는 브라우저에 정보를 요청하는 것을 의미합니다. 우리는 브라우저를 제어하지 않으므로 브라우저에서 얻은 응답을 자동으로 신뢰할 수 없습니다.

대신, IdP는 브라우저에 서명할 문자열을 보낼 것입니다. 브라우저의 지갑이 이 문자열에 서명하면, 그것이 실제로 해당 주소임을 의미합니다(즉, 해당 주소에 해당하는 개인 키를 알고 있음을 의미합니다).

이것이 작동하는 것을 보려면 기존 IdP와 SP를 중지하고 다음 명령을 실행하세요.

git checkout eth-signatures
pnpm install
pnpm start

그런 다음 SP로 이동 (opens in a new tab)하여 지침을 따르세요.

이 시점에서는 이더리움 주소에서 이메일 주소를 가져오는 방법을 모르기 때문에 대신 SP에 <ethereum address>@bad.email.address를 보고합니다.

상세 설명

변경 사항은 이전 다이어그램의 4~5단계에 있습니다.

SAML with an Ethereum signature

변경된 유일한 파일은 idp.mts입니다. 변경된 부분은 다음과 같습니다.

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

이 두 가지 추가 라이브러리가 필요합니다. 논스 (opens in a new tab) 값을 생성하기 위해 uuid (opens in a new tab)를 사용합니다. 값 자체는 중요하지 않으며, 단 한 번만 사용된다는 사실이 중요합니다.

viem (opens in a new tab) 라이브러리를 사용하면 이더리움 정의를 사용할 수 있습니다. 여기서는 서명이 실제로 유효한지 확인하기 위해 필요합니다.

const loginPrompt = "To access the service provider, sign this nonce: "

지갑은 사용자에게 메시지에 서명할 권한을 요청합니다. 논스만 있는 메시지는 사용자를 혼란스럽게 할 수 있으므로 이 프롬프트를 포함합니다.

// 여기에 requestID 보관
let nonces = {}

요청에 응답하려면 요청 정보가 필요합니다. 요청과 함께 보내고(4단계), 다시 받을 수 있습니다(5단계). 하지만 잠재적으로 적대적인 사용자의 통제하에 있는 브라우저에서 얻은 정보는 신뢰할 수 없습니다. 따라서 논스를 키로 사용하여 여기에 저장하는 것이 좋습니다.

단순성을 위해 여기서는 변수로 처리하고 있다는 점에 유의하세요. 하지만 이 방식에는 몇 가지 단점이 있습니다.

  • 서비스 거부(DoS) 공격에 취약합니다. 악의적인 사용자가 여러 번 로그인을 시도하여 메모리를 가득 채울 수 있습니다.
  • IdP 프로세스를 다시 시작해야 하는 경우 기존 값을 잃게 됩니다.
  • 각 프로세스가 자체 변수를 가지기 때문에 여러 프로세스에 걸쳐 로드 밸런싱을 할 수 없습니다.

프로덕션 시스템에서는 데이터베이스를 사용하고 일종의 만료 메커니즘을 구현할 것입니다.

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

논스를 생성하고 나중에 사용할 수 있도록 requestId를 저장합니다.

  return `
<html>
  <head>
    <script type="module">

이 JavaScript는 페이지가 로드될 때 자동으로 실행됩니다.

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

viem의 여러 함수가 필요합니다.

      if (!window.ethereum) {
          alert("Please install MetaMask or a compatible wallet and then reload")
      }

브라우저에 지갑이 있는 경우에만 작동할 수 있습니다.

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

지갑에서 계정 목록을 요청합니다(window.ethereum). 최소한 하나가 있다고 가정하고 첫 번째 계정만 저장합니다.

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

브라우저 지갑과 상호작용할 지갑 클라이언트 (opens in a new tab)를 생성합니다.

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

사용자에게 메시지에 서명하도록 요청합니다. 이 전체 HTML은 템플릿 문자열 (opens in a new tab) 안에 있으므로 idp 프로세스에 정의된 변수를 사용할 수 있습니다. 이는 시퀀스 다이어그램의 4.5단계입니다.

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

/idp/signature/<nonce>/<address>/<signature>로 리디렉션합니다. 이는 시퀀스 다이어그램의 5단계입니다.

      window.badSignature = () => {
        const path= "/${config.idpDir}/signature/${nonce}/" + 
          getAddress("0x" + "BAD060A7".padEnd(40, "0")) + 
          "/0x" + "BAD0516".padStart(130, "0")
        window.location.href = path
      }

서명은 잠재적으로 악의적일 수 있는 브라우저에 의해 다시 전송됩니다(브라우저에서 http://localhost:3001/idp/signature/bad-nonce/bad-address/bad-signature를 그냥 여는 것을 막을 방법은 없습니다). 따라서 IdP 프로세스가 잘못된 서명을 올바르게 처리하는지 확인하는 것이 중요합니다.

나머지는 표준 HTML일 뿐입니다.

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

이는 시퀀스 다이어그램의 5단계에 대한 핸들러입니다.

  const requestId = nonces[req.params.nonce]
  if (requestId === undefined) {
    res.send("Bad nonce")
    return ;
  }  
  
  nonces[req.params.nonce] = undefined

요청 ID를 가져오고, 재사용할 수 없도록 nonces에서 논스를 삭제합니다.

  try {

서명이 유효하지 않을 수 있는 방법이 너무 많기 때문에, 발생한 오류를 잡기 위해 이를 try ... catch 블록으로 감쌉니다.

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

시퀀스 다이어그램의 5.5단계를 구현하기 위해 verifyMessage (opens in a new tab)를 사용합니다.

    if (!validSignature)
      throw("Bad signature")
  } catch (err) {
    res.send("Error:" + err)
    return ;
  }

핸들러의 나머지 부분은 한 가지 작은 변경 사항을 제외하고 이전에 /loginSubmitted 핸들러에서 수행한 작업과 동일합니다.

  const loginResponse = await idp.createLoginResponse(
      .
      .
      .
    {
      email: req.params.account + "@bad.email.address"
    }
  );

실제 이메일 주소가 없으므로(다음 섹션에서 가져올 것입니다), 지금은 이더리움 주소를 반환하고 이메일 주소가 아님을 명확하게 표시합니다.

이제 3단계 핸들러에서 getLoginPage 대신 getSignaturePage를 사용합니다.

이메일 주소 가져오기

다음 단계는 서비스 제공자가 요청한 식별자인 이메일 주소를 얻는 것입니다. 이를 위해 이더리움 증명 서비스(EAS) (opens in a new tab)를 사용합니다.

증명을 얻는 가장 쉬운 방법은 GraphQL API (opens in a new tab)를 사용하는 것입니다. 다음 쿼리를 사용합니다.

schemaId (opens in a new tab)에는 이메일 주소만 포함되어 있습니다. 이 쿼리는 이 스키마의 증명을 요청합니다. 증명의 주체는 recipient라고 합니다. 이는 항상 이더리움 주소입니다.

경고: 여기서 증명을 얻는 방식에는 두 가지 보안 문제가 있습니다.

  • 우리는 중앙화된 구성 요소인 API 엔드포인트 https://optimism.easscan.org/graphql로 이동합니다. id 속성을 가져온 다음 온체인에서 조회하여 증명이 진짜인지 확인할 수 있지만, API 엔드포인트는 여전히 우리에게 알려주지 않는 방식으로 증명을 검열할 수 있습니다.

    이 문제를 해결하는 것이 불가능한 것은 아닙니다. 자체 GraphQL 엔드포인트를 실행하고 체인 로그에서 증명을 가져올 수 있지만, 이는 우리의 목적에 비해 과도합니다.

  • 우리는 증명자의 신원을 확인하지 않습니다. 누구나 우리에게 거짓 정보를 제공할 수 있습니다. 실제 구현에서는 신뢰할 수 있는 증명자 집합을 두고 그들의 증명만 확인할 것입니다.

이것이 작동하는 것을 보려면 기존 IdP와 SP를 중지하고 다음 명령을 실행하세요.

git checkout email-address
pnpm install
pnpm start

그런 다음 이메일 주소를 제공하세요. 두 가지 방법이 있습니다.

  • 개인 키를 사용하여 지갑을 가져오고, 테스트용 개인 키 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80를 사용합니다.

  • 자신의 이메일 주소에 대한 증명을 추가합니다.

    1. 증명 탐색기의 스키마 (opens in a new tab)로 이동합니다.

    2. Attest with Schema를 클릭합니다.

    3. 수신자로 이더리움 주소를, 이메일 주소로 자신의 이메일 주소를 입력하고 Onchain을 선택합니다. 그런 다음 Make Attestation을 클릭합니다.

    4. 지갑에서 트랜잭션을 승인합니다. 가스비를 지불하려면 옵티미즘 블록체인 (opens in a new tab)에 약간의 ETH가 필요합니다.

어느 쪽이든 이 작업을 수행한 후 http://localhost:3000 (opens in a new tab)으로 이동하여 지침을 따르세요. 테스트용 개인 키를 가져온 경우 수신하는 이메일은 test_addr_0@example.com입니다. 자신의 주소를 사용한 경우 증명한 내용이 무엇이든 표시되어야 합니다.

상세 설명

Getting from Ethereum address to e-mail

새로운 단계는 GraphQL 통신인 5.6단계와 5.7단계입니다.

다시 한 번, idp.mts의 변경된 부분은 다음과 같습니다.

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

필요한 라이브러리를 가져옵니다.

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

각 블록체인마다 별도의 엔드포인트 (opens in a new tab)가 있습니다.

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

엔드포인트를 쿼리하는 데 사용할 수 있는 새로운 GraphQLClient 클라이언트를 생성합니다.

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

GraphQL은 바이트가 포함된 불투명한 데이터 객체만 제공합니다. 이를 이해하려면 스키마가 필요합니다.

const ethereumAddressToEmail = async ethAddr => {

이더리움 주소에서 이메일 주소를 가져오는 함수입니다.

  const query = `
    query GetAttestationsByRecipient {

이것은 GraphQL 쿼리입니다.

      attestations(

우리는 증명을 찾고 있습니다.

        where: { 
          recipient: { equals: "${getAddress(ethAddr)}" }
          schemaId: { equals: "0xfa2eff59a916e3cc3246f9aec5e0ca00874ae9d09e4678e5016006f07622f977" }
        }

우리가 원하는 증명은 수신자가 getAddress(ethAddr)인 스키마의 증명입니다. getAddress (opens in a new tab) 함수는 우리 주소가 올바른 체크섬 (opens in a new tab)을 갖도록 보장합니다. GraphQL은 대소문자를 구분하므로 이는 필수적입니다. "0xBAD060A7", "0xBad060A7", "0xbad060a7"은 서로 다른 값입니다.

        take: 1

얼마나 많은 증명을 찾든 상관없이 첫 번째 증명만 원합니다.

      ) {
        data
        id
        attester
      }
    }`

우리가 받고자 하는 필드입니다.

  • attester: 증명을 제출한 주소입니다. 일반적으로 증명을 신뢰할지 여부를 결정하는 데 사용됩니다.
  • id: 증명 ID입니다. 이 값을 사용하여 온체인에서 증명을 읽고 (opens in a new tab) GraphQL 쿼리의 정보가 올바른지 확인할 수 있습니다.
  • data: 스키마 데이터(이 경우 이메일 주소)입니다.
  const queryResult = await graphqlClient.request(query)

  if (queryResult.attestations.length == 0)
    return "no_address@available.is"

증명이 없는 경우, 명백히 잘못되었지만 서비스 제공자에게는 유효해 보이는 값을 반환합니다.

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

값이 있는 경우 decodeData를 사용하여 데이터를 디코딩합니다. 제공되는 메타데이터는 필요하지 않으며 값 자체만 필요합니다.

새로운 함수를 사용하여 이메일 주소를 가져옵니다.

탈중앙화는 어떻게 되나요?

이 구성에서 이더리움과 이메일 주소 매핑을 위해 신뢰할 수 있는 증명자에 의존하는 한, 사용자는 자신이 아닌 다른 사람인 척할 수 없습니다. 하지만 우리의 신원 제공자는 여전히 중앙화된 구성 요소입니다. 신원 제공자의 개인 키를 가진 사람은 누구나 서비스 제공자에게 거짓 정보를 보낼 수 있습니다.

다자간 컴퓨팅(MPC) (opens in a new tab)을 사용하는 해결책이 있을 수 있습니다. 향후 튜토리얼에서 이에 대해 다룰 수 있기를 바랍니다.

결론

이더리움 서명과 같은 로그인 표준의 채택은 닭과 달걀의 문제에 직면해 있습니다. 서비스 제공자는 가능한 한 가장 넓은 시장에 어필하고 싶어 합니다. 사용자는 자신의 로그인 표준 지원 여부를 걱정하지 않고 서비스에 접근할 수 있기를 원합니다. 이더리움 IdP와 같은 어댑터를 만들면 이러한 장애물을 극복하는 데 도움이 될 수 있습니다.

제 다른 작업물은 여기에서 확인하세요 (opens in a new tab).

페이지 최근 업데이트: 2026년 4월 3일