Ana içeriğe geç

web2 kimlik doğrulaması için Ethereum'u kullanma

web2
kimlik doğrulama
eas
Acemi
Ori Pomerantz
30 Nisan 2025
16 dakikalık okuma

Giriş

SAML (opens in a new tab), bir kimlik sağlayıcının (IdP) (opens in a new tab), hizmet sağlayıcılara (SP) (opens in a new tab) Kullanıcı bilgileri sağlamasına izin vermek için web2'de kullanılan bir standarttır.

Bu öğreticide, Kullanıcıların Ethereum'u henüz yerel olarak desteklemeyen web2 hizmetlerinde kimliklerini doğrulamak için Ethereum cüzdanlarını kullanmalarına olanak sağlamak üzere Ethereum imzalarını SAML ile nasıl entegre edeceğinizi öğreneceksiniz.

Bu öğreticinin iki ayrı kitle için yazıldığını unutmayın:

  • Ethereum'u anlayan ve SAML öğrenmesi gereken Ethereum meraklıları
  • SAML ve web2 kimlik doğrulamasını anlayan ve Ethereum öğrenmesi gereken Web2 meraklıları

Sonuç olarak, zaten bildiğiniz birçok giriş materyali içerecektir. Atlamaktan çekinmeyin.

Ethereum meraklıları için SAML

