12k
All articles

Подтверждение адреса электронной почты без отправки письма

Узнайте, как проверка почты в браузере подтверждает доступ к ящику без отправки письма: настройка формы, проверка токена в Node.js за пять шагов и ограничения испытания.

OpenReplay Team
OpenReplay Team
Подтверждение адреса электронной почты без отправки письма

Протокол Email Verification Protocol позволяет сайту убедиться, что пользователь владеет адресом электронной почты, не отправляя ему письмо: браузер получает подписанный токен от почтового провайдера пользователя и передаёт его вместе с формой.

В большинстве сценариев регистрации пользователя по-прежнему просят «проверить почту», и часть тех, кто переключается на другую вкладку за кодом, так и не возвращается. Chrome тестирует альтернативу. Origin trial в Chrome, анонсированный 8 июля 2026 года, стартовал в Chrome 150, а позже Chrome опубликовал обновление за август 2026 года. Сам протокол описан в черновике IETF (Internet-Draft) draft-hardt-email-verification.

Это не проверка через SMTP и не очистка списков по MX-записям. Такие инструменты проверяют, существует ли почтовый ящик. Этот протокол доказывает, что человек, заполняющий форму, авторизован в этом почтовом ящике. Ниже рассматриваются модель с тремя участниками, разметка формы, полноценный верификатор на Node.js, разбитый на пять шагов, а также то, насколько на него можно полагаться в период тестирования.

Ключевые выводы

  • Email Verification Protocol возвращает SD-JWT с привязкой к ключу (key binding): токен Email Verification Token, подписанный издателем, затем тильда, затем JWT привязки к ключу (KB-JWT), подписанный браузером.
  • Браузер выступает посредником между вашим сайтом и почтовым провайдером, поэтому провайдер выдаёт токен, не узнавая, какой сайт его запросил.
  • Валидный токен доказывает наличие активной сессии у почтового провайдера для этого адреса. Он не гарантирует, что письма, отправленные на этот адрес, будут доставлены.
  • Функция находится на стадии предложения в рамках origin trial в Chrome с ограничениями по трафику; в публикациях Chrome дата окончания не указана. В слайдах к IETF 126 авторы черновика (а не Chrome) оценили срок в три–шесть месяцев. В документации Chrome единственным участвующим провайдером назван Gmail.
  • Если скрытое поле с токеном приходит пустым или любой из шагов проверки завершается неудачей, отправляйте обычное письмо с подтверждением.

Что заменяет подтверждение адреса через браузер?

Подтверждение адреса через браузер заменяет письмо с одноразовым кодом или magic link, которое уводит пользователя с вашего сайта. В черновике IETF подробно описано, чего сценарий с кодом требует от пользователя: перейти в почтовое приложение, дождаться доставки, возможно, поискать письмо в спаме, скопировать код, а затем вернуться и вставить его. В черновике также отмечается, что многие бросают процесс на полпути. Magic link избавляет от копирования и вставки, но пользователю всё равно приходится идти в почту и ждать письма.

Вход через соцсети вообще обходится без писем. В черновике указано, чем за это приходится платить: у пользователя должен быть аккаунт у соответствующего провайдера, а вам нужно делать отдельную интеграцию для каждого провайдера. Email Verification Protocol использует тот же принцип (провайдер ручается за адрес), но работает через браузер, без SDK конкретного провайдера на вашем сайте.

Как работает Email Verification Protocol: три участника

В Email Verification Protocol участвуют три стороны. Ваш сайт — верификатор (проверяющая сторона, relying party). Браузер находится между вами и издателем (issuer) — сервисом, который обслуживает почтовый аккаунт пользователя. Поскольку весь обмен идёт через браузер, издатель так и не узнаёт, какой сайт запросил токен.

Verifier (your site) <--> Browser <--> Issuer (e.g. accounts.google.com)
   nonce, validation      binds token      checks session,
                          to your origin   signs EVT

