Implementação do provedor de e-mail (emissor)

Para mais detalhes, confira o código de demonstração do provedor de e-mail simulado e consulte as etapas do emissor nas propostas da API Email Verification e do protocolo Email Verification.

Como emissor, você não precisa se inscrever no teste de origem nem fornecer um token porque o site da parte confiante aciona o comportamento do navegador. Verifique se os endpoints estão configurados para responder a essas solicitações.

Configurar a descoberta de emissores

Para permitir que os navegadores descubram automaticamente seus endpoints de verificação quando um endereço de e-mail pertencente ao seu domínio for selecionado, exponha sua configuração usando DNS e um endpoint HTTP .well-known.

Configurar um registro de delegação de DNS

Configure um registro TXT de DNS no seu domínio de e-mail que delegue a autoridade de verificação ao identificador do emissor. Esses identificadores podem usar o mesmo domínio, dependendo da sua infraestrutura.

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

Exemplo de arquivo de zona:

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

Hospedar um endpoint .well-known/email-verification

Hospede um arquivo JSON de metadados no domínio do emissor no caminho /.well-known/. Esse arquivo descreve suas capacidades de emissão e os algoritmos de assinatura criptográfica compatíveis com sua infraestrutura.

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

Exemplo de resposta:

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

Hospedar um endpoint .well-known/web-identity

Talvez você já tenha implementado um recurso JSON .well-known adicional como parte da API Federated Credentials (FedCM). Esse recurso fornece links para o endpoint de contas e o URL de login.

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

Exemplo de resposta:

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

Usar um endpoint de contas

O endpoint "accounts" da API FedCM fornece uma lista de contas conectadas no momento. O exemplo a seguir mostra uma resposta mínima. Para mais detalhes, consulte o guia de implementação do provedor de identidade.

Endpoint: conforme especificado em .well-known/web-identity

Veja a seguir um exemplo de resposta:

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

Integrar com a API Login Status

O usuário precisa ter uma sessão ativa com o provedor, e você precisa sinalizar isso para o navegador usando a API Login Status (em inglês).

Quando um usuário fizer login ou sair, veicule o cabeçalho de resposta HTTP correspondente:

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

Como alternativa, atualize o status usando JavaScript no contexto do aplicativo da Web:

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

Processar solicitações de emissão

Seu issuance_endpoint recebe uma solicitação application/json POST que contém a chave email e os cabeçalhos Assinaturas de mensagens HTTP para Signature, Signature-Input e Signature-Key.

Use uma biblioteca que ofereça suporte a cabeçalhos estruturados e assinaturas de mensagens HTTP para seu ambiente. No Node.js, é possível usar structured-headers e http-message-sig.

Formato completo da solicitação:

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

Analise e valide a solicitação:

  • Autenticação de sessão: valide os cookies de sessão próprios enviados com a solicitação. O usuário precisa ser autenticado.
  • Cabeçalho Sec-Fetch-Dest: defina como email-verification.
  • Assinaturas de mensagens HTTP: verifique a assinatura da solicitação usando a chave pública efêmera em Signature-Key e valide o Content-Digest.
  • Payload: o corpo JSON contém a string email solicitada para verificação.

Resposta de emissão

Após a validação bem-sucedida da sessão e do token de solicitação, gere um JWT de divulgação seletiva (SD-JWT) assinado retornado como JSON usando as bibliotecas adequadas para sua plataforma. Por exemplo, para Node, use @sd-jwt/core e jose.

O formato de payload bruto será semelhante a este:

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

Crie, assine e retorne o 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);

O corpo da resposta resultante é semelhante a:

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

Depois de criar e assinar o token de verificação de e-mail, o navegador passa isso para o site do verificador para validação.

Solução de problemas

Se o navegador não entrar em contato com seus endpoints ou rejeitar os tokens emitidos, verifique os seguintes problemas comuns:

O navegador nunca chama accounts_endpoint ou issuance_endpoint

  • Status de login não definido: o Chrome só consulta seus endpoints se souber que o usuário fez login. Confira se o fluxo de login define o cabeçalho HTTP Set-Login: logged-in ou chama navigator.login.setStatus("logged-in").
  • Cookies de sessão bloqueados (SameSite=None): o navegador busca seu accounts_endpoint e issuance_endpoint de um terceiro confiável em um site diferente. Como essas são solicitações entre sites, seu cookie de sessão precisa incluir SameSite=None; Secure. Um cookie SameSite=Lax funciona ao testar em um verificador do mesmo site, mas é omitido em solicitações entre sites.
  • Descoberta ou incompatibilidade de conta: verifique se _email-verification.<email-domain> retorna um único registro TXT (iss=<issuer-domain>, sem https://), se os dois endpoints .well-known retornam Content-Type: application/json e se a resposta accounts_endpoint inclui uma conta cujo email corresponde ao endereço inserido.

A validação da solicitação de emissão falha

  • Ortografia do cabeçalho Sec-Fetch-Dest: o Chrome 154 e versões mais recentes enviam Sec-Fetch-Dest: email-verification (com um hífen), enquanto o Chrome 153 enviava emailverification (sem um hífen). Aceite os dois valores durante o lançamento.
  • Incompatibilidade de assinatura de mensagem HTTP (@authority) por trás de um proxy: ao verificar a assinatura RFC 9421, o componente @authority reflete o host público. Se o servidor estiver atrás de um proxy reverso ou balanceador de carga, reconstrua o URL de verificação usando X-Forwarded-Host (ou sua origem pública) em vez do nome do host interno e calcule Content-Digest nos bytes do corpo da solicitação bruta antes da análise JSON.

O navegador rejeita o issuance_token retornado

  • Declaração email modificada ou canonizada: a partir do Chrome 156, o navegador verifica se a declaração EVT email corresponde ao email solicitado byte a byte. Se o back-end normalizar o endereço para um formato de conta canônica (como retornar First.Last@example.com quando first.LAST@example.com foi solicitado), o Chrome vai descartar o token. Corresponda a solicitação à conta do usuário, mas retorne a string email exata recebida no corpo da solicitação.
  • Til final ausente (~): mesmo sem divulgações, o issuance_token precisa ser um SD-JWT válido que termine com um til (<Issuer-signed-JWT>~) para que o navegador possa anexar o <KB-JWT>.
  • iss ou cnf.jwk incompatíveis: verifique se a declaração iss do EVT é a origem HTTPS exata (https://<issuer-domain>, sem barra invertida à direita) que corresponde aos metadados .well-known/email-verification, e se cnf.jwk incorpora a chave pública efêmera do navegador do cabeçalho Signature-Key.