Чтобы узнать больше, ознакомьтесь с демонстрационным кодом фиктивного поставщика услуг электронной почты и инструкциями для эмитента в предложениях API для подтверждения адреса электронной почты и протокола для подтверждения адреса электронной почты.
Как эмитент, вам не нужно регистрироваться в эксперименте с источником или предоставлять токен, поскольку поведение браузера запускается на сайте проверяющей стороны. Убедитесь, что ваши конечные точки настроены на обработку таких запросов.
Как настроить обнаружение эмитента
Чтобы браузеры могли автоматически обнаруживать ваши конечные точки проверки при выборе адреса электронной почты, принадлежащего вашему домену, предоставьте доступ к конфигурации с помощью DNS и конечной точки HTTP .well-known.
Как настроить запись делегирования DNS
Настройте TXT-запись DNS в домене электронной почты, которая делегирует полномочия на проверку идентификатору эмитента. Эти идентификаторы могут использовать один и тот же домен, в зависимости от вашей инфраструктуры.
Формат записи: _email-verification.<email-domain>
Пример файла зоны:
_email-verification.example.com IN TXT "iss=accounts.issuer.example"
Как разместить конечную точку .well-known/email-verification
Разместите файл JSON с метаданными в домене эмитента по пути /.well-known/.
В этом файле указаны ваши возможности по выпуску сертификатов и криптографические алгоритмы подписи, которые поддерживает ваша инфраструктура.
Конечная точка: https://<issuer-domain>/.well-known/email-verification
Пример ответа:
{
"issuance_endpoint": "https://accounts.issuer.example/email-verification/issuance",
"jwks_uri": "https://accounts.issuer.example/.well-known/vc-public-jwks",
"signing_alg_values_supported": ["EdDSA", "ES256"]
}
Как разместить конечную точку .well-known/web-identity
Возможно, вы уже реализовали дополнительный ресурс JSON .well-known как часть Federated Credentials (FedCM) API. Этот ресурс содержит ссылки на конечную точку аккаунта и URL для входа.
Конечная точка: https://<domain>/.well-known/web-identity
Пример ответа:
{
"accounts_endpoint": "https://accounts.issuer.example/accounts",
"login_url": "https://accounts.issuer.example/login"
}
Используйте конечную точку аккаунтов
Конечная точка accounts из FedCM API предоставляет список аккаунтов, в которые выполнен вход в данный момент. Ниже приведен пример минимального ответа. Подробнее о реализации поставщика идентификационной информации…
Конечная точка: как указано в .well-known/web-identity.
Пример ответа:
{
"accounts": [
{
"id": "demo-example",
"name": "Demo User",
"email": "demo@example.com",
"given_name": "Demo"
}
]
}
Как интегрировать Login Status API
У пользователя должен быть активный сеанс с поставщиком, и вы должны сообщить об этом браузеру с помощью API статуса входа.
Когда пользователь успешно входит в аккаунт или выходит из него, отправьте соответствующий заголовок HTTP-ответа:
Set-Login: logged-in
Set-Login: logged-out
Вы также можете обновить статус с помощью JavaScript в контексте веб-приложения:
navigator.login.setStatus("logged-in");
navigator.login.setStatus("logged-out");
Как обрабатывать запросы на выпуск
Ваш issuance_endpoint получает запрос application/json POST, который содержит ключ email и заголовки HTTP Message Signatures для Signature, Signature-Input и Signature-Key.
Используйте библиотеку, которая поддерживает структурированные заголовки и подписи HTTP-сообщений для вашей среды. В Node.js можно использовать structured-headers и http-message-sig.
Полный формат запроса:
POST /email-verification/issuance HTTP/1.1
Host: provider.example
Accept: application/json
Content-Digest: sha-256=:aBc123aBc123aBc123aBc123aBc123=:
Content-Type: application/json
Sec-Fetch-Dest: email-verification
Signature: sig=:+dEf567dEf567/dEf567dEf567dEf567/dEf567==:
Signature-Input: sig=("@method" "@authority" "@path" "content-digest" "signature-key");created=1786455840
Signature-Key: sig=hwk;crv="Ed25519";kty="OKP";x="gHi890_gHi890_gHi890"
{email: "demo@example.com"}
Проанализируйте и проверьте запрос:
- Аутентификация сеанса. Проверьте собственные файлы cookie сеанса, отправленные вместе с запросом. Пользователь должен пройти аутентификацию.
- Заголовок
Sec-Fetch-Dest: задайте значениеemail-verification. - Подписи HTTP-сообщений. Проверьте подпись запроса с помощью временного открытого ключа в
Signature-Keyи подтвердите действительностьContent-Digest. - Полезная нагрузка. Тело JSON содержит строку
email, необходимую для проверки.
Ответ на запрос выпуска
После успешной проверки токена сеанса и запроса создайте подписанный SD-JWT, возвращаемый в формате JSON, с помощью подходящих библиотек для вашей платформы. Например, для Node можно использовать @sd-jwt/core и jose.
Формат необработанной полезной нагрузки должен выглядеть примерно так:
{
"iss": "https://accounts.issuer.example",
"iat": 1780272000,
"exp": 1780272300,
"cnf": {
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "pUbLiCKeY123pUbLiCKeY123pUbLiCKeY123",
"y": "pUbLiCKeY456pUbLiCKeY456pUbLiCKeY456"
}
},
"email": "demo@example.com",
"email_verified": true
}
Создайте, подпишите и верните токен:
import { importJWK, CompactSign } from "jose";
import { SDJwtInstance } from "@sd-jwt/core";
import crypto from "node:crypto";
// Issuer private key from secure storage
const privateKey = await importJWK(PRIVATE_KEY_JWK, "EdDSA");
const origin = url.origin;
const currentTime = Math.floor(Date.now() / 1000);
const evtPayload = {
iss: origin,
iat: currentTime,
exp: currentTime + 300, // 5 minutes
cnf: {
jwk: browserJwk,
},
email: payload.email, // exactly as received in payload
email_verified: true,
};
const sdJwt = new SDJwtInstance({
signer: async (data) => {
const [headerB64, payloadB64] = data.split(".");
const header = JSON.parse(Buffer.from(headerB64, "base64url").toString());
const payload = Buffer.from(payloadB64, "base64url");
const signed = await new CompactSign(payload).setProtectedHeader(header).sign(privateKey);
return signed.split(".").pop()!;
},
signAlg: "EdDSA",
hasher: async (data, alg) => {
const nodeAlg = alg.replace("-", "");
return new Uint8Array(crypto.createHash(nodeAlg).update(data).digest());
},
hashAlg: "sha-256",
saltGenerator: async () => crypto.randomBytes(16).toString("base64url"),
});
const issuanceToken = await sdJwt.issue(evtPayload, undefined, {
header: {
alg: "EdDSA",
kid: PRIVATE_KEY_JWK.kid,
typ: "evt+jwt",
},
});
return sendResponse({ issuance_token: issuanceToken, }, 200);
Текст ответа будет выглядеть примерно так:
{
"issuance_token": "tOkEn123tOkEn123tOkEn123...~"
}
После того как вы создадите и подпишете токен подтверждения адреса электронной почты, браузер передаст его на сайт верификатора для проверки.
Устранение неполадок
Если браузер не подключается к вашим конечным точкам или отклоняет выданные токены, проверьте, нет ли следующих распространенных проблем:
Браузер никогда не вызывает accounts_endpoint или issuance_endpoint
- Статус входа не задан. Chrome отправляет запросы к конечным точкам, только если знает, что пользователь вошел в аккаунт. Убедитесь, что в процессе входа задается HTTP-заголовок
Set-Login: logged-inили вызывается функцияnavigator.login.setStatus("logged-in"). - Сеансовые файлы cookie заблокированы (
SameSite=None). Браузер получает ваши файлыaccounts_endpointиissuance_endpointот проверяющей стороны на другом сайте. Поскольку это межсайтовые запросы, в файле cookie сеанса должен быть указан атрибутSameSite=None; Secure. Файл cookieSameSite=Laxработает при тестировании на верификаторе одного сайта, но не передается в межсайтовых запросах. - Несоответствие аккаунта или проблемы с обнаружением. Убедитесь, что команда
_email-verification.<email-domain>возвращает одну запись TXT (iss=<issuer-domain>безhttps://), оба конечных пункта.well-knownвозвращаютContent-Type: application/json, а ответaccounts_endpointсодержит аккаунт,emailкоторого соответствует введенному адресу.
Не удалось подтвердить запрос на выпуск
- Заголовок
Sec-Fetch-Dest. В Chrome 154 и более поздних версиях отправляется заголовокSec-Fetch-Dest: email-verification(с дефисом), а в Chrome 153 –emailverification(без дефиса). Во время развертывания принимайте оба значения. - Несоответствие подписи сообщения HTTP (
@authority) за прокси-сервером. При проверке подписи RFC 9421 компонент@authorityотражает общедоступный хост. Если ваш сервер находится за обратным прокси-сервером или балансировщиком нагрузки, восстановите URL подтверждения, используяX-Forwarded-Host(или ваш общедоступный источник), а не внутреннее имя хоста, и вычислитеContent-Digestдля необработанных байтов тела запроса до синтаксического анализа JSON.
Браузер отклоняет возвращенный файл issuance_token
- Измененное или канонизированное утверждение
email. В Chrome 156 и более поздних версиях браузер проверяет, чтобы утверждениеemailв EVT совпадало с запрошенным утверждениемemailпобайтово. Если сервер приводит адрес к каноническому формату (например, возвращаетFirst.Last@example.com, когда был запрошен адресfirst.LAST@example.com), Chrome удаляет токен. Сопоставьте запрос с аккаунтом пользователя, но верните строкуemail, полученную в теле запроса. - Отсутствует завершающая тильда (
~). Даже если раскрытие информации не требуется,issuance_tokenдолжен быть действительным SD-JWT, заканчивающимся тильдой (<Issuer-signed-JWT>~), чтобы браузер мог добавить<KB-JWT>. - Несоответствие
issилиcnf.jwk. Убедитесь, что утверждение EVTissявляется точным источником HTTPS (https://<issuer-domain>, без косой черты в конце), соответствующим метаданным.well-known/email-verification, аcnf.jwkсодержит временный открытый ключ браузера из заголовкаSignature-Key.