メインコンテンツへスキップ

Web2認証にイーサリアムを使用する

web2
認証
eas
初級
Ori Pomerantz
2025年4月30日
31 分の読書

はじめに

SAML (opens in a new tab)は、Web2でIDプロバイダー (IdP) (opens in a new tab)サービスプロバイダー (SP) (opens in a new tab)にユーザー情報を提供するために使用される標準です。

このチュートリアルでは、イーサリアム署名をSAMLと統合し、まだイーサリアムをネイティブにサポートしていないWeb2サービスに対して、ユーザーがイーサリアムウォレットを使用して認証できるようにする方法を学びます。

このチュートリアルは、2つの異なる読者を対象に書かれていることに注意してください。

  • イーサリアムを理解していて、SAMLを学ぶ必要があるイーサリアム関係者
  • SAMLとWeb2認証を理解していて、イーサリアムを学ぶ必要があるWeb2関係者

そのため、すでにご存知の入門的な内容が多く含まれています。 適宜読み飛ばしてください。

イーサリアム関係者向けのSAML

SAMLは中央集権型のプロトコルです。 サービスプロバイダー (SP) は、IDプロバイダー (IdP) との間、またはそのIdPの証明書に署名した証明書認証局 (opens in a new tab)との間に、既存の信頼関係がある場合にのみ、IdPからのアサーション (「これは私のユーザーJohnで、A、B、Cを行う権限を持つべきである」など) を受け入れます。

たとえば、SPは企業に旅行サービスを提供する旅行代理店、IdPは企業の社内ウェブサイトであるとします。 従業員が出張の予約をする際、旅行代理店は、実際に旅行の予約を許可する前に、従業員を会社の認証に送ります。

SAMLプロセスのステップバイステップ

これが、ブラウザ、SP、IdPの3つのエンティティがアクセスを交渉する方法です。 SPは、ブラウザを使用しているユーザーについて事前に何も知る必要はなく、IdPを信頼するだけで済みます。

SAML関係者向けのイーサリアム

イーサリアムは分散型システムです。

イーサリアムのログオン

ユーザーは秘密鍵を持っています (通常はブラウザ拡張機能に保存されています)。 秘密鍵から公開鍵を導出し、そこから20バイトのアドレスを導出できます。 ユーザーがシステムにログインする必要がある場合、ノンス (1回限りの値) を持つメッセージに署名するよう要求されます。 サーバーは、署名がそのアドレスによって作成されたことを検証できます。

アテステーションから追加データを取得

署名はイーサリアムアドレスを検証するだけです。 他のユーザー属性を取得するには、通常アテステーション (opens in a new tab)を使用します。 アテステーションには通常、以下のフィールドがあります。

  • 証明者、アテステーションを行ったアドレス
  • 受取人、アテステーションが適用されるアドレス
  • データ、名前や権限など、証明されるデータ
  • スキーマ、データの解釈に使用されるスキーマのID。

イーサリアムの分散型の性質により、どのユーザーでもアテステーションを作成できます。 どのアテステーションが信頼できるかを判断するには、証明者のIDが重要です。

セットアップ

最初のステップは、SAML SPとSAML IdPが互いに通信できるようにすることです。

  1. ソフトウェアをダウンロードします。 この記事のサンプルソフトウェアは、github (opens in a new tab)にあります。 異なるステージは異なるブランチに保存されています。このステージでは saml-only を使用します。

    1git clone https://github.com/qbzzt/250420-saml-ethereum -b saml-only
    2cd 250420-saml-ethereum
    3pnpm install
  2. 自己署名証明書でキーを作成します。 これは、キーがそれ自体の証明書認証局であることを意味し、サービスプロバイダーに手動でインポートする必要があります。 詳細については、OpenSSLのドキュメント (opens in a new tab)を参照してください。

    1mkdir keys
    2cd keys
    3openssl req -new -x509 -days 365 -nodes -sha256 -out saml-sp.crt -keyout saml-sp.pem -subj /CN=sp/
    4openssl req -new -x509 -days 365 -nodes -sha256 -out saml-idp.crt -keyout saml-idp.pem -subj /CN=idp/
    5cd ..
  3. サーバー (SPとIdPの両方) を起動します。

    1pnpm start
  4. URL http://localhost:3000/ (opens in a new tab)でSPにアクセスし、ボタンをクリックしてIdP (ポート3001) にリダイレクトします。

  5. IdPにメールアドレスを入力し、サービスプロバイダーにログインをクリックします。 サービスプロバイダー (ポート3000) にリダイレクトされ、メールアドレスで認識されていることを確認します。

詳細な説明

ステップバイステップで起こることは次のとおりです。

イーサリアムを使用しない通常のSAMLログオン

src/config.mts

このファイルには、IDプロバイダーとサービスプロバイダー両方の設定が含まれています。 通常、これら2つは異なるエンティティですが、ここでは簡潔にするためにコードを共有します。

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

今のところテストだけなので、HTTPを使用して問題ありません。

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

通常は両方のコンポーネントで利用可能な公開鍵 (直接信頼されるか、信頼された証明書認証局によって署名される) を読み取ります。

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}`
すべて表示

両コンポーネントのURLです。

1export const spPublicData = {

サービスプロバイダーの公開データです。

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

慣例的に、SAMLではentityIDはエンティティのメタデータが利用可能なURLです。 このメタデータはここの公開データに対応しますが、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 }
すべて表示

私たちの目的にとって最も重要な定義は assertionConsumerServer です。 これは、サービスプロバイダーに何かをアサートする (例えば、「この情報を送信するユーザーは somebody@example.com (opens email client) です」) ためには、URL http://localhost:3000/sp/assertionHTTP POST (opens in a new tab) を使用する必要があることを意味します。

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 }
すべて表示

IDプロバイダーの公開データも同様です。 これは、ユーザーをログインさせるには http://localhost:3001/idp/login にPOSTし、ユーザーをログアウトさせるには http://localhost:3001/idp/logout にPOSTすることを指定しています。

src/sp.mts

これはサービスプロバイダーを実装するコードです。

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

SAMLを実装するために samlify (opens in a new tab) ライブラリを使用します。

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

samlify ライブラリは、XMLが正しいこと、期待される公開鍵で署名されていることなどを検証するパッケージを必要とします。 この目的のために @authenio/samlify-node-xmllint (opens in a new tab) を使用します。

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

express (opens in a new tab)Router (opens in a new tab)は、ウェブサイト内にマウントできる「ミニウェブサイト」です。 この場合、すべてのサービスプロバイダーの定義をグループ化するために使用します。

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

サービスプロバイダー自身の表現は、すべての公開データと、情報に署名するために使用する秘密鍵です。

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

公開データには、サービスプロバイダーがIDプロバイダーについて知る必要があるすべてのものが含まれています。

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

他のSAMLコンポーネントとの相互運用性を確保するため、サービスプロバイダーとIDプロバイダーは、公開データ (メタデータと呼ばれる) をXML形式で /metadata にて利用可能にする必要があります。

1spRouter.post(`/assertion`,

これは、ブラウザが自身を識別するためにアクセスするページです。 アサーションにはユーザー識別子 (ここではメールアドレスを使用) が含まれ、追加の属性を含めることもできます。 これは、上記のシーケンス図のステップ7のハンドラです。

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

コメントアウトされたコマンドを使用して、アサーションで提供されるXMLデータを確認できます。 これはbase64でエンコード (opens in a new tab)されています。

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

IDサーバーからのログインリクエストを解析します。

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

ユーザーにログインが成功したことを示すために、HTMLレスポンスを送信します。

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

失敗した場合は、ユーザーに通知します。

1spRouter.get('/login',

ブラウザがこのページを取得しようとするときに、ログインリクエストを作成します。 これは、上記のシーケンス図のステップ1のハンドラです。

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

ログインリクエストをPOSTするための情報を取得します。

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

このページは、フォーム (下記参照) を自動的に送信します。 これにより、ユーザーはリダイレクトされるために何もする必要がありません。 これは、上記のシーケンス図のステップ2です。

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

loginRequest.entityEndpoint (IDプロバイダーエンドポイントのURL) にPOSTします。

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

入力名は loginRequest.type (SAMLRequest) です。 そのフィールドのコンテンツは loginRequest.context で、これもbase64エンコードされたXMLです。

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

このミドルウェア (opens in a new tab)HTTPリクエスト (opens in a new tab)のボディを読み取ります。 ほとんどのリクエストはそれを必要としないため、Expressはデフォルトでそれを無視します。 POSTはボディを使用するため、これが必要です。

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

サービスプロバイダーディレクトリ (/sp) にルーターをマウントします。

1app.get("/", (req, res) => {
2 res.send(`
3 <html>
4 <body>
5 <button onClick="document.location.href='${config.spUrl}/login'">
6 ここをクリックしてログオン
7 </button>
8 </body>
9 </html>
10 `)
11})
すべて表示

ブラウザがルートディレクトリを取得しようとした場合は、ログインページへのリンクを提供します。

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

このExpressアプリケーションで spPort をリッスンします。

src/idp.mts

これはIDプロバイダーです。 これはサービスプロバイダーと非常によく似ており、以下の説明は異なる部分についてです。

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

サービスプロバイダーから受け取ったXMLリクエストを読み取り、理解する必要があります。

1const getLoginPage = requestId => `

この関数は、上記のシーケンス図のステップ4で返される、自動送信フォーム付きのページを作成します。

1<html>
2 <head>
3 <title>ログインページ</title>
4 </head>
5 <body>
6 <h2>ログインページ</h2>
7 <form method="post" action="./loginSubmitted">
8 <input type="hidden" name="requestId" value="${requestId}" />
9 メールアドレス: <input name="email" />
10 <br />
11 <button type="Submit">
12 サービスプロバイダーにログイン
13 </button>
すべて表示

サービスプロバイダーに送信するフィールドは2つあります。

  1. 応答する requestId
  2. ユーザー識別子 (ここではユーザーが提供するメールアドレスを使用)。
1 </form>
2 </body>
3</html>
4
5const idpRouter = express.Router()
6
7idpRouter.post("/loginSubmitted", async (req, res) => {
8 const loginResponse = await idp.createLoginResponse(

これは、上記のシーケンス図のステップ5のハンドラです。 idp.createLoginResponse (opens in a new tab)はログインレスポンスを作成します。

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

オーディエンスはサービスプロバイダーです。

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

リクエストから抽出された情報です。 リクエストで重要なパラメータはrequestIdで、これによりサービスプロバイダーはリクエストとそのレスポンスを一致させることができます。

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

レスポンスに署名するためのデータを持つために signingKey が必要です。 サービスプロバイダーは署名されていないリクエストを信頼しません。

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

これは、サービスプロバイダーに送り返すユーザー情報を持つフィールドです。

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})
すべて表示

ここでも、自動送信フォームを使用します。 これは、上記のシーケンス図のステップ6です。

1
2// ログインリクエスト用のIdPエンドポイント
3idpRouter.post(`/login`,

これはサービスプロバイダーからログインリクエストを受け取るエンドポイントです。 これは、上記のシーケンス図のステップ3のハンドラです。

1 async (req, res) => {
2 try {
3 // 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"]))

idp.parseLoginRequest (opens in a new tab)を使用して認証リクエストのIDを読み取ることができるはずです。 しかし、動作させることができず、それに多くの時間を費やす価値もなかったので、汎用XMLパーサー (opens in a new tab)を使用します。 必要な情報は、XMLのトップレベルにある <samlp:AuthnRequest> タグ内の ID 属性です。

イーサリアム署名の使用

ユーザーIDをサービスプロバイダーに送信できるようになったので、次のステップは信頼できる方法でユーザーIDを取得することです。 Viemを使用すると、ウォレットにユーザーアドレスを尋ねるだけで済みますが、これはブラウザに情報を要求することを意味します。 私たちはブラウザを制御していないため、そこから得られる応答を自動的に信頼することはできません。

代わりに、IdPはブラウザに署名するための文字列を送信します。 ブラウザのウォレットがこの文字列に署名すれば、それが本当にそのアドレスであること (つまり、そのアドレスに対応する秘密鍵を知っていること) を意味します。

これを実際に確認するには、既存のIdPとSPを停止し、次のコマンドを実行します。

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

次に、SP (opens in a new tab)にアクセスし、指示に従ってください。

この時点では、イーサリアムアドレスからメールアドレスを取得する方法がわからないため、代わりに <イーサリアムアドレス>@bad.email.address をSPに報告します。

詳細な説明

変更点は、前の図のステップ4-5にあります。

イーサリアム署名付きSAML

変更したファイルは idp.mts だけです。 以下が変更された部分です。

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

これら2つの追加ライブラリが必要です。 nonce (opens in a new tab)値を作成するために uuid (opens in a new tab) を使用します。 値自体は問題ではなく、一度しか使用されないという事実が重要です。

viem (opens in a new tab) ライブラリを使用すると、イーサリアムの定義を使用できます。 ここでは、署名が実際に有効であることを検証するためにこれが必要です。

1const loginPrompt = "サービスプロバイダーにアクセスするには、このノンスに署名してください: "

ウォレットは、ユーザーにメッセージに署名する許可を求めます。 ノンスだけのメッセージはユーザーを混乱させる可能性があるため、このプロンプトを含めます。

1// ここにrequestIDを保持する
2let nonces = {}

応答するためには、リクエスト情報が必要です。 リクエストと一緒に送信し (ステップ4)、それを受け取る (ステップ5) こともできます。 しかし、潜在的に敵対的なユーザーの制御下にあるブラウザから得られる情報は信頼できません。 したがって、ノンスをキーとしてここに保存する方が良いでしょう。

簡潔さのために、ここでは変数としてこれを行っていることに注意してください。 ただし、これにはいくつかの欠点があります。

  • サービス拒否攻撃に対して脆弱です。 悪意のあるユーザーが複数回ログオンを試み、メモリを使い果たす可能性があります。
  • IdPプロセスを再起動する必要がある場合、既存の値を失います。
  • 各プロセスが独自の変数を持つため、複数のプロセス間で負荷分散を行うことはできません。

本番システムでは、データベースを使用し、何らかの有効期限メカニズムを実装します。

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

ノンスを作成し、後で使用するために requestId を保存します。

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

このJavaScriptは、ページが読み込まれると自動的に実行されます。

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

viemからいくつかの関数が必要です。

1 if (!window.ethereum) {
2 alert("MetaMaskまたは互換性のあるウォレットをインストールしてからリロードしてください")
3 }

ブラウザにウォレットがある場合にのみ機能します。

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

ウォレット (window.ethereum) からアカウントのリストを要求します。 少なくとも1つあると仮定し、最初のものだけを保存します。

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

ブラウザウォレットとやり取りするためのウォレットクライアント (opens in a new tab)を作成します。

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

ユーザーにメッセージへの署名を依頼します。 このHTML全体がテンプレート文字列 (opens in a new tab)内にあるため、idpプロセスで定義された変数を使用できます。 これはシーケンス図のステップ4.5です。

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

/idp/signature/<nonce>/<address>/<signature> にリダイレクトします。 これはシーケンス図のステップ5です。

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 }

署名はブラウザから返送されますが、これは潜在的に悪意のある可能性があります (ブラウザで http://localhost:3001/idp/signature/bad-nonce/bad-address/bad-signature を開くのを止めるものは何もありません)。 したがって、IdPプロセスが不正な署名を正しく処理することを確認することが重要です。

1 </script>
2 </head>
3 <body>
4 <h2>署名してください</h2>
5 <button onClick="window.goodSignature()">
6 良好な (有効な) 署名を送信
7 </button>
8 <br/>
9 <button onClick="window.badSignature()">
10 不正な (無効な) 署名を送信
11 </button>
12 </body>
13</html>
14`
15}
すべて表示

残りは標準のHTMLです。

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

これはシーケンス図のステップ5のハンドラです。

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

リクエストIDを取得し、再利用されないように nonces からノンスを削除します。

1 try {

署名が無効になる方法が非常に多いため、これを try ... でラップします。 catch`ブロックで、スローされたエラーをキャッチします。

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

シーケンス図のステップ5.5を実装するには、verifyMessage (opens in a new tab) を使用します。

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

ハンドラの残りの部分は、1つの小さな変更を除いて、以前に /loginSubmitted ハンドラで行ったことと同等です。

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

実際のメールアドレスは持っていないので (次のセクションで取得します)、今のところはイーサリアムアドレスを返し、それがメールアドレスではないことを明確にマークします。

1// ログインリクエスト用のIdPエンドポイント
2idpRouter.post(`/login`,
3 async (req, res) => {
4 try {
5 // 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('SAMLレスポンスの処理中にエラーが発生しました:', err);
11 res.status(400).send('SAML認証に失敗しました');
12 }
13 }
14)
すべて表示

ステップ3のハンドラでは、getLoginPage の代わりに getSignaturePage を使用します。

メールアドレスの取得

次のステップは、サービスプロバイダーによって要求された識別子であるメールアドレスを取得することです。 そのためには、Ethereum Attestation Service (EAS) (opens in a new tab) を使用します。

アテステーションを取得する最も簡単な方法は、GraphQL API (opens in a new tab)を使用することです。 このクエリを使用します。

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}
すべて表示

このschemaId (opens in a new tab)には、メールアドレスのみが含まれます。 このクエリは、このスキーマのアテステーションを要求します。 アテステーションのサブジェクトは recipient と呼ばれます。 これは常にイーサリアムアドレスです。

警告:ここでアテステーションを取得する方法には、2つのセキュリティ上の問題があります。

  • 中央集権的なコンポーネントであるAPIエンドポイント https://optimism.easscan.org/graphql にアクセスしています。 id属性を取得し、オンチェーンでルックアップしてアテステーションが本物であることを確認できますが、APIエンドポイントは、アテステーションについて通知しないことで、依然としてアテステーションを検閲できます。

    この問題は解決不可能ではありません。独自のGraphQLエンドポイントを実行し、チェーンログからアテステーションを取得できますが、私たちの目的には過剰です。

  • 私たちは証明者のIDを見ていません。 誰でも偽の情報を私たちに与えることができます。 実際の環境では、信頼できる証明者のセットを持ち、彼らのアテステーションのみを参照します。

これを実際に確認するには、既存のIdPとSPを停止し、次のコマンドを実行します。

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

次に、メールアドレスを入力します。 これを行うには2つの方法があります。

  • 秘密鍵を使用してウォレットをインポートし、テスト用の秘密鍵 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 を使用します。

  • 自分のメールアドレスにアテステーションを追加します。

    1. アテステーションエクスプローラーのスキーマ (opens in a new tab)に移動します。

    2. スキーマで証明をクリックします。

    3. 受信者としてイーサリアムアドレスを入力し、メールアドレスとしてメールアドレスを入力し、オンチェーンを選択します。 次に、アテステーションを作成をクリックします。

    4. ウォレットでトランザクションを承認します。 ガスを支払うために、Optimism Blockchain (opens in a new tab)にETHが必要です。

どちらの場合も、これを行った後、http://localhost:3000 (opens in a new tab) にアクセスして指示に従ってください。 テスト用の秘密鍵をインポートした場合、受け取るメールは test_addr_0@example.com です。 独自のアドレスを使用した場合は、証明した内容になります。

詳細な説明

イーサリアムアドレスからメールアドレスへの変換

新しいステップはGraphQL通信、ステップ5.6と5.7です。

再度、idp.mtsの変更部分を示します。

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

必要なライブラリをインポートします。

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

各ブロックチェーンに個別のエンドポイント (opens in a new tab)があります。

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

エンドポイントのクエリに使用できる新しいGraphQLClientクライアントを作成します。

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

GraphQLは、バイトを持つ不透明なデータオブジェクトのみを提供します。 それを理解するためにはスキーマが必要です。

1const ethereumAddressToEmail = async ethAddr => {

イーサリアムアドレスからメールアドレスを取得する関数です。

1 const query = `
2 query GetAttestationsByRecipient {

これはGraphQLクエリです。

1 アテステーション(

アテステーションを探しています。

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

必要なアテステーションは、私たちのスキーマにあり、受信者がgetAddress(ethAddr)であるものです。 getAddress (opens in a new tab)関数は、アドレスが正しいチェックサム (opens in a new tab)を持つことを保証します。 GraphQLは大文字と小文字を区別するため、これは必要です。 「0xBAD060A7」、「0xBad060A7」、および「0xbad060a7」は異なる値です。

1 take: 1

見つかったアテステーションの数に関係なく、最初のものだけが必要です。

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

受け取りたいフィールドです。

  • attester: アテステーションを送信したアドレス。 通常、これはアテステーションを信頼するかどうかを決定するために使用されます。
  • id: アテステーションID。 この値を使用して、アテステーションをオンチェーンで読み取り (opens in a new tab)、GraphQLクエリからの情報が正しいことを確認できます。
  • data: スキーマデータ (この場合はメールアドレス)。
1 const queryResult = await graphqlClient.request(query)
2
3 if (queryResult.attestations.length == 0)
4 return "no_address@available.is"

アテステーションがない場合は、明らかに不正であるが、サービスプロバイダーには有効に見える値を返します。

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

値がある場合は、decodeDataを使用してデータをデコードします。 提供されるメタデータは不要で、値自体のみが必要です。

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 );
すべて表示

新しい関数を使用してメールアドレスを取得します。

分散化については?

この構成では、イーサリアムとメールアドレスのマッピングに信頼できる証明者に依存している限り、ユーザーは他人になりすますことはできません。 しかし、私たちのIDプロバイダーは依然として中央集権的なコンポーネントです。 IDプロバイダーの秘密鍵を持っている人は誰でも、サービスプロバイダーに偽の情報を送信できます。

マルチパーティ計算 (MPC) (opens in a new tab) を使用した解決策があるかもしれません。 将来のチュートリアルでそれについて書きたいと思っています。

まとめ

イーサリアム署名のようなログオン標準の採用は、鶏が先か卵が先かの問題に直面します。 サービスプロバイダーは、可能な限り広い市場にアピールしたいと考えています。 ユーザーは、ログオン標準のサポートを心配することなく、サービスにアクセスできることを望んでいます。 イーサリアムIdPなどのアダプターを作成することは、このハードルを乗り越えるのに役立ちます。

私の他の作品はこちらでご覧いただけます (opens in a new tab).

最終更新: 2025年11月23日

このチュートリアルは役に立ちましたか?