Kayıt, oturum açma, ödeme veya hesap kurtarma sırasında e-posta adresi toplarken sahipliği doğrulamak için genellikle bant dışı tek kullanımlık şifre (OTP) veya sihirli bağlantı gönderilmesi gerekir. Bu durum, kullanıcıları sayfanızdan ayrılmaya, e-postanın teslim edilmesini beklemeye ve kodları kopyalamaya zorlar. Bu da sürtüşmeye, ayrılmaya ve kimlik avı riskine yol açarken kullanıcının hangi bağlı tarafı (RP) ziyaret ettiğini e-posta sağlayıcıya gösterir.
E-posta Doğrulama API'si (istemci taraflı HTML uzantısı) ve E-posta Doğrulama Protokolü (EVP) (arka uç kriptografik protokolü), bağlı tarafın doğrulama e-postası göndermeden bir kullanıcının e-posta adresini kontrol ettiğini doğrulamasını sağlar. Tarayıcı, güvenen taraf ile kullanıcının e-posta sağlayıcısı (veren) arasında aracılık yapar:
- Bağlı taraf, e-posta girişiyle aynı
<form>içindeautocomplete="email-verification-token"içeren gizli bir<input>ve tek kullanımlık kriptografiknonceiçerir. - Kullanıcı bir e-posta adresi seçtiğinde veya girdiğinde tarayıcı, alanın
_email-verification.<domain>DNSTXTkaydını arar, kullanıcının yetkili verenle etkin bir oturumu olduğunu doğrular ve güvenen tarafın kimliğini verene ifşa etmeden bir E-posta Doğrulama Jetonu (EVT) ister. - Tarayıcı, EVT'yi geçici bir tarayıcı anahtar çifti (
EVT + KB-JWT) ile imzalanmış bir Anahtar Bağlama JWT'sinde (KB-JWT), güvenen tarafın kaynağına ve oturumunanoncebağlar, gizli girişi doldurur ve formu gönderir. - Güvenen tarafın arka ucu, şifreleme zincirini doğrular ve geçerliyse e-posta OTP'sini veya sihirli bağlantı adımını atlar.
1. Kaynak Denemesini Etkinleştirme ve Doğrulama Formunu Oluşturma
Doğrulama sitesi (güvenen taraf) olarak katılmak için:
- Formu barındıran sayfada
Origin-TrialHTTP yanıt başlığını veya<meta http-equiv="origin-trial">öğesini kullanarak kaynak denemesi jetonunu sağlayın.- Üçüncü taraf kaynak denemeleri: Yerleşik kimlik SDK'ları, kaynak denemesi tescil ettiren kaynağın, veren alan adıyla eşleşmesi (verenle aynı site, örneğin
https://issuer.example) koşuluyla üçüncü taraf deneme jetonu ekleyebilir.
- Üçüncü taraf kaynak denemeleri: Yerleşik kimlik SDK'ları, kaynak denemesi tescil ettiren kaynağın, veren alan adıyla eşleşmesi (verenle aynı site, örneğin
- En az 128 bit entropi içeren (ör.
node:crypto'tenrandomBytes(32).toString('base64url')) form oluşturma başına kriptografik olarak rastgele bir tek seferlik rastgele sayı oluşturun, bunuHttpOnly,Secure,SameSite=Laxoturum kimliği çereziyle anahtarlanmış sunucu tarafı oturum durumunda depolayın (beklenennoncedeğerini hiçbir zaman doğrudan bir istemci çerezinde depolamayın. Saldırgan hem çerezi hem de jetonu tekrar oynatabilir) ve gizli jeton girişininnonceözelliğinde oluşturun. Geri/ileri gezinmenin daha önce kullanılmış bir tek seferlik rastgele sayıyı yeniden kullanmaması için sayfayıCache-Control: no-storeile sunun. - Gizli jeton girişini (
autocomplete="email-verification-token"), e-posta girişiyle (autocomplete="email") aynı<form>içine ekleyin.
<!-- Serve with:
Origin-Trial: <YOUR_ORIGIN_TRIAL_TOKEN>
Cache-Control: no-store -->
<form method="POST" action="/api/verify-email">
<label for="email">Email address:</label>
<input
type="email"
id="email"
name="email"
autocomplete="email"
required
/>
<!-- MANDATORY: Must be in the same <form> as the email input.
The nonce value must be freshly generated on the server per form render. -->
<input
type="hidden"
name="token"
nonce="SERVER_GENERATED_PER_FORM_NONCE"
autocomplete="email-verification-token"
/>
<button type="submit">Continue</button>
</form>
2. Gönderilen jetonu sunucuda doğrulama
Form gönderildiğinde token alanını güvenilmeyen giriş olarak değerlendirin. Beklenen nonce değerini, alındığında sunucu tarafı oturum deponuzdan atomik olarak okuyup silin. token alanı boşsa (ör. EVP'yi desteklemeyen tarayıcılarda veya kullanıcı e-posta sağlayıcısında oturumunu kapattığında) ya da doğrulama başarısız olursa hemen standart e-posta OTP'nize veya sihirli bağlantı akışınıza geri dönün:
// Replace with your public relying-party origin
const RP_ORIGIN = 'https://rp.example.com';
export async function handleSignupSubmission(request) {
const formData = await request.formData();
const submittedEmail = String(formData.get('email') || '').trim();
const rawToken = String(formData.get('token') || '').trim();
// Atomically read and delete the single-use nonce from server-side session state
const expectedNonce = await consumeSessionNonce(request);
if (rawToken && expectedNonce) {
try {
await verifyEmailVerificationToken({
rawToken,
submittedEmail,
expectedNonce,
expectedAudience: RP_ORIGIN,
});
// Fast path: email ownership is cryptographically verified; skip OTP email.
return completeAccountCreation(submittedEmail);
} catch (err) {
// Log token verification failures before falling back to standard email verification.
console.error('Email verification token check failed:', err);
}
}
// Fallback path: send standard OTP code or magic link email to submittedEmail.
return sendTraditionalVerificationEmail(submittedEmail);
}
verifyEmailVerificationToken uygulamasını kullanmak için özel JWT veya SD-JWT ayrıştırma kodunu manuel olarak oluşturmak yerine her zaman arka uç platformunuz için oluşturulmuş SD-JWT ve JOSE/JWT kitaplıklarını (ör. Node.js'de @sd-jwt/core ve jose, Python'da sd-jwt-python/jwcrypto veya JVM'de sd-jwt-java/Nimbus JOSE+JWT) kullanın.
Step 1: Parse the SD-JWT claims and validate the email address
Sunucu tarafı oturum depolama alanından tek kullanımlık expectedNonce kullanıldıktan sonra SD-JWT sunumunun (EVT + KB-JWT) kodunu çözün ve şunları doğrulayın:
- Hem veren JWT (
jwt) hem de anahtar bağlama JWT (kbJwt) ve veren hak talebi (iss) mevcut. evtPayload.email_verified === true.evtPayload.email, büyük/küçük harfe duyarlı olmayan bir karşılaştırma (toLowerCase()) kullanarak gönderilen form e-postasıyla eşleşir.
import { createHash } from 'node:crypto';
import { decodeSdJwtSync } from '@sd-jwt/core';
const hasher = (data, alg) =>
createHash(alg === 'sha-256' ? 'sha256' : alg).update(data).digest();
const decoded = decodeSdJwtSync(rawToken, hasher);
const evtPayload = decoded.jwt.payload;
const iss = evtPayload.iss;
if (!expectedNonce || !decoded.kbJwt || !iss || evtPayload.email_verified !== true) {
throw new Error('Missing nonce, KB-JWT, issuer, or email_verified !== true.');
}
// MANDATORY: Compare email addresses case-insensitively
if (evtPayload.email?.toLowerCase() !== submittedEmail.toLowerCase()) {
throw new Error('Submitted email does not match the EVT email claim.');
}
2. adım: DNS TXT yetkilendirmesini doğrulayın ve veren kuruluşun JWKS'sini yükleyin
iss adresine herhangi bir ağ isteği göndermeden önce, gönderilen e-postanın alan adının, bir _email-verification.<domain> DNS TXT kaydı (bağlantı noktası, yol veya sondaki eğik çizgi olmadan iss=<issuer-host> -> https://<issuer-host>) aracılığıyla tam olarak bu veren kuruluşa yetki verdiğini doğrulayın. DNS yetkilendirmesi iss ile bayt bazında eşleştiğinde HTTPS üzerinden ${iss}/.well-known/email-verification değerini getirin, metadata.issuer === iss değerini onaylayın ve verenin JSON Web Anahtarı Kümesi'ni (JWKS) yükleyin.
kidişleme ile ilgili not: Bazı büyük e-posta sağlayıcıları (ör. Gmail), EVT üstbilgisindenkiddeğerini çıkarır.kidyoksa ve JWKS birden fazla anahtar içeriyorsajose,ERR_JWKS_MULTIPLE_MATCHING_KEYS(eşleşenCryptoKey'lerinAsyncIterable) hatası verir. 3. adımda gösterildiği gibi her anahtarı deneme amaçlı doğrulamak içinfor await (const publicKey of err)üzerinde iterasyon yapın.
import dns from 'node:dns/promises';
import { createRemoteJWKSet } from 'jose';
const domain = submittedEmail.split('@')[1];
const txtRecords = (await dns.resolveTxt(`_email-verification.${domain}`)).map((r) => r.join(''));
if (!txtRecords.some((txt) => txt.startsWith('iss=') && `https://${txt.slice(4).trim()}` === iss)) {
throw new Error(`Issuer "${iss}" is not delegated via DNS for "${domain}".`);
}
const discoveryUrl = new URL('/.well-known/email-verification', iss);
if (discoveryUrl.protocol !== 'https:') throw new Error('Issuer must use HTTPS.');
const metadata = await (await fetch(discoveryUrl)).json();
if (metadata.issuer !== iss || !metadata.jwks_uri?.startsWith('https://')) {
throw new Error('Invalid issuer metadata or non-HTTPS jwks_uri.');
}
const issuerJwks = createRemoteJWKSet(new URL(metadata.jwks_uri));
3. adım: EVT imzasını ve anahtar bağlama JWT'sini doğrulayın (KB-JWT)
Son olarak, açık asimetrik algoritma izin verilenler listesini (['Ed25519', 'EdDSA', 'ES256']) kullanarak her iki imzayı ve iki jeton arasındaki kriptografik bağlamayı doğrulayın:
- EVT imzası (
verifier):issuerJwksile doğrulayın (kidatlandığında aday anahtarları yinelemek içinERR_JWKS_MULTIPLE_MATCHING_KEYSyakalanır),issve sıkı bir yenilik aralığı (maxTokenAge: '5m',clockTolerance: '1m') zorunlu kılınır. - Kart sahibi bağlama (
kbVerifiervesdJwt.verifyseçenekleri): Tarayıcının geçici ortak anahtarınıevtPayload.cnf.jwköğesinden çıkarın, hemcnf.jwk.alghem dedecoded.kbJwt.header.algöğelerininALLOWED_ALGSiçinde olduğunu ve eşleştiğini doğrulayın (Kaynak Denemesi uygulamaları RFC 9864'Ed25519''e geçerken'Ed25519've'EdDSA'öğelerini eşdeğer olarak kabul edin) veKB-JWTimzasınıtyp: 'kb+jwt'ile doğrulayın.keyBindingNonce: expectedNonce,expectedKeyBindingAudience: expectedAudiencevekeyBindingMaxAgeSeconds: 300değerlerinisdJwt.verify()öğesine iletin.@sd-jwt/core, yalnızcakeyBindingNoncesağlandığındakbVerifieröğesini çağırır venonce,aud,iatvesd_hashdeğerlerini doğrular.
import { SDJwtInstance } from '@sd-jwt/core';
import { importJWK, jwtVerify } from 'jose';
// IETF draft-hardt-email-verification-02 (§3.3, §5.1.1) specifies RFC 9864 'Ed25519' and 'ES256'.
// TODO: Remove 'EdDSA' once live issuers (e.g. Gmail) and Chrome Origin Trial builds finish
// transitioning from 'EdDSA' (with crv: 'Ed25519') to 'Ed25519'.
const ALLOWED_ALGS = ['Ed25519', 'EdDSA', 'ES256'];
const normalizeAlg = (alg) => (alg === 'Ed25519' ? 'EdDSA' : alg);
const evtOptions = {
typ: 'ev+sd-jwt',
issuer: iss,
algorithms: ALLOWED_ALGS,
maxTokenAge: '5m',
clockTolerance: '1m',
};
const sdJwt = new SDJwtInstance({
hasher,
verifier: async (data, sig) => {
const compactEvt = `${data}.${sig}`;
try {
await jwtVerify(compactEvt, issuerJwks, evtOptions);
} catch (err) {
// When `kid` is omitted (e.g. Gmail) and multiple keys match in the JWKS,
// jose throws JWKSMultipleMatchingKeys, which yields each matching CryptoKey.
if (err?.code !== 'ERR_JWKS_MULTIPLE_MATCHING_KEYS') throw err;
let matched = false;
for await (const publicKey of err) {
try {
await jwtVerify(compactEvt, publicKey, evtOptions);
matched = true;
break;
} catch {}
}
if (!matched) throw err;
}
return true;
},
kbVerifier: async (data, sig) => {
const holderJwk = evtPayload.cnf?.jwk;
const holderAlg = holderJwk?.alg;
const kbAlg = decoded.kbJwt.header.alg;
if (
!holderAlg ||
!kbAlg ||
!ALLOWED_ALGS.includes(holderAlg) ||
!ALLOWED_ALGS.includes(kbAlg) ||
normalizeAlg(kbAlg) !== normalizeAlg(holderAlg)
) {
throw new Error('Missing, disallowed, or mismatched KB-JWT algorithm.');
}
const holderKey = await importJWK(holderJwk, kbAlg);
await jwtVerify(`${data}.${sig}`, holderKey, {
typ: 'kb+jwt',
audience: expectedAudience,
algorithms: [kbAlg],
maxTokenAge: '5m',
clockTolerance: '1m',
});
return true;
},
});
const verified = await sdJwt.verify(rawToken, {
requiredClaimKeys: ['email', 'email_verified'],
keyBindingNonce: expectedNonce,
expectedKeyBindingAudience: expectedAudience,
keyBindingMaxAgeSeconds: 300,
skewSeconds: 60,
});
En iyi uygulamalar
- ÖNERİLİR: Tüm jeton doğrulama işlemlerini kesinlikle sunucuda gerçekleştirin. İstemci taraflı JavaScript'te onay anahtarlarını DOĞRULAMAYIN veya istemci taraflı doğrulama durumlarına güvenmeyin. İstemci taraflı kontroller kolayca atlanabilir veya sahtesi yapılabilir ve güvenen tarafa sıfır güven garantisi sağlar.
- MUTLAKA
EVT~KB-JWTsunumunu ayrıştırmak ve doğrulamak için arka uç platformunuzda yerleşik SD-JWT ve JOSE/JWT kitaplıklarını (ör. Node.js'de@sd-jwt/corevejose) kullanın vekeyBindingNoncedeğerinisdJwt.verify()değerine iletin. Böylece anahtar bağlama doğrulaması atlanamaz. Özel JWT, SD-JWT veyasd_hashayrıştırma ve imza doğrulama rutinlerini manuel olarak oluşturmayın. - Her form oluşturma işlemi için sunucuda yeni bir şifreleme
nonce(en az 128 bit, örneğinrandomBytes(32).toString('base64url')) OLUŞTURUN, bunu sunucu tarafı oturum durumunda (doğrudan istemci çerezinde değil) saklayın ve form sayfasınıCache-Control: no-storeile sunun. POSTisteği geldiğinde, beklenennoncedeğerini sunucu tarafı oturum depolama alanından atomik olarak okuyup hemen SİLİN. Böylece, ele geçirilen jetonlar yeniden kullanılamaz.evtPayload.emailile gönderilen form e-posta adresi (email.toLowerCase()) arasında büyük/küçük harfe duyarlı olmayan bir karşılaştırma YAPIN. Bu sırada gönderilen e-posta dizesini koruyun.kid, EVT üstbilgisinde yoksa (Gmail gibi sağlayıcılarkid'yi atlar) veren kuruluşun JWKS'sindeki tüm anahtarları deneme amaçlı olarak DO doğrulayın.- EVT'deki
isshak talebine güvenmeden önce DNS yetkilendirmesini (_email-verification.<domain>) DOĞRULAYIN ve.well-known/email-verificationyanıtındametadata.issuer === issdeğerini onaylayın. - E-posta sahipliği doğrulamasını, gelen kutusuna teslim edilebilirlik kanıtı olarak KULLANMAYIN. EVP, kullanıcının e-posta hesabını yetkili sağlayıcıyla kontrol ettiğini kanıtlar. İşlemle ilgili karşılama e-postaları göndermeye ve geri dönen e-postaları normal şekilde işlemeye devam edin.
tokenboş olduğunda veya doğrulama başarısız olduğunda hesap oluşturma ya da oturum açma işlemlerini ENGELLEMEYİN. Her zaman standart e-posta OTP'nize veya sihirli bağlantı akışınıza sorunsuz bir şekilde geçiş yapın.
Yedek strateji
E-posta Doğrulaması (autocomplete="email-verification-token"), geleneksel bant dışı e-posta doğrulama (OTP kodları veya sihirli bağlantılar) yöntemlerine kıyasla progresif geliştirme olarak tasarlanmıştır:
- Desteklenmeyen tarayıcılar:
autocomplete="email-verification-token"özelliğini desteklemeyen tarayıcılar, gizli girişi yoksayar ve formu boş birtokendeğeriyle gönderir. - Kimliği doğrulanmamış veya katılmayan e-posta alanları: Kullanıcı, alanı
_email-verificationDNSTXTkaydı yayınlamayan bir e-posta adresi girdiğinde veya kullanıcının tarayıcıda e-posta sağlayıcısıyla etkin bir oturumu olmadığında tarayıcı, formu boş birtokenile gönderir. - Süresi dolmuş veya geçersiz jetonlar:
noncesüresi dolmuşsa, daha önce kullanılmışsa veyahandleSignupSubmissioniçindekiverifyEmailVerificationTokenadımlarından herhangi birinde başarısız olursa sunucu hatayı yakalar ve kullanıcının kaydolma veya oturum açma akışını kesintiye uğratmadan geleneksel bir OTP ya da sihirli bağlantı e-postası gönderir.