כדי להטמיע אימות של כתובות אימייל באתר, צריך לעדכן את תגי העיצוב של הטופס כדי לבקש את הטוקן ולהוסיף אימות בצד השרת לטוקנים נכנסים.
הרשמה לגרסת מקור לניסיון
כדי לאמת אתרים, צריך להגדיר באתר את גרסת המקור לניסיון.
החל מגרסה 154 של Chrome, יש תמיכה בתקופות ניסיון של מקורות צד שלישי, אבל יש אזהרה חשובה: המקור שרשום לתקופת הניסיון צריך להיות מאותו אתר כמו המנפיק. לדוגמה:
- דומיין המנפיק:
issuer.example - OT registrant:
https://issuer.example - מקור JavaScript:
https://issuer.example(אוhttps://app.issuer.exampleעם התאמה לתת-דומיין)
הגדרת השדות בטופס
מוסיפים שדה מוסתר של טוקן לטופס שליחת האימייל:
<input
type="email"
name="email-address"
autocomplete="email">
<input
type="hidden"
name="token"
autocomplete="email-verification-token"
nonce="rAnD0m-VaLuE">
דרישות לגבי השדות:
- שדה האימייל: מגדירים את
type="email"ואתautocomplete="email"כדי ש-Chrome יוכל למלא את הכתובת באופן אוטומטי ולזהות אותה. - מאפיינים של שדות טוקן:
- הגדרת
autocomplete="email-verification-token": Chrome מזהה את השדה הזה כדי לאכלס את הטוקן בשליחה. - הגדרה
nonce="<VALUE>": האתר צריך לספק ערך חד-פעמי ייחודי שקשור לסשן כדי לאמת את שליחת הטופס.
- הגדרת
אימות הטוקן לאימות כתובת האימייל (EVT)
כשהמשתמש שולח את הטופס, השרת שלכם מקבל את כתובת האימייל ואת הטוקן מהשדה המוסתר. שדה ריק של טוקן מציין שהדפדפן או הספק לא תומכים ב-EVP, או שהמשתמש דילג על האימות. אם זה קורה, צריך לחזור לתהליך האימות הקיים, כמו שליחת קוד אימות חד-פעמי או קישור קסם.
אם יש אסימון, צריך לאמת אותו באופן הבא:
- מנתחים את הטוקן באמצעות ספריית SD-JWT.
- אימות של ערכים צפויים וטענות לגבי סשנים.
- מאמתים את ההקצאה של DNS.
- גילוי מטא-נתונים של מנפיק ואחזור של JWKS.
- אימות חתימות קריפטוגרפיות וקישור מפתחות.
1. ניתוח הטוקן
האסימון הוא בפורמט RFC 9901: Selective Disclosure JWT
(SD-JWT+KB). משתמשים בספריות המתאימות לפלטפורמה כדי לנתח ולאמת את הטוקן.
לדוגמה, ב-Node אפשר להשתמש ב-@sd-jwt/core וב-jose. בפורמט הגולמי שלו, הוא נראה כך: אסימון JWT בחתימת מנפיק, שאחריו אפס או יותר גילויים, ומסתיים באסימון JWT של שיוך מפתח, כאשר כל רכיב מופרד באמצעות סימן הטילדה:
<Issuer-signed EVT>~<Disclosure 1>~...~<Disclosure N>~<Key Binding JWT>
ביישום הנוכחי, האסימון מכיל אפס גילוי נאות
(<Issuer-signed EVT>~<Key Binding JWT>). עם זאת, יכול להיות שהמצב הזה ישתנה בעתיד.
מפענחים את האסימון באמצעות הספרייה:
import { decodeSdJwtSync } from "@sd-jwt/core";
import { createHash } from "node:crypto";
const hasher = (data, alg) =>
createHash(alg === "sha-256" ? "sha256" : alg)
.update(data)
.digest();
const decoded = decodeSdJwtSync(rawToken, hasher);
const evtPayload = decoded.jwt.payload;
const kbPayload = decoded.kbJwt?.payload;
אם verifier.example מאמת את demo@provider.example, האסימון המפוענח נראה כך:
{
"evtJwtDecodedHeader": {
"typ": "evt+jwt",
"alg": "EdDSA",
"kid": "issuer-key-id"
},
"evtJwtDecodedPayload": {
"iss": "https://provider.example",
"iat": 12345678901,
"exp": 12345679901,
"cnf": {
"jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "pUbLiCkEy123pUbLiCkEy123pUbLiCkEy123"
}
},
"email": "demo@provider.example",
"email_verified": true
},
"kbJwtDecodedHeader": {
"alg": "EdDSA",
"typ": "kb+jwt"
},
"kbJwtDecodedPayload": {
"aud": "https://verifier.example",
"iat": 12345678901,
"nonce": "rAnDoM123rAnDoM123rAnDoM123rAnDoM123",
"sd_hash": "hAsH456hAsH456hAsH456hAsH456hAsH456"
},
"disclosures": []
}
2. אימות של ערכים צפויים וטענות לגבי סשנים
בודקים שהערכים הבסיסיים במטען הייעודי (payload) תואמים לערכים שסיפקתם ולערכים הצפויים:
-
email_verified: הערך צריך להיותtrue. -
email: כתובת האימייל צריכה להיות זהה לכתובת האימייל שצוינה בטופס. -
aud(קהל): חייב להיות זהה למקור של האתר. -
nonce: הערך צריך להיות זהה לערך ה-nonce שצוין בטופס. -
iat(הונפק בתאריך) ו-exp(תפוגה): מוודאים שהאסימון נמצא בחלון הזמן התקף שלו ושלא פג תוקפו.
3. אימות של העברת הרשאות ב-DNS
מאמתים את _email-verification רשומת ה-DNS של הדומיין של כתובת האימייל. לדוגמה, כדי לבדוק את demo@gmail.com, צריך לשלוח שאילתה לרשומת ה-TXT של _email-verification.gmail.com. עבור הספק הזה, השאילתה מחזירה את המיקום של ספק החשבון, כלומר accounts.google.com.
$ dig +short TXT _email-verification.gmail.com
"iss=accounts.google.com"
מוודאים שסכמת המנפיק היא https:// ושhttps://<domain> תואם להצהרת iss ב-EVT.
4. אימות החתימה של EVT
מאחזרים את מטא-נתוני הגילוי של המנפיק מכתובת
https://<issuer>/.well-known/email-verification:
{
"issuance_endpoint": "https://accounts.google.com/gsi/email-verification/issue",
"jwks_uri": "https://verifiablecredentials-pa.googleapis.com/.well-known/vc-public-jwks",
"signing_alg_values_supported": ["EdDSA"]
}
מאחזרים את קבוצת מפתחות האינטרנט של JSON מ-jwks_uri.
משתמשים בספריית SD-JWT כדי לאמת את חבילת הטוקן. הספרייה מתאמת את האימות:
- אימות חתימת המנפיק ב-EVT מול ה-JWKS שאוחזר.
- אימות החתימה של הדפדפן על ה-KB-JWT באמצעות המפתח הציבורי הזמני ב-
cnf.jwk. - אימות של הקישור למפתח (
aud,nonceוהגיבוב (hash) של התקצירsd_hash).
דוגמה ללוגיקת אימות ב-Node.js:
import { SDJwtInstance } from "@sd-jwt/core";
import { importJWK, compactVerify } from "jose";
import { createHash } from "node:crypto";
const hasher = (data, alg) =>
createHash(alg === "sha-256" ? "sha256" : alg)
.update(data)
.digest();
const sdJwt = new SDJwtInstance({ hasher });
sdJwt.config({
hasher,
// Verifier for the Issuer-signed EVT
verifier: async (data, sig) => {
const token = `${data}.${sig}`;
const header = decoded.jwt.header;
const headerAlg = header.alg || "ES256";
// Match by kid if present, or iterate across matching algorithm keys
const keysToTry = header.kid
? jwksData.keys.filter(k => k.kid === header.kid)
: jwksData.keys;
for (const jwk of keysToTry) {
try {
const pubKey = await importJWK(jwk, jwk.alg || headerAlg);
await compactVerify(token, pubKey);
return true;
} catch {
// Try next candidate key
}
}
return false;
},
// Verifier for the Key Binding JWT (KB-JWT)
kbVerifier: async (data, sig) => {
try {
const browserJwkKey = evtPayload.cnf?.jwk;
if (!browserJwkKey) return false;
const pubKey = await importJWK(browserJwkKey, decoded.kbJwt.header.alg || "ES256");
await compactVerify(`${data}.${sig}`, pubKey);
return true;
} catch {
return false;
}
},
});
// The library automatically verifies EVT signature, KB-JWT signature, audience, nonce, and sd_hash
const result = await sdJwt.verify(rawToken, {
kb: {
expectedNonce: sessionNonce,
expectedAudience: "https://example.com",
required: true,
},
});
const verifiedPayload = result.payload;
אם כל השלבים יסתיימו בהצלחה, כתובת האימייל תאומת מול הספק. אם האימות נכשל, צריך לחזור לשליחת אימייל אישור למשתמש באמצעות התהליך הרגיל.
פתרון בעיות
אם האימות נכשל או שהדפדפן לא מספק טוקן, כדאי לבדוק את הבעיות הנפוצות הבאות:
שדה הטוקן ריק כששולחים את הטופס
- הרשמה לגרסת מקור לניסיון: מוודאים שהכותרת
Origin-Trialאו התג<meta>מוצגים בדף. בגרסאות מקור לניסיון של צד שלישי (Chrome 154 ואילך), מקור הניסיון הרשום חייב להיות מאותו אתר כמו המנפיק (https://<issuer-domain>). אפשר לבדוק את ההגדרה של גרסת המקור לניסיון באתר בכלי הפיתוח בקטע Application (אפליקציה) > Frames (מסגרות) > (בוחרים את המסגרת הרלוונטית) > Origin trials (גרסאות מקור לניסיון). - תגי עיצוב של הטופס: התגים
<input type="email" autocomplete="email">ו-<input type="hidden" autocomplete="email-verification-token" nonce="...">צריכים להיות באותו רכיב<form>(לא מבודדים בגבולות של Shadow DOM), והתגnonceלא יכול להיות ריק. - שליחה מוקדמת או שימוש חוזר בדף: הדפדפן מאחזר את האסימון ברקע אחרי שהמשתמש מזין את כתובת האימייל או אחרי שהיא מוזנת אוטומטית. אם שולחים את הבקשה לפני שהיא מסתיימת, האסימון נשאר ריק. זה יכול לקרות אם המשתמש לוחץ על מקש Return כדי לשלוח את הטופס אחרי שהוא מזין את כתובת האימייל שלו.
- דרישות מוקדמות לגבי הדפדפן והספק: המשתמש צריך להיות מחובר לספק משתתף באותו פרופיל דפדפן, ולהפעיל את ההגדרה אימות כתובת אימייל בהגדרות Chrome (
chrome://settings/contactInfo).
האימות של חתימת המוסד המנפיק נכשל
- חסרה הכותרת
kid: הטענהkid(מזהה מפתח) בכותרת EVT וב-JWKS היא אופציונלית (לדוגמה, Gmail משמיט אתkid). אםkidלא מופיע, צריך לחזור על כל המפתחות האפשריים ב-jwks_uriשל המנפיק במקום להיכשל בחיפוש מזהה מפתח. - מזהי אלגוריתמים (EdDSA ו-Ed25519): יכול להיות שגורמים מנפיקים וספריות יציינו את
EdDSAאו אתEd25519(לצדES256). חשוב לוודא שהלוגיקה של ייבוא ואימות JWK מקבלת את שני המזהים. - פורמט המקור של המנפיק (
iss): רשומת ה-TXT של ה-DNS (_email-verification.<domain>) מכילה שם מארח ללא קידומת (iss=accounts.issuer.example), בעוד שהטענה של EVTissהיא מקור מלא של HTTPS (https://accounts.issuer.example, ללא לוכסן בסוף). הקידומתhttps://לערך רשומת ה-DNS לפני ההשוואה.
האימות של קישור המפתח (KB-JWT) נכשל
- ערך nonce לא תואם או שפג תוקפו: מוודאים שהערך
nonceשמוצג ב-<input>תואם לערך nonce של הסשן הפעיל בשרת, ושלא בוצעה עליו החלפה על ידי כרטיסייה אחרת או שהוא לא נוצל על ידי בקשה קודמת. - אי התאמה של הקהל (
aud): טענתaudהיא מקור ה-HTTPS של מאמת הזהות (https://verifier.example, ללא נתיב או קו נטוי בסוף).
השוואה של הצהרת אימייל (email) נכשלת
- שינוי אותיות רישיות וקנוניזציה: ב-Chrome מגרסה 156 ואילך, ההצהרה
emailמוחזרת בבייט אחר בייט כמו שהיא הוזנה בטופס, אבל בגרסאות קודמות של הדפדפן או אצל ספקים קודמים, יכול להיות שכתובת קנונית תוחזר (לדוגמה,First.Last@example.comבמקוםfirst.last@example.com). כשמשווים את ההצהרהemailבאסימון לערך בטופס שנשלח, צריך להשתמש בהשוואה לא תלוית-אותיות רישיות.