Implementación del proveedor de correo electrónico (entidad emisora)

Para obtener más detalles, puedes consultar el código de demostración del proveedor de correo electrónico simulado y consultar los pasos del emisor en las propuestas de la API de Email Verification y el Protocolo de Email Verification.

Como entidad emisora, no necesitas registrarte en la prueba de origen ni proporcionar un token, ya que el sitio de la parte que confía activa el comportamiento del navegador. Asegúrate de que tus extremos estén configurados para responder a esas solicitudes.

Configura el descubrimiento del emisor

Para permitir que los navegadores descubran automáticamente tus extremos de verificación cuando se selecciona una dirección de correo electrónico que pertenece a tu dominio, expón tu configuración con DNS y un extremo HTTP .well-known.

Configura el registro de delegación de DNS

Configura un registro TXT de DNS en tu dominio de correo electrónico que delegue la autoridad de verificación en el identificador de tu entidad emisora. Estos identificadores pueden usar el mismo dominio según tu infraestructura.

Formato de registro: _email-verification.<email-domain>

Ejemplo de archivo de zona:

_email-verification.example.com IN TXT "iss=accounts.issuer.example"

Cómo alojar un extremo de .well-known/email-verification

Aloja un archivo de metadatos JSON en la ruta /.well-known/ de tu dominio de la entidad emisora. En este archivo, se describen tus capacidades de emisión y los algoritmos de firma criptográfica que admite tu infraestructura.

Endpoint: https://<issuer-domain>/.well-known/email-verification

Respuesta de ejemplo:

{
  "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"]
}

Cómo alojar un extremo de .well-known/web-identity

Es posible que ya hayas implementado un recurso JSON de .well-known adicional como parte de la API de Federated Credentials (FedCM). Este recurso proporciona vínculos al extremo de tus cuentas y a la URL de acceso.

Endpoint: https://<domain>/.well-known/web-identity

Respuesta de ejemplo:

{
  "accounts_endpoint": "https://accounts.issuer.example/accounts",
  "login_url": "https://accounts.issuer.example/login"
}

Usa un extremo de cuentas

El extremo de cuentas de la API de FedCM proporciona una lista de las cuentas que accedieron en el momento. En el siguiente ejemplo, se muestra una respuesta mínima. Para obtener más detalles, consulta la guía de implementación del proveedor de identidad.

Endpoint: Como se especifica en .well-known/web-identity

A continuación, se muestra una respuesta de ejemplo:

{
  "accounts": [
    {
      "id": "demo-example",
      "name": "Demo User",
      "email": "demo@example.com",
      "given_name": "Demo"
    }
  ]
}

Realiza la integración con la API de Login Status

El usuario debe tener una sesión activa con el proveedor, y debes indicárselo al navegador con la API de estado de acceso.

Cuando un usuario acceda o salga correctamente, publica el encabezado de respuesta HTTP correspondiente:

Set-Login: logged-in
Set-Login: logged-out

Como alternativa, actualiza el estado con JavaScript en el contexto de tu aplicación web:

navigator.login.setStatus("logged-in");
navigator.login.setStatus("logged-out");

Cómo controlar las solicitudes de emisión

Tu issuance_endpoint recibe una solicitud application/json POST que contiene la clave email y los encabezados de firmas de mensajes HTTP para Signature, Signature-Input y Signature-Key.

Usa una biblioteca que admita encabezados estructurados y firmas de mensajes HTTP para tu entorno. En Node.js, puedes usar structured-headers y http-message-sig.

Formato de solicitud completo:

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"}

Analiza y valida la solicitud:

  • Autenticación de sesión: Valida las cookies de sesión propias que se envían con la solicitud. El usuario debe estar autenticado.
  • Encabezado Sec-Fetch-Dest: Se establece en email-verification.
  • Firmas de mensajes HTTP: Verifica la firma de la solicitud con la clave pública efímera en Signature-Key y valida Content-Digest.
  • Carga útil: El cuerpo JSON contiene la cadena email solicitada para la verificación.

