12k
All articles

Verificar una dirección de correo electrónico sin enviar ningún correo

Vea cómo la verificación de correo en el navegador prueba el control del buzón sin enviar mensajes, con formulario, verificador de tokens Node.js en cinco pasos y límites de la prueba.

OpenReplay Team
OpenReplay Team
Verificar una dirección de correo electrónico sin enviar ningún correo

El Email Verification Protocol permite que un sitio confirme que un usuario controla una dirección de correo electrónico sin enviarle un correo: el navegador obtiene un token firmado del proveedor de correo del usuario y lo envía junto con el formulario.

La mayoría de los flujos de registro siguen diciendo “revisa tu bandeja de entrada”, y algunas de las personas que cambian de pestaña para buscar el código nunca regresan. Chrome está probando una alternativa. La prueba de origen (origin trial) de Chrome anunciada el 8 de julio de 2026 comenzó en Chrome 150, y posteriormente Chrome publicó una actualización en agosto de 2026. El protocolo en sí está definido en el Internet-Draft del IETF draft-hardt-email-verification.

No se trata de SMTP ni de limpieza de listas mediante registros MX. Esas herramientas comprueban si un buzón existe. Este protocolo demuestra que la persona que rellena el formulario tiene una sesión iniciada en ese buzón. Las secciones siguientes abordan el modelo de tres partes, el marcado del formulario, un verificador completo en Node.js dividido en cinco pasos y hasta qué punto se puede confiar en él durante la prueba.

Puntos clave

  • El Email Verification Protocol devuelve un SD-JWT con vinculación de clave (key binding): un Email Verification Token firmado por el emisor, seguido de una tilde y de un JWT de vinculación de clave firmado por el navegador.
  • El navegador se sitúa entre tu sitio y el proveedor de correo, de modo que el proveedor emite el token sin saber qué sitio lo solicitó.
  • Un token válido demuestra que existe una sesión activa con el proveedor de correo para esa dirección. No demuestra que el correo enviado a esa dirección vaya a entregarse.
  • La funcionalidad es una propuesta en una prueba de origen de Chrome con límites de tráfico y sin fecha de finalización en las publicaciones de Chrome. En sus diapositivas del IETF 126, los autores del borrador (no Chrome) estimaron entre tres y seis meses. La documentación de Chrome solo menciona a Gmail como proveedor participante.
  • Si el campo oculto del token llega vacío, o si falla cualquier paso de la verificación, envía tu correo de confirmación habitual.

¿Qué reemplaza la verificación de correo electrónico desde el navegador?

La verificación de correo electrónico desde el navegador reemplaza el correo con código de un solo uso o con enlace mágico que obliga a los usuarios a salir de tu sitio. El borrador del IETF describe lo que el flujo con código exige al usuario: ir a la aplicación de correo, esperar a que llegue el mensaje, quizá rebuscar en la carpeta de spam, copiar el código y, por último, volver y pegarlo. El borrador añade que muchas personas abandonan a mitad del proceso. Un enlace mágico ahorra el paso de copiar y pegar, pero el usuario todavía tiene que ir a la bandeja de entrada y esperar el mensaje.

El inicio de sesión social prescinde por completo del correo. El borrador señala lo que eso cuesta: el usuario necesita tener una cuenta con ese proveedor y tú tienes que desarrollar una integración por cada proveedor. El Email Verification Protocol aplica el mismo principio (el proveedor avala la dirección), pero funciona a través del navegador, sin ningún SDK específico de proveedor en tu sitio.

Cómo funciona el Email Verification Protocol: tres partes

En el Email Verification Protocol intervienen tres partes. Tu sitio es el verificador (la relying party). El navegador se sitúa entre tú y el emisor, que es el servicio que gestiona la cuenta de correo del usuario. Como todos los intercambios pasan por el navegador, el emisor nunca llega a saber qué sitio solicitó el token.

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

El proveedor de correo y el emisor pueden ser hosts distintos. En el ejemplo de Chrome, el proveedor es gmail.com y el emisor es accounts.google.com.

Del lado del emisor, el dominio de correo publica un registro DNS TXT que delega en el emisor. El emisor sirve los metadatos en /.well-known/email-verification, informa al navegador del estado de inicio de sesión mediante la Login Status API y, antes de firmar nada, comprueba las cookies de sesión first-party existentes del usuario (consulta Secure Cookies and HttpOnly para saber cómo deben configurarse esas cookies). El borrador también permite que el navegador solicite direcciones privadas por sitio, que el EVT marca con is_private_email: true. La documentación de la prueba de Chrome no describe las direcciones privadas. Las revisiones anteriores del borrador permitían que un emisor recurriera a un desafío WebAuthn cuando no había una cookie válida, pero la revisión -02 eliminó esa opción, por lo que ahora una solicitud sin sesión simplemente falla.