Почтовый провайдер и издатель могут располагаться на разных хостах. В примере Chrome провайдер — gmail.com, а издатель — accounts.google.com.

Со стороны издателя почтовый домен публикует DNS-запись TXT, делегирующую полномочия издателю. Издатель отдаёт метаданные по адресу /.well-known/email-verification, сообщает браузеру о состоянии входа через Login Status API и, прежде чем что-либо подписывать, проверяет существующие first-party cookie сессии пользователя (о том, как настраивать такие cookie, см. Secure Cookies and HttpOnly). Черновик также позволяет браузеру запрашивать приватные адреса для отдельных сайтов — такие адреса помечаются в EVT как is_private_email: true. В документации Chrome к origin trial приватные адреса не описаны. Ранние редакции черновика позволяли издателю при отсутствии валидной cookie переходить к проверке через WebAuthn, но в редакции -02 эта возможность убрана, так что запрос без сессии теперь просто завершается ошибкой.

Сторона сайта: разметка формы и nonce

С вашей стороны в форме нужны два поля ввода: обычное поле email с autocomplete="email" и скрытое поле с autocomplete="email-verification-token", содержащее nonce, привязанный к сессии пользователя. При отправке формы браузер заполняет скрытое поле токеном. Вот разметка из публикации Chrome, где nonce подставляется сервером:

<input
  name="email-address"
  type="email"
  autocomplete="email">
<input
  type="hidden"
  name="token"
  nonce="rAnD0m-VaLuE"
  autocomplete="email-verification-token">

Правила привязки к сессии из черновика требуют случайного nonce с энтропией не менее 128 бит. Генерируйте новый nonce для каждого запроса на проверку и задавайте ему короткий срок жизни. Сохраняйте рядом с ним время отрисовки формы — оно понадобится на шаге 2 верификатора:

import { randomBytes } from "node:crypto";

const nonce = randomBytes(32).toString("base64url"); // 256 bits
req.session.evp = { nonce, renderedAt: Date.now() };

На странице также должен присутствовать ваш токен origin trial. Как его указать, объясняется в документации Chrome по origin trials. Согласно августовскому обновлению, сторонние (third-party) origin trials для этой функции не поддерживаются.

Проверка токена на сервере

Серверная проверка токена Email Verification Protocol состоит из пяти шагов: разбор, проверка ожидаемых значений, проверка привязки к ключу, проверка DNS-делегирования и проверка подписи издателя. Значение из формы имеет вид <EVT>~<KB-JWT>. EVT — это токен, подписанный издателем; он содержит email, email_verified, iss, iat и ключ браузера cnf.jwk. KB-JWT подписан браузером и содержит aud, nonce, iat и sd_hash. Не доверяйте ни одному декодированному утверждению (claim), пока не пройдены все перечисленные ниже шаги. В коде используются jose и встроенные модули Node. Каждый блок выполняется внутри блока try в единой функции verifyEmailToken(), которая возвращает false, если выброшено какое-либо исключение.

Импорты, необходимые для всех пяти шагов:

import { createHash } from "node:crypto";
import { resolveTxt } from "node:dns/promises";
import {
  createRemoteJWKSet,
  decodeJwt,
  decodeProtectedHeader,
  importJWK,
  jwtVerify,
} from "jose";

1. Разбор. Разделите значение по тильде и проверьте заголовки typ.

const [evt, kb, ...rest] = token.split("~");
if (!evt || !kb || rest.length) return false;
const evtHeader = decodeProtectedHeader(evt);
const kbHeader = decodeProtectedHeader(kb);
if (evtHeader.typ !== "evt+jwt" || kbHeader.typ !== "kb+jwt") return false;
const evtClaims = decodeJwt(evt);
const kbClaims = decodeJwt(kb);

