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 enemail-verification. - Firmas de mensajes HTTP: Verifica la firma de la solicitud con la clave pública efímera en
Signature-Keyy validaContent-Digest. - Carga útil: El cuerpo JSON contiene la cadena
emailsolicitada 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-ino llame anavigator.login.setStatus("logged-in"). - Cookies de sesión bloqueadas (
SameSite=None): El navegador recupera tuaccounts_endpointyissuance_endpointde un tercero de confianza en un sitio diferente. Dado que se trata de solicitudes entre sitios, tu cookie de sesión debe incluirSameSite=None; Secure. Una cookieSameSite=Laxfunciona 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>, sinhttps://), que ambos extremos.well-knowndevuelvanContent-Type: application/jsony que la respuestaaccounts_endpointincluya una cuenta cuyoemailcoincida 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íanSec-Fetch-Dest: email-verification(con un guion), mientras que Chrome 153 enviabaemailverification(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@authorityrefleja 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 conX-Forwarded-Host(o tu origen público) en lugar del nombre de host interno y calculaContent-Digestsobre los bytes del cuerpo de la solicitud sin procesar antes del análisis de JSON.
El navegador rechaza el issuance_token devuelto
- Reclamo
emailmodificado o canonizado: A partir de Chrome 156, el navegador verifica que el reclamoemaildel EVT coincida byte por byte con elemailsolicitado. Si tu backend normaliza la dirección a un formato de cuenta canónico (por ejemplo, devuelveFirst.Last@example.comcuando se solicitófirst.LAST@example.com), Chrome descarta el token. Haz coincidir la solicitud con la cuenta del usuario, pero devuelve la cadenaemailexacta que se recibió en el cuerpo de la solicitud. - Falta la virgulilla final (
~): Incluso con cero divulgaciones, elissuance_tokendebe ser un SD-JWT válido que termine con una virgulilla final (<Issuer-signed-JWT>~) para que el navegador pueda agregar el<KB-JWT>. issocnf.jwkno coinciden: Asegúrate de que el reclamoissdel EVT sea el origen HTTPS exacto (https://<issuer-domain>, sin barra final) que coincida con tus metadatos de.well-known/email-verificationy quecnf.jwkincorpore la clave pública efímera del navegador desde el encabezadoSignature-Key.