Implementación de la parte autenticada

Para implementar la verificación por correo electrónico en tu sitio, actualiza el lenguaje de marcado de tu formulario para solicitar el token y agrega la validación del servidor para los tokens entrantes.

Regístrate en la prueba de origen

Los sitios que se verifican deben tener configurada la prueba de origen en su sitio.

A partir de Chrome 154, se admiten las pruebas de origen de terceros con una advertencia importante: el origen registrado para la prueba debe ser del mismo sitio que el emisor. Por ejemplo:

  • Dominio de la entidad emisora: issuer.example
  • Registrante de OT: https://issuer.example
  • Origen de JavaScript: https://issuer.example (o https://app.issuer.example con coincidencia de subdominio)

Configura los campos del formulario

Agrega un campo de token oculto a tu formulario de envío de correo electrónico:

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

Requisitos de los campos:

  • Campo de correo electrónico: Establece type="email" y autocomplete="email" para que Chrome pueda autocompletar y reconocer la dirección.
  • Atributos del campo de token:
    • Conjunto autocomplete="email-verification-token": Chrome identifica este campo para completar el token cuando se envía.
    • Establece nonce="<VALUE>": El sitio debe proporcionar un nonce único vinculado a la sesión para verificar el envío del formulario.

Valida el token de verificación por correo electrónico (EVT)

Cuando el usuario envía el formulario, tu servidor recibe la dirección de correo electrónico y el token del campo oculto. Un campo de token vacío indica que el navegador o el proveedor no admiten la EVP, o que el usuario omitió la verificación. Si esto ocurre, vuelve a tu proceso de verificación existente, como enviar una OTP o un vínculo mágico.

Si hay un token, valídalo de la siguiente manera:

  1. Analiza el token con una biblioteca de SD-JWT.
  2. Valida los valores esperados y los reclamos de sesión.
  3. Verifica la delegación de DNS.
  4. Descubre los metadatos del emisor y recupera los JWKS.
  5. Verifica las firmas criptográficas y la vinculación de claves.

1. Analiza el token

El token usa el formato RFC 9901: JWT de divulgación selectiva (SD-JWT+KB). Usa las bibliotecas adecuadas para tu plataforma para analizar y validar el token. Por ejemplo, para Node, puedes usar @sd-jwt/core y jose. En su forma sin procesar, se ve de la siguiente manera: un JWT firmado por la entidad emisora, seguido de cero o más divulgaciones y que termina con un JWT de vinculación de clave, con cada componente separado por una virgulilla:

<Issuer-signed EVT>~<Disclosure 1>~...~<Disclosure N>~<Key Binding JWT>

En la implementación actual, el token no contiene divulgaciones (<Issuer-signed EVT>~<Key Binding JWT>). Sin embargo, esto podría cambiar en el futuro.

Decodifica el token con la biblioteca:

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;

Si verifier.example verifica demo@provider.example, el token decodificado se verá similar al siguiente:

{
  "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. Valida los valores esperados y los reclamos de sesión

Verifica que los valores básicos de la carga útil coincidan con los valores proporcionados y esperados:

  • email_verified: Debe ser true.
  • email: Debe coincidir con la dirección de correo electrónico enviada en el formulario.
  • aud (público): Debe coincidir con el origen de tu sitio.
  • nonce: Debe coincidir con el nonce proporcionado en tu formulario.
  • iat (fecha de emisión) y exp (fecha de vencimiento): Confirma que el token se encuentre dentro de su período válido y que no haya vencido.

3. Verifica la delegación de DNS

Verifica el registro DNS _email-verification del dominio de la dirección de correo electrónico. Por ejemplo, para demo@gmail.com, consulta el registro TXT de _email-verification.gmail.com. En el caso de este proveedor, la consulta devuelve la ubicación del proveedor de la cuenta, es decir, accounts.google.com.

$ dig +short TXT _email-verification.gmail.com
"iss=accounts.google.com"

Verifica que el esquema de la entidad emisora sea https:// y que https://<domain> coincida con el reclamo iss en el EVT.

4. Verifica la firma del EVT

Recupera los metadatos de descubrimiento del emisor de 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"]
}