Нынешние токены не содержат выборочных раскрытий (selective disclosures), поэтому значение аккуратно делится на две части. В августовском обновлении такой упрощённый подход назван ненадёжным: он перестанет работать, если раскрытия когда-нибудь появятся. Chrome рекомендует разбирать токен с помощью библиотеки для SD-JWT, например @sd-jwt/core, которую теперь использует и его собственное демо.

2. Ожидаемые значения. В шагах валидации от Chrome проверяются email_verified, email, nonce, aud, а также то, что iat приходится на момент после отрисовки формы. Сравнивайте email без учёта регистра: в августовском обновлении предупреждается, что провайдеры могут вернуть адрес с иным регистром букв, чем ввёл пользователь.

const { nonce, renderedAt } = req.session.evp ?? {};
delete req.session.evp; // single use
if (evtClaims.email_verified !== true) return false;
if (String(evtClaims.email).toLowerCase() !== submittedEmail.toLowerCase()) return false;
if (!nonce || kbClaims.nonce !== nonce) return false;
if (kbClaims.aud !== "https://example.com") return false;
if (kbClaims.iat < Math.floor(renderedAt / 1000)) return false;
// Any maximum age is your own policy; the sources define none.

3. Привязка к ключу. sd_hash — это SHA-256 от EVT вместе с завершающей ~. Кроме того, подпись KB-JWT должна успешно проверяться ключом из cnf.jwk.

const hash = createHash("sha256").update(evt + "~").digest("base64url");
if (hash !== kbClaims.sd_hash) return false;
const holderKey = await importJWK(evtClaims.cnf.jwk, kbHeader.alg);
await jwtVerify(kb, holderKey, { typ: "kb+jwt" });

4. DNS-делегирование. Запросите _email-verification.<domain>. Черновик требует ровно одну TXT-запись, значение которой должно начинаться с iss=. Чтобы проверить домен вручную, используйте dig +short TXT _email-verification.gmail.com.

const domain = submittedEmail.split("@").pop().toLowerCase();
const records = (await resolveTxt(`_email-verification.${domain}`))
  .map((chunks) => chunks.join(""));
if (records.length !== 1 || !records[0].startsWith("iss=")) return false;
const issuer = records[0].slice(4);

5. Обнаружение (discovery) и подпись EVT. Получите метаданные издателя, затем проверьте EVT по его jwks_uri. Черновик определяет идентификатор издателя как https:// плюс хост из DNS-записи, и утверждение iss в EVT должно в точности ему соответствовать.

const meta = await (await fetch(`https://${issuer}/.well-known/email-verification`)).json();
const jwks = createRemoteJWKSet(new URL(meta.jwks_uri));
const { payload } = await jwtVerify(evt, jwks, {
  algorithms: meta.signing_alg_values_supported,
  typ: "evt+jwt",
}); // written so it does not depend on a kid header
// The draft's issuer identifier is "https://" plus the DNS host, compared byte for byte.
if (payload.iss !== `https://${issuer}`) return false;
return true;
ПроверкаЧто проверяетсяПри неудаче
Разборразделение по ~, заголовки typОтправить письмо с подтверждением
Ожидаемые значенияemail_verified, email, nonce, aud, iatОтправить письмо с подтверждением
Привязка к ключуsd_hash, подпись KB-JWT относительно cnf.jwkОтправить письмо с подтверждением
DNSединственная TXT-запись iss=Отправить письмо с подтверждением
Подпись издателяметаданные, jwks_uri, issОтправить письмо с подтверждением

Модуль DNS в Node возвращает каждую TXT-запись в виде массива фрагментов, поэтому на шаге 4 они склеиваются.

Каковы ограничения подтверждения адреса через браузер?

Успешная проверка по Email Verification Protocol показывает, что пользователь в данный момент авторизован у своего почтового провайдера с этим адресом. Она ничего не говорит о том, дойдут ли туда ваши письма. Об этом пишут и Chrome, и авторы черновика, так что продолжайте отправлять приветственные письма.