El lado del sitio: marcado del formulario y nonce

En tu lado, el formulario necesita dos campos: un campo de correo normal con autocomplete="email" y un campo oculto con autocomplete="email-verification-token" que contiene un nonce vinculado a la sesión del usuario. Cuando se envía el formulario, el navegador rellena el campo oculto con el token. Este es el marcado de la publicación de Chrome, con el nonce generado por el servidor:

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

Las reglas de vinculación de sesión del borrador exigen un nonce aleatorio con al menos 128 bits de entropía. Genera uno nuevo para cada solicitud de verificación y haz que caduque al cabo de poco tiempo. Guarda junto a él el momento en que se renderizó el formulario, porque el paso 2 del verificador lo necesita:

import { randomBytes } from "node:crypto";

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

La página también necesita tu token de la prueba de origen. La documentación sobre pruebas de origen de Chrome explica cómo proporcionarlo. Según la actualización de agosto, las pruebas de origen de terceros no son compatibles con esta funcionalidad.

Verificar el token en el servidor

La verificación en el servidor de un token del Email Verification Protocol consta de cinco pasos: analizarlo, comprobar los valores esperados, comprobar la vinculación de clave, comprobar la delegación DNS y verificar la firma del emisor. El valor del formulario es <EVT>~<KB-JWT>. El EVT es el token firmado por el emisor y contiene email, email_verified, iss, iat y una clave del navegador en cnf.jwk. El KB-JWT lo firma el navegador y contiene aud, nonce, iat y sd_hash. No confíes en ningún claim decodificado hasta que se hayan superado todos los pasos siguientes. El código utiliza jose y módulos nativos de Node. Cada bloque se ejecuta dentro de un bloque try en una única función verifyEmailToken() que devuelve false si se lanza cualquier excepción.

Estas son las importaciones que necesitan los cinco pasos:

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

1. Análisis. Divide el valor por la tilde y comprueba las cabeceras 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);

Los tokens actuales no incluyen divulgaciones selectivas (selective disclosures), por lo que el valor se divide limpiamente en dos partes. La actualización de agosto califica este atajo de frágil, porque dejaría de funcionar si algún día se añadieran divulgaciones. Chrome recomienda analizar el token con una biblioteca de SD-JWT como @sd-jwt/core, que es la que usa ahora su propia demo.

2. Valores esperados. Los pasos de validación de Chrome comprueban email_verified, email, nonce, aud y un iat posterior al momento en que se renderizó el formulario. Compara el correo sin distinguir mayúsculas de minúsculas: la actualización de agosto advierte que los proveedores pueden devolver la dirección con un uso de mayúsculas distinto al que introdujo el usuario.

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. Vinculación de clave. sd_hash es el SHA-256 del EVT incluida su ~ final. El KB-JWT también debe verificarse con la clave contenida en 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. Delegación DNS. Consulta _email-verification.<domain>. El borrador exige exactamente un registro TXT, y su valor debe empezar por iss=. Usa dig +short TXT _email-verification.gmail.com para comprobar un dominio manualmente.

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. Descubrimiento y firma del EVT. Obtén los metadatos del emisor y, a continuación, verifica el EVT con su jwks_uri. El borrador define el identificador del emisor como https:// seguido del host del registro DNS, y el claim iss del EVT debe coincidir exactamente con él.

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;
ComprobaciónElementoEn caso de fallo
AnálisisDivisión por ~, cabeceras typEnviar correo de confirmación
Valores esperadosemail_verified, email, nonce, aud, iatEnviar correo de confirmación
Vinculación de clavesd_hash, firma del KB-JWT frente a cnf.jwkEnviar correo de confirmación
DNSun único registro TXT iss=Enviar correo de confirmación
Firma del emisormetadatos, jwks_uri, issEnviar correo de confirmación

El módulo DNS de Node devuelve cada registro TXT como un array de fragmentos, motivo por el cual el paso 4 los une.

¿Cuáles son los límites de la verificación de correo electrónico desde el navegador?

Una comprobación satisfactoria del Email Verification Protocol muestra que el usuario tiene actualmente una sesión iniciada con su proveedor de correo con esa dirección. No dice nada sobre si el correo que envíes a esa dirección llegará. Tanto Chrome como el borrador insisten en este punto, así que sigue enviando correos de bienvenida.

El perfil de navegador desde el que el usuario rellena el formulario ya debe tener una sesión iniciada con el proveedor, y la primera vez que se verifica una dirección el usuario ve una solicitud de permiso, una vez por dirección. En la versión de julio, la dirección tenía que proceder del autocompletado. Según la actualización de agosto, ahora escribir o pegar la dirección activa la verificación cuando el campo pierde el foco. El borrador también señala una pequeña fuga de privacidad: el hecho de que llegue o no un token le indica a tu sitio si el usuario tiene una sesión iniciada con el emisor.

Qué hacer ahora

El Email Verification Protocol todavía no está listo para reemplazar tu correo de confirmación. La API del navegador se encuentra en fase de incubación en el WICG, y el protocolo es un Internet-Draft individual (revisión -02, agosto de 2026). Chrome limita el tráfico durante la prueba para que los sitios no construyan sobre la funcionalidad antes de su lanzamiento. Las publicaciones de Chrome no indican ninguna fecha de finalización. La estimación de tres a seis meses procede de los autores del borrador, en sus diapositivas de DISPATCH del IETF 126, no de Chrome. Chrome también advierte de posibles cambios incompatibles con versiones anteriores, y la actualización de agosto indica que la funcionalidad solo está disponible en escritorio hasta Chrome 152, mientras que la compatibilidad con Android aún se está estudiando. La documentación de Chrome solo menciona a Gmail como proveedor participante. El borrador también incluye a Hellō como emisor que lo implementa y menciona una prueba de origen en Microsoft Edge.

El proceso de emisión ya ha cambiado una vez. La publicación de julio describía un JWT request_token codificado como formulario. El borrador lo sustituye por HTTP Message Signatures (RFC 9421) y marca el formato antiguo como obsoleto. A partir de Chrome 153, Chrome envía application/json con las cabeceras Signature, Signature-Input y Signature-Key, y un cuerpo que contiene únicamente la dirección de correo. Se trata de un cambio del lado del emisor y no afecta al código de tu verificador.

Mantén tu flujo de OTP o de enlace mágico como opción predeterminada. Si te unes a la prueba, trata el token como una mejora progresiva: cuando se verifique correctamente, omite el correo. Tras la verificación, crea la sesión como lo haces normalmente (consulta Cookies vs localStorage for JWT Auth). El EVT+KB es una prueba de un solo uso y no debe utilizarse como token de sesión.

El protocolo sustituye el “ve a revisar tu bandeja de entrada” por una comprobación de firma que ejecutas en tu propio servidor, y el verificador solo necesita cinco pasos. Añade el campo oculto y el nonce, despliega verifyEmailToken() por delante de tu correo de confirmación actual y consulta la publicación de actualizaciones de Chrome para conocer los cambios antes de cada versión de Chrome.

Preguntas frecuentes

¿En qué se diferencia el Email Verification Protocol de Sign in with Google?

Sign in with Google es una integración de inicio de sesión específica de un proveedor que crea una relación de identidad entre tu sitio y Google. El Email Verification Protocol solo confirma que el usuario controla una dirección de correo electrónico. No requiere ningún SDK del proveedor en tu sitio y, como el navegador actúa de intermediario en el intercambio, el emisor firma el token sin saber qué sitio lo solicitó.

¿Puedo usar crypto.randomUUID() para generar el nonce?

No. Un UUID de versión 4 contiene solo 122 bits aleatorios, porque los campos de versión y variante ocupan los 6 bits restantes. El borrador del IETF exige un nonce con al menos 128 bits de entropía. Usa en su lugar randomBytes(32).toString('base64url') de node:crypto, que proporciona 256 bits. Guarda el nonce en la sesión del servidor y elimínalo tras un único uso.

¿Funciona el Email Verification Protocol con direcciones de dominios personalizados o de proveedores distintos de Gmail?

Solo si el dominio de correo publica exactamente un registro TXT _email-verification, que empiece por iss= y que delegue en un emisor compatible con el protocolo. La documentación de Chrome menciona a Gmail como proveedor de correo participante. Para cualquier otro dominio que no tenga ese registro, el navegador no puede obtener un token, por lo que tu verificador debe tratarlo como una comprobación fallida y enviar el correo de confirmación habitual.

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.