Respuesta de emisión

Una vez que se validen correctamente la sesión y el token de solicitud, genera un SD-JWT firmado que se muestre como JSON con las bibliotecas adecuadas para tu plataforma. Por ejemplo, para Node, puedes usar @sd-jwt/core y jose.

El formato de carga útil sin procesar debería verse de la siguiente manera:

{
  "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
}

Crea, firma y devuelve el token:

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);

El cuerpo de la respuesta resultante es similar al siguiente:

{
  "issuance_token": "tOkEn123tOkEn123tOkEn123...~"
}

Después de crear y firmar el token de verificación por correo electrónico, el navegador lo pasa al sitio del verificador para su validación.

Solución de problemas

Si el navegador no se comunica con tus extremos o rechaza los tokens emitidos, verifica los siguientes problemas habituales:

El navegador nunca llama a accounts_endpoint o issuance_endpoint

  • Estado de acceso no establecido: Chrome solo consulta tus extremos si sabe que el usuario accedió. Asegúrate de que tu flujo de acceso establezca el encabezado HTTP Set-Login: logged-in o llame a navigator.login.setStatus("logged-in").
  • Cookies de sesión bloqueadas (SameSite=None): El navegador recupera tu accounts_endpoint y issuance_endpoint de un tercero de confianza en un sitio diferente. Dado que se trata de solicitudes entre sitios, tu cookie de sesión debe incluir SameSite=None; Secure. Una cookie SameSite=Lax funciona cuando se prueba en un verificador del mismo sitio, pero se omite en las solicitudes entre sitios.
  • Descubrimiento o discrepancia de la cuenta: Verifica que _email-verification.<email-domain> devuelva un solo registro TXT (iss=<issuer-domain>, sin https://), que ambos extremos .well-known devuelvan Content-Type: application/json y que la respuesta accounts_endpoint incluya una cuenta cuyo email coincida con la dirección ingresada.

Falla la validación de la solicitud de emisión

  • Ortografía del encabezado Sec-Fetch-Dest: Chrome 154 y versiones posteriores envían Sec-Fetch-Dest: email-verification (con un guion), mientras que Chrome 153 enviaba emailverification (sin un guion). Acepta ambos valores durante el lanzamiento.
  • No coincide la firma del mensaje HTTP (@authority) detrás de un proxy: Cuando se verifica la firma de RFC 9421, el componente @authority refleja el host público. Si tu servidor se encuentra detrás de un proxy inverso o un balanceador de cargas, reconstruye la URL de verificación con X-Forwarded-Host (o tu origen público) en lugar del nombre de host interno y calcula Content-Digest sobre los bytes del cuerpo de la solicitud sin procesar antes del análisis de JSON.

El navegador rechaza el issuance_token devuelto

  • Reclamo email modificado o canonizado: A partir de Chrome 156, el navegador verifica que el reclamo email del EVT coincida byte por byte con el email solicitado. Si tu backend normaliza la dirección a un formato de cuenta canónico (por ejemplo, devuelve First.Last@example.com cuando se solicitó first.LAST@example.com), Chrome descarta el token. Haz coincidir la solicitud con la cuenta del usuario, pero devuelve la cadena email exacta que se recibió en el cuerpo de la solicitud.
  • Falta la virgulilla final (~): Incluso con cero divulgaciones, el issuance_token debe ser un SD-JWT válido que termine con una virgulilla final (<Issuer-signed-JWT>~) para que el navegador pueda agregar el <KB-JWT>.
  • iss o cnf.jwk no coinciden: Asegúrate de que el reclamo iss del EVT sea el origen HTTPS exacto (https://<issuer-domain>, sin barra final) que coincida con tus metadatos de .well-known/email-verification y que cnf.jwk incorpore la clave pública efímera del navegador desde el encabezado Signature-Key.