В профиле браузера, в котором пользователь заполняет форму, уже должна быть активная сессия у провайдера, а при первой проверке адреса пользователь видит запрос разрешения — один раз для каждого адреса. В июльском релизе адрес должен был подставляться через автозаполнение. Согласно августовскому обновлению, теперь проверка запускается и при ручном вводе или вставке адреса — когда поле теряет фокус. Черновик также указывает на небольшую утечку приватности: сам факт получения токена сообщает вашему сайту, авторизован ли пользователь у издателя.

Что делать сейчас

Email Verification Protocol пока не готов заменить письмо с подтверждением. Браузерный API находится на стадии инкубации в WICG, а протокол — индивидуальный Internet-Draft (редакция -02, август 2026 года). В рамках origin trial Chrome ограничивает трафик, чтобы сайты не начали полагаться на функцию до её официального запуска. В публикациях Chrome дата окончания не указана. Оценка в три–шесть месяцев принадлежит авторам черновика и приведена в их слайдах IETF 126 DISPATCH, а не Chrome. Chrome также предупреждает о несовместимых изменениях, а в августовском обновлении сказано, что до Chrome 152 включительно функция доступна только на десктопе, а поддержка Android пока изучается. В документации Chrome единственным участвующим провайдером назван Gmail. В черновике также указан Hellō как издатель, реализовавший протокол, и упоминается origin trial в Microsoft Edge.

Процесс выдачи токенов уже менялся. В июльской публикации описывался JWT request_token, передаваемый в виде данных формы. Черновик заменяет его на HTTP Message Signatures (RFC 9421) и помечает старый формат как устаревший. Начиная с Chrome 153 браузер отправляет application/json с заголовками Signature, Signature-Input и Signature-Key, а тело запроса содержит только адрес электронной почты. Это изменение на стороне издателя, и код вашего верификатора оно не затрагивает.

Оставьте сценарий с OTP или magic link основным. Если вы участвуете в origin trial, рассматривайте токен как прогрессивное улучшение: если проверка прошла успешно, письмо можно не отправлять. После проверки создавайте сессию так же, как обычно (см. Cookies vs localStorage for JWT Auth). EVT+KB — одноразовое доказательство, и использовать его в качестве токена сессии не следует.

Протокол заменяет «проверьте почту» проверкой подписи, которую вы выполняете на собственном сервере, а верификатору достаточно пяти шагов. Добавьте скрытое поле и nonce, разверните verifyEmailToken() перед существующим письмом с подтверждением и перед каждым релизом Chrome проверяйте публикацию с обновлениями на предмет изменений.

Часто задаваемые вопросы

Чем Email Verification Protocol отличается от Sign in with Google?

Sign in with Google — это интеграция входа для конкретного провайдера, которая устанавливает связь идентичности между вашим сайтом и Google. Email Verification Protocol лишь подтверждает, что пользователь владеет адресом электронной почты. Для него не нужен SDK провайдера на вашем сайте, а поскольку обмен идёт через браузер, издатель подписывает токен, не узнавая, какой сайт его запросил.

Можно ли использовать crypto.randomUUID() для генерации nonce?

Нет. UUID версии 4 содержит лишь 122 случайных бита, поскольку оставшиеся 6 бит занимают поля версии и варианта. Черновик IETF требует nonce с энтропией не менее 128 бит. Вместо этого используйте randomBytes(32).toString('base64url') из node:crypto — это даёт 256 бит. Храните nonce в серверной сессии и удаляйте его после однократного использования.

Работает ли Email Verification Protocol для адресов на собственных доменах или у провайдеров, отличных от Gmail?

Только если почтовый домен публикует ровно одну TXT-запись _email-verification, начинающуюся с iss= и делегирующую полномочия издателю, который поддерживает протокол. В документации Chrome участвующим почтовым провайдером назван Gmail. Для любого другого домена без такой записи браузер не сможет получить токен, поэтому верификатор должен считать проверку неудачной и отправлять обычное письмо с подтверждением.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.