Recupera el conjunto de claves web JSON de jwks_uri.

Usa tu biblioteca de SD-JWT para verificar el paquete de tokens. La biblioteca coordina la validación:

  1. Validar la firma del emisor en el EVT con respecto al JWKS recuperado
  2. Validar la firma del navegador en el KB-JWT con la clave pública efímera en cnf.jwk
  3. Verifica la vinculación de la clave (aud, nonce y el hash del resumen sd_hash).

Ejemplo de lógica de verificación en 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;

Si todos los pasos se completan correctamente, habrás verificado la dirección de correo electrónico con el proveedor. Si falla la verificación, vuelve a enviar un correo electrónico de confirmación al usuario con tu flujo normal.

Solución de problemas

Si la verificación falla o el navegador no proporciona un token, verifica los siguientes problemas comunes:

El campo de token está vacío al enviar

  • Registro de la prueba de origen: Confirma que el encabezado Origin-Trial o la etiqueta <meta> se publiquen en la página. En el caso de las pruebas de origen de terceros (Chrome 154 y versiones posteriores), el origen de la prueba registrado debe ser del mismo sitio que el emisor (https://<issuer-domain>). Puedes inspeccionar la configuración de la prueba de origen en un sitio en Herramientas para desarrolladores en Aplicación > Marcos > (selecciona el marco pertinente) > Pruebas de origen.
  • Marcado del formulario: Tanto <input type="email" autocomplete="email"> como <input type="hidden" autocomplete="email-verification-token" nonce="..."> deben estar en el mismo elemento <form> (no aislados a través de los límites del DOM de sombra), y nonce no debe estar vacío.
  • Envío anticipado o página reutilizada: El navegador recupera el token en segundo plano después de que se ingresa o autocompleta el correo electrónico. Si envías el formulario antes de que se complete la solicitud, el token quedará vacío. Esto puede suceder si el usuario presiona Retorno para enviar el formulario después de ingresar su correo electrónico.
  • Requisitos previos del navegador y el proveedor: El usuario debe haber accedido a un proveedor participante en el mismo perfil del navegador y tener habilitado el correo electrónico verificado en la configuración de Chrome (chrome://settings/contactInfo).

Falla la verificación de la firma de la entidad emisora

  • Falta el encabezado kid: La reclamación kid (ID de clave) en el encabezado EVT y el JWKS es opcional (por ejemplo, Gmail omite kid). Si falta kid, itera por todas las claves candidatas en el jwks_uri del emisor en lugar de fallar en una búsqueda de ID de clave.
  • Identificadores de algoritmo (EdDSA y Ed25519): Las entidades emisoras y las bibliotecas pueden especificar EdDSA o Ed25519 (junto con ES256). Asegúrate de que la lógica de importación y verificación de JWK acepte ambos identificadores.
  • Formato de origen del emisor (iss): El registro TXT de DNS (_email-verification.<domain>) contiene un nombre de host simple (iss=accounts.issuer.example), mientras que el reclamo de EVT iss es un origen HTTPS completo (https://accounts.issuer.example, sin barra final). Agrega el prefijo https:// al valor del registro DNS antes de compararlo.

Falla la validación de la vinculación de claves (KB-JWT)

  • Nonce no coincidente o vencido: Asegúrate de que el nonce renderizado en el <input> coincida con el nonce de sesión activo en tu servidor y que no se haya reemplazado con otra pestaña ni se haya consumido con una solicitud anterior.
  • Incongruencia de público (aud): El reclamo aud es el origen HTTPS del verificador (https://verifier.example, sin ruta de acceso ni barra final).

Falla la comparación de la reclamación de correo electrónico (email)

  • Uso de mayúsculas y minúsculas y canonización: Chrome 156 y versiones posteriores devuelven el reclamo email byte por byte tal como se ingresó en el formulario, pero las versiones anteriores del navegador o los proveedores pueden devolver una dirección canonizada (por ejemplo, First.Last@example.com para first.last@example.com). Usa una comparación que no distinga mayúsculas de minúsculas cuando compares el reclamo email del token con el valor del formulario enviado.