SAML merkezi bir protokoldür. bir hizmet sağlayıcı (SP), bir kimlik sağlayıcıdan (IdP) gelen iddiaları (örneğin \

Örneğin, SP şirketlere seyahat hizmetleri sağlayan bir seyahat acentesi olabilir ve IdP bir şirketin dahili web sitesi olabilir. Çalışanların iş seyahati rezervasyonu yapması gerektiğinde, seyahat acentesi, gerçekten seyahat rezervasyonu yapmalarına izin vermeden önce onları kimlik doğrulaması için şirkete gönderir.

Adım adım SAML süreci

Bu, üç varlığın, tarayıcının, SP'nin ve IdP'nin erişim için müzakere etme şeklidir. SP'nin tarayıcıyı kullanan Kullanıcı hakkında önceden bir şey bilmesine gerek yoktur, sadece IdP'ye güvenmesi yeterlidir.

SAML meraklıları için Ethereum

Ethereum merkeziyetsiz bir sistemdir.

Ethereum girişi

Kullanıcıların bir özel anahtarı vardır (genellikle bir tarayıcı uzantısında tutulur). Özel anahtardan bir açık anahtar ve ondan da 20 baytlık bir adres türetebilirsiniz. Kullanıcıların bir sisteme giriş yapması gerektiğinde, onlardan bir nonce (tek kullanımlık bir değer) ile bir mesaj imzalamaları istenir. Sunucu, imzanın bu adres tarafından oluşturulduğunu doğrulayabilir.

Tasdiklerden ekstra veri alma

İmza yalnızca Ethereum adresini doğrular. Diğer kullanıcı niteliklerini almak için genellikle tasdikleri (opens in a new tab) kullanırsınız. Bir tasdik genellikle şu alanlara sahiptir:

  • Tasdik eden, tasdiki yapan adres
  • Alıcı, tasdikin geçerli olduğu adres
  • Veri, ad, izinler vb. gibi tasdik edilen veriler.
  • Şema, verileri yorumlamak için kullanılan şemanın kimliği.

Ethereum'un merkeziyetsiz doğası nedeniyle, herhangi bir kullanıcı tasdik yapabilir. Tasdik edenin kimliği, hangi tasdikleri güvenilir olarak kabul ettiğimizi belirlemek için önemlidir.

Kurulum

İlk adım, kendi aralarında iletişim kuran bir SAML SP ve bir SAML IdP'ye sahip olmaktır.

  1. Yazılımı indirin. Bu makalenin örnek yazılımı github'da (opens in a new tab). Farklı aşamalar farklı dallarda saklanır, bu aşama için saml-only istersiniz

    1git clone https://github.com/qbzzt/250420-saml-ethereum -b saml-only
    2cd 250420-saml-ethereum
    3pnpm install
  2. Kendinden imzalı sertifikalarla anahtarlar oluşturun. Bu, anahtarın kendi sertifika yetkilisi olduğu ve hizmet sağlayıcıya manuel olarak içe aktarılması gerektiği anlamına gelir. Daha fazla bilgi için OpenSSL belgelerine (opens in a new tab) bakın.

    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. Sunucuları başlatın (hem SP hem de IdP)

    1pnpm start
  4. http://localhost:3000/ (opens in a new tab) URL'sindeki SP'ye gidin ve IdP'ye (bağlantı noktası 3001) yönlendirilmek için düğmeye tıklayın.

  5. IdP'ye e-posta adresinizi verin ve Hizmet sağlayıcıda oturum aç'a tıklayın. Hizmet sağlayıcıya geri yönlendirildiğinizi (bağlantı noktası 3000) ve sizi e-posta adresinizle tanıdığını görün.

Ayrıntılı açıklama

Adım adım olanlar şunlardır:

Ethereum olmadan normal SAML girişi

src/config.mts

Bu dosya hem Kimlik Sağlayıcı hem de Hizmet Sağlayıcı için yapılandırmayı içerir. Normalde bu ikisi farklı varlıklar olurdu, ancak burada basitlik için kodu paylaşabiliriz.

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

Şimdilik sadece test ediyoruz, bu yüzden HTTP kullanmakta bir sakınca yok.

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

Normalde her iki bileşen için de mevcut olan (ve doğrudan güvenilen veya güvenilir bir sertifika yetkilisi tarafından imzalanan) açık anahtarları okuyun.

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}`
Tümünü göster

Her iki bileşen için URL'ler.

1export const spPublicData = {

Hizmet sağlayıcı için genel veriler.

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

Geleneksel olarak SAML'de entityID, varlığın meta verilerinin bulunduğu URL'dir. Bu meta veriler, XML biçiminde olması dışında, buradaki genel verilere karşılık gelir.

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 }
Tümünü göster

Amaçlarımız için en önemli tanım assertionConsumerServer'dır. Hizmet sağlayıcıya bir şey iddia etmek (örneğin, \

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 }
Tümünü göster

Kimlik sağlayıcı için genel veriler benzerdir. Bir kullanıcının oturumunu açmak için http://localhost:3001/idp/login adresine POST ve bir kullanıcının oturumunu kapatmak için http://localhost:3001/idp/logout adresine POST yapmanız gerektiğini belirtir.

src/sp.mts

Bu, bir hizmet sağlayıcıyı uygulayan koddur.

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

SAML'yi uygulamak için samlify (opens in a new tab) kütüphanesini kullanıyoruz.

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

samlify kütüphanesi, XML'in doğru olduğunu, beklenen açık anahtarla imzalandığını vb. doğrulayan bir pakete sahip olmayı bekler. Bu amaçla @authenio/samlify-node-xmllint (opens in a new tab) kullanıyoruz.

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

Bir express (opens in a new tab) Router (opens in a new tab), bir web sitesinin içine monte edilebilen bir \ Bu durumda, tüm hizmet sağlayıcı tanımlarını bir araya getirmek için kullanırız.

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

Hizmet sağlayıcının kendi temsili, tüm genel veriler ve bilgileri imzalamak için kullandığı özel anahtardır.

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

Genel veriler, hizmet sağlayıcının kimlik sağlayıcı hakkında bilmesi gereken her şeyi içerir.

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

Diğer SAML bileşenleriyle birlikte çalışabilirliği sağlamak için, hizmet ve kimlik sağlayıcıların genel verileri (meta veriler olarak adlandırılır) /metadata içinde XML biçiminde mevcut olmalıdır.

1spRouter.post(`/assertion`,

Bu, tarayıcının kendini tanıtmak için eriştiği sayfadır. Beyan, kullanıcı tanımlayıcısını (burada e-posta adresi kullanıyoruz) içerir ve ek nitelikler içerebilir. Bu, yukarıdaki sıra diyagramındaki 7. adımın işleyicisidir.

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

Onayda sağlanan XML verilerini görmek için yorum satırı yapılmış komutu kullanabilirsiniz. Base64 kodludur (opens in a new tab).

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

Kimlik sunucusundan gelen oturum açma isteğini ayrıştırın.

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

Sadece kullanıcıya giriş yaptığımızı göstermek için bir HTML yanıtı gönderin.

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

Başarısızlık durumunda kullanıcıyı bilgilendirin.

1spRouter.get('/login',

Tarayıcı bu sayfayı almaya çalıştığında bir oturum açma isteği oluşturun. Bu, yukarıdaki sıra diyagramındaki 1. adımın işleyicisidir.

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

Bir oturum açma isteği göndermek için bilgileri alın.

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

Bu sayfa formu (aşağıya bakın) otomatik olarak gönderir. Bu şekilde kullanıcının yönlendirilmek için herhangi bir şey yapmasına gerek kalmaz. Bu, yukarıdaki sıra diyagramındaki 2. adımdır.

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

loginRequest.entityEndpoint'e (kimlik sağlayıcı uç noktasının URL'si) gönderin.

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

Girdi adı loginRequest.type'dır (SAMLRequest). Bu alanın içeriği loginRequest.context'tir, bu da yine base64 kodlu XML'dir.

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

Bu ara katman yazılımı (opens in a new tab), HTTP isteğinin (opens in a new tab) gövdesini okur. Varsayılan olarak express bunu yok sayar, çünkü çoğu istek bunu gerektirmez. Buna ihtiyacımız var çünkü POST gövdeyi kullanıyor.

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

Yönlendiriciyi hizmet sağlayıcı dizinine (/sp) bağlayın.

1app.get("/", (req, res) => {
2 res.send(`
3 <html>
4 <body>
5 <button onClick=\"document.location.href='${config.spUrl}/login'\">
6 Oturum açmak için buraya tıklayın
7 </button>
8 </body>
9 </html>
10 `)
11})
Tümünü göster

Bir tarayıcı kök dizini almaya çalışırsa, ona oturum açma sayfasına bir bağlantı sağlayın.

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

Bu express uygulamasıyla spPort'u dinleyin.

src/idp.mts

Bu kimlik sağlayıcıdır. Hizmet sağlayıcıya çok benzer, aşağıdaki açıklamalar farklı olan kısımlar içindir.

1const xmlParser = new (await import("fast-xml-parser")).XMLParser(
2 {
3 ignoreAttributes: false, // Nitelikleri Koru
4 attributeNamePrefix: "@_", // Nitelikler için ön ek
5 }
6)

Hizmet sağlayıcıdan aldığımız XML isteğini okumamız ve anlamamız gerekiyor.

1const getLoginPage = requestId => `

Bu işlev, yukarıdaki sıra diyagramının 4. adımında döndürülen, otomatik gönderilen formu içeren sayfayı oluşturur.

1<html>
2 <head>
3 <title>Oturum açma sayfası</title>
4 </head>
5 <body>
6 <h2>Oturum açma sayfası</h2>
7 <form method="post" action="./loginSubmitted">
8 <input type="hidden" name="requestId" value="${requestId}" />
9 E-posta adresi: <input name="email" />
10 <br />
11 <button type="Submit">
12 Hizmet sağlayıcıda oturum aç
13 </button>
Tümünü göster

Hizmet sağlayıcıya gönderdiğimiz iki alan var:

  1. Yanıtladığımız requestId.
  2. Kullanıcı tanımlayıcısı (şimdilik kullanıcının verdiği e-posta adresini kullanıyoruz).
1 </form>
2 </body>
3</html>
4
5const idpRouter = express.Router()
6
7idpRouter.post("/loginSubmitted", async (req, res) => {
8 const loginResponse = await idp.createLoginResponse(

Bu, yukarıdaki sıra diyagramındaki 5. adımın işleyicisidir. idp.createLoginResponse (opens in a new tab) oturum açma yanıtını oluşturur.

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

Hedef kitle hizmet sağlayıcıdır.

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

İstekten çıkarılan bilgiler. İstekte önem verdiğimiz tek parametre, hizmet sağlayıcının istekleri ve yanıtlarını eşleştirmesine olanak tanıyan requestId'dir.

1 signingKey: { privateKey: idpPrivateKey, publicKey: config.idpCert } // İmzalamayı sağla

Yanıtı imzalamak için veriye sahip olmak için signingKey'e ihtiyacımız var. Hizmet sağlayıcı imzasız isteklere güvenmez.

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

Bu, hizmet sağlayıcıya geri gönderdiğimiz kullanıcı bilgilerini içeren alandır.

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})
Tümünü göster

Yine, otomatik gönderilen bir form kullanın. Bu, yukarıdaki sıra diyagramındaki 6. adımdır.

1
2// Oturum açma istekleri için IdP uç noktası
3idpRouter.post(`/login`,

Bu, hizmet sağlayıcıdan bir oturum açma isteği alan uç noktadır. Bu, yukarıdaki sıra diyagramının 3. adımının işleyicisidir.

1 async (req, res) => {
2 try {
3 // parseLoginRequest'i çalıştıramadığım için geçici çözüm.
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"]))

Kimlik doğrulama isteğinin kimliğini okumak için idp.parseLoginRequest (opens in a new tab) kullanabilmeliyiz. Ancak, çalıştıramadım ve üzerinde çok fazla zaman harcamaya değmedi, bu yüzden sadece genel amaçlı bir XML ayrıştırıcısı (opens in a new tab) kullanıyorum. İhtiyacımız olan bilgi, XML'in en üst düzeyinde bulunan <samlp:AuthnRequest> etiketinin içindeki ID özniteliğidir.

Ethereum imzalarını kullanma

Artık hizmet sağlayıcıya bir kullanıcı kimliği gönderebildiğimize göre, bir sonraki adım kullanıcı kimliğini güvenilir bir şekilde elde etmektir. Viem, cüzdandan kullanıcı adresini istememize izin verir, ancak bu, bilgiyi tarayıcıdan istemek anlamına gelir. Tarayıcıyı kontrol etmiyoruz, bu yüzden ondan aldığımız yanıta otomatik olarak güvenemeyiz.

Bunun yerine, IdP tarayıcıya imzalaması için bir dize gönderecek. Tarayıcıdaki cüzdan bu dizeyi imzalarsa, bu gerçekten o adres olduğu anlamına gelir (yani, adrese karşılık gelen özel anahtarı bilir).

Bunu çalışırken görmek için mevcut IdP ve SP'yi durdurun ve şu komutları çalıştırın:

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

Ardından SP'ye (opens in a new tab) gidin ve yönergeleri izleyin.

Bu noktada Ethereum adresinden e-posta adresini nasıl alacağımızı bilmediğimizi, bu yüzden SP'ye <ethereum adresi>@bad.email.address olarak rapor ettiğimizi unutmayın.

Ayrıntılı açıklama

Değişiklikler önceki diyagramdaki 4-5. adımlardadır.

Ethereum imzalı SAML

Değiştirdiğimiz tek dosya idp.mts. İşte değiştirilen kısımlar.

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

Bu iki ek kütüphaneye ihtiyacımız var. nonce (opens in a new tab) değerini oluşturmak için uuid (opens in a new tab) kullanıyoruz. Değerin kendisi önemli değil, sadece bir kez kullanıldığı gerçeği önemli.

viem (opens in a new tab) kütüphanesi Ethereum tanımlarını kullanmamızı sağlar. Burada imzanın gerçekten geçerli olduğunu doğrulamak için buna ihtiyacımız var.

1const loginPrompt = \

Cüzdan, mesajı imzalamak için kullanıcıdan izin ister. Sadece bir nonce olan bir mesaj kullanıcıların kafasını karıştırabilir, bu yüzden bu istemi ekliyoruz.

1// requestID'leri burada tutun
2let nonces = {}

Ona yanıt verebilmek için istek bilgisine ihtiyacımız var. İstekle birlikte gönderebilir (adım 4) ve geri alabiliriz (adım 5). Ancak, potansiyel olarak düşman bir kullanıcının kontrolü altında olan tarayıcıdan aldığımız bilgilere güvenemeyiz. Bu yüzden anahtar olarak nonce ile burada saklamak daha iyidir.

Bunu burada basitlik adına bir değişken olarak yaptığımızı unutmayın. Ancak, bunun birkaç dezavantajı vardır:

  • Hizmet reddi saldırısına karşı savunmasızız. Kötü niyetli bir kullanıcı birden çok kez oturum açmaya çalışarak belleğimizi doldurabilir.
  • IdP işlemi yeniden başlatılması gerekirse, mevcut değerleri kaybederiz.
  • Her birinin kendi değişkeni olacağı için birden çok işlem arasında yük dengeleyemeyiz.

Bir üretim sisteminde bir veritabanı kullanır ve bir tür sona erme mekanizması uygularız.

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

Bir nonce oluşturun ve requestId'yi ileride kullanmak üzere saklayın.

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

Bu JavaScript, sayfa yüklendiğinde otomatik olarak yürütülür.

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

viem'den birkaç fonksiyona ihtiyacımız var.

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

Sadece tarayıcıda bir cüzdan varsa çalışabiliriz.

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

Cüzdandan (window.ethereum) hesap listesini isteyin. En az bir tane olduğunu varsayın ve sadece ilkini saklayın.

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

Tarayıcı cüzdanıyla etkileşim kurmak için bir cüzdan istemcisi (opens in a new tab) oluşturun.

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

Kullanıcıdan bir mesaj imzalamasını isteyin. Tüm bu HTML bir şablon dizesi (opens in a new tab) içinde olduğundan, idp işleminde tanımlanan değişkenleri kullanabiliriz. Bu, sıra diyagramındaki 4.5. adımdır.

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

/idp/signature/<nonce>/<address>/<signature> adresine yönlendir. Bu, sıra diyagramındaki 5. adımdır.

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 }

İmza, potansiyel olarak kötü niyetli olan tarayıcı tarafından geri gönderilir (sizi tarayıcıda sadece http://localhost:3001/idp/signature/bad-nonce/bad-address/bad-signature açmaktan alıkoyan hiçbir şey yoktur). Bu nedenle, IdP işleminin kötü imzaları doğru şekilde işlediğini doğrulamak önemlidir.

1 </script>
2 </head>
3 <body>
4 <h2>Lütfen imzalayın</h2>
5 <button onClick="window.goodSignature()">
6 İyi (geçerli) bir imza gönderin
7 </button>
8 <br/>
9 <button onClick="window.badSignature()">
10 Kötü (geçersiz) bir imza gönderin
11 </button>
12 </body>
13</html>
14`
15}
Tümünü göster

Gerisi sadece standart HTML.

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

Bu, sıra diyagramındaki 5. adımın işleyicisidir.

1 const requestId = nonces[req.params.nonce]
2 if (requestId === undefined) {
3 res.send("Kötü nonce")
4 return ;
5 }
6
7 nonces[req.params.nonce] = undefined

İstek kimliğini alın ve yeniden kullanılamadığından emin olmak için nonces'tan nonce'ı silin.

1 try {

İmzanın geçersiz olabileceği pek çok yol olduğundan, bunu bir try ... içine sararız. catch bloğu ile atılan hataları yakalarız.

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

Sıra diyagramında 5.5. adımı uygulamak için verifyMessage (opens in a new tab) kullanın.

1 if (!validSignature)
2 throw("Kötü imza")
3 } catch (err) {
4 res.send("Hata:" + err)
5 return ;
6 }

İşleyicinin geri kalanı, küçük bir değişiklik dışında, daha önce /loginSubmitted işleyicisinde yaptığımızla eşdeğerdir.

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

Gerçek e-posta adresimiz yok (bir sonraki bölümde alacağız), bu yüzden şimdilik Ethereum adresini döndürüyoruz ve bir e-posta adresi olmadığını açıkça belirtiyoruz.

1// Oturum açma istekleri için IdP uç noktası
2idpRouter.post(`/login`,
3 async (req, res) => {
4 try {
5 // parseLoginRequest'i çalıştıramadığım için geçici çözüm.
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 yanıtı işlenirken hata:', err);
11 res.status(400).send('SAML kimlik doğrulaması başarısız oldu');
12 }
13 }
14)
Tümünü göster
  1. adım işleyicisinde getLoginPage yerine şimdi getSignaturePage kullanın.

E-posta adresini alma

Bir sonraki adım, hizmet sağlayıcı tarafından istenen tanımlayıcı olan e-posta adresini elde etmektir. Bunu yapmak için Ethereum Tasdik Hizmetini (EAS) (opens in a new tab) kullanıyoruz.

Tasdik almanın en kolay yolu GraphQL API'sini (opens in a new tab) kullanmaktır. Bu sorguyu kullanıyoruz:

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}
Tümünü göster

Bu schemaId (opens in a new tab) yalnızca bir e-posta adresi içerir. Bu sorgu, bu şemanın tasdiklerini ister. Tasdikin konusu recipient (alıcı) olarak adlandırılır. Her zaman bir Ethereum adresidir.

Uyarı: Burada tasdikleri alma şeklimizin iki güvenlik sorunu var.

  • Merkezi bir bileşen olan https://optimism.easscan.org/graphql API uç noktasına gidiyoruz. id özniteliğini alabilir ve ardından bir tasdikin gerçek olduğunu doğrulamak için zincir üzerinde bir arama yapabiliriz, ancak API uç noktası yine de bize onlardan bahsetmeyerek tasdikleri sansürleyebilir.

    Bu sorunu çözmek imkansız değil, kendi GraphQL uç noktamızı çalıştırabilir ve tasdikleri zincir günlüklerinden alabiliriz, ancak bu amaçlarımız için aşırıdır.

  • Tasdik eden kimliğine bakmıyoruz. Herkes bize yanlış bilgi verebilir. Gerçek bir dünya uygulamasında, bir dizi güvenilir tasdik edicimiz olurdu ve yalnızca onların tasdiklerine bakardık.

Bunu çalışırken görmek için mevcut IdP ve SP'yi durdurun ve şu komutları çalıştırın:

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

Ardından e-posta adresinizi girin. Bunu yapmanın iki yolu var:

  • Özel bir anahtar kullanarak bir cüzdanı içe aktarın ve 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 test özel anahtarını kullanın.

  • Kendi e-posta adresiniz için bir tasdik ekleyin:

    1. Tasdik gezginindeki şemaya (opens in a new tab) gidin.

    2. Şema ile Tasdik Et'e tıklayın.

    3. Alıcı olarak Ethereum adresinizi, e-posta adresi olarak e-posta adresinizi girin ve Zincir Üstü'nü seçin. Ardından Tasdik Oluştur'a tıklayın.

    4. Cüzdanınızdaki işlemi onaylayın. Gaz ücretini ödemek için Optimism Blokzincirinde (opens in a new tab) bir miktar ETH'ye ihtiyacınız olacak.

Her iki durumda da, bunu yaptıktan sonra http://localhost:3000 (opens in a new tab) adresine gidin ve yönergeleri izleyin. Test özel anahtarını içe aktardıysanız, aldığınız e-posta test_addr_0@example.com'dur. Kendi adresinizi kullandıysanız, tasdik ettiğiniz ne ise o olmalıdır.

Ayrıntılı açıklama

Ethereum adresinden e-postaya geçiş

Yeni adımlar GraphQL iletişimi, 5.6 ve 5.7. adımlardır.

Yine, idp.mts'nin değiştirilmiş kısımları burada.

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

İhtiyacımız olan kütüphaneleri içe aktarın.

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

Her blokzincir için ayrı bir uç nokta (opens in a new tab) vardır.

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

Uç noktayı sorgulamak için kullanabileceğimiz yeni bir GraphQLClient istemcisi oluşturun.

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

GraphQL bize yalnızca bayt içeren opak bir veri nesnesi verir. Onu anlamak için şemaya ihtiyacımız var.

1const ethereumAddressToEmail = async ethAddr => {

Bir Ethereum adresinden bir e-posta adresine geçmek için bir işlev.

1 const query = `
2 query GetAttestationsByRecipient {`

Bu bir GraphQL sorgusudur.

1 tasdikler(

Tasdikler arıyoruz.

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

İstediğimiz tasdikler, şemamızdaki, alıcının getAddress(ethAddr) olduğu tasdiklerdir. getAddress (opens in a new tab) işlevi, adresimizin doğru sağlama toplamına (opens in a new tab) sahip olduğundan emin olur. Bu, GraphQL'in büyük/küçük harfe duyarlı olması nedeniyle gereklidir. `

1 take: 1

Kaç tane tasdik bulursak bulalım, sadece ilkini istiyoruz.

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

Almak istediğimiz alanlar.

  • attester: Tasdiki gönderen adres. Normalde bu, tasdike güvenip güvenmeyeceğine karar vermek için kullanılır.
  • id: Tasdik kimliği. GraphQL sorgusundan gelen bilgilerin doğru olduğunu doğrulamak için bu değeri tasdiki zincir üzerinde okumak (opens in a new tab) için kullanabilirsiniz.
  • data: Şema verileri (bu durumda, e-posta adresi).
1 const queryResult = await graphqlClient.request(query)
2
3 if (queryResult.attestations.length == 0)
4 return "no_address@available.is"

Tasdik yoksa, açıkça yanlış olan ancak hizmet sağlayıcıya geçerli görünecek bir değer döndürün.

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

Bir değer varsa, verileri çözmek için decodeData kullanın. Sağladığı meta verilere ihtiyacımız yok, sadece değerin kendisine ihtiyacımız var.

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 );
Tümünü göster

E-posta adresini almak için yeni işlevi kullanın.

Peki ya merkeziyetsizlik?

Bu yapılandırmada, Ethereum'dan e-posta adresine eşleme için güvenilir tasdik edicilere güvendiğimiz sürece, kullanıcılar olmadıkları biri gibi davranamazlar. Ancak, kimlik sağlayıcımız hala merkezi bir bileşendir. Kimlik sağlayıcısının özel anahtarına sahip olan herkes, hizmet sağlayıcıya yanlış bilgi gönderebilir.

Çok taraflı hesaplama (MPC) (opens in a new tab) kullanarak bir çözüm olabilir. Gelecekteki bir öğreticide bunun hakkında yazmayı umuyorum.

Sonuç

Ethereum imzaları gibi bir oturum açma standardının benimsenmesi, bir tavuk ve yumurta sorunuyla karşı karşıyadır. Hizmet sağlayıcılar mümkün olan en geniş pazara hitap etmek ister. Kullanıcılar, oturum açma standartlarını destekleme konusunda endişelenmeden hizmetlere erişebilmek ister. Bir Ethereum IdP gibi adaptörler oluşturmak, bu engeli aşmamıza yardımcı olabilir.

Çalışmalarımdan daha fazlası için buraya bakın (opens in a new tab).

Sayfanın son güncellenmesi: 23 Kasım 2025

Bu rehber yararlı oldu mu?