Implementazione del provider email (emittente)

Per ulteriori dettagli, puoi esaminare il codice della demo del provider di email simulato e consultare i passaggi per l'emittente nelle proposte relative all'API Email Verification e al protocollo Email Verification.

In qualità di emittente, non devi registrarti alla prova dell'origine né fornire un token perché il sito della relying party attiva il comportamento del browser. Assicurati che i tuoi endpoint siano configurati per rispondere a queste richieste.

Configura il rilevamento dell'emittente

Per consentire ai browser di rilevare automaticamente gli endpoint di verifica quando viene selezionato un indirizzo email appartenente al tuo dominio, esponi la configurazione utilizzando DNS e un endpoint HTTP .well-known.

Configura il record di delega DNS

Configura un record TXT DNS sul tuo dominio email che delega l'autorità di verifica all'identificatore dell'emittente. Questi identificatori possono utilizzare lo stesso dominio a seconda dell'infrastruttura.

Formato record: _email-verification.<email-domain>

File di zona di esempio:

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

Ospitare un endpoint .well-known/email-verification

Ospita un file di metadati JSON sul tuo dominio emittente nel percorso /.well-known/. Questo file descrive le tue funzionalità di emissione e gli algoritmi di firma crittografica supportati dalla tua infrastruttura.

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

Esempio di risposta:

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

Ospitare un endpoint .well-known/web-identity

Potresti aver già implementato una risorsa JSON .well-known aggiuntiva nell'ambito dell'API Federated Credentials (FedCM). Questa risorsa fornisce link all'endpoint e all'URL di accesso dei tuoi account.

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

Esempio di risposta:

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

Utilizzare un endpoint degli account

L'endpoint accounts dell'API FedCM fornisce un elenco degli account a cui è stato eseguito l'accesso al momento. L'esempio seguente mostra una risposta minima. Per maggiori dettagli, consulta la guida all'implementazione del provider di identità.

Endpoint: come specificato in .well-known/web-identity

Di seguito è riportato un esempio di risposta:

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

Integrare l'API Login Status

L'utente deve avere una sessione attiva con il fornitore e devi segnalarlo al browser utilizzando l'API Login Status.

Quando un utente esegue l'accesso o la disconnessione, pubblica l'intestazione della risposta HTTP corrispondente:

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

In alternativa, aggiorna lo stato utilizzando JavaScript nel contesto dell'applicazione web:

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

Gestire le richieste di emissione

Il tuo issuance_endpoint riceve una richiesta application/json POST che contiene la chiave email e le intestazioni HTTP Message Signatures per Signature, Signature-Input e Signature-Key.

Utilizza una libreria che supporti le intestazioni strutturate e le firme dei messaggi HTTP per il tuo ambiente. In Node.js puoi utilizzare structured-headers e http-message-sig.

Formato della richiesta completa:

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

Analizza e convalida la richiesta:

  • Autenticazione della sessione: convalida i cookie di sessione proprietari inviati con la richiesta. L'utente deve essere autenticato.
  • Intestazione Sec-Fetch-Dest: impostata su email-verification.
  • Firme dei messaggi HTTP: verifica la firma della richiesta utilizzando la chiave pubblica effimera in Signature-Key e convalida Content-Digest.
  • Payload: il corpo JSON contiene la stringa email richiesta per la verifica.

Risposta all'emissione

Una volta convalidati correttamente la sessione e il token di richiesta, genera un JWT di divulgazione selettiva (SD-JWT) firmato restituito come JSON utilizzando le librerie appropriate per la tua piattaforma. Ad esempio, per Node puoi utilizzare @sd-jwt/core e jose.

Il formato del payload non elaborato dovrebbe essere simile a questo:

{
  "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 e restituisci il 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);

Il corpo della risposta risultante è simile al seguente:

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

Dopo aver creato e firmato il token di verifica email, il browser lo passa al sito di verifica per la convalida.

Risoluzione dei problemi

Se il browser non contatta gli endpoint o rifiuta i token emessi, controlla i seguenti problemi comuni:

Il browser non chiama mai accounts_endpoint o issuance_endpoint

  • Stato accesso non impostato: Chrome esegue query sugli endpoint solo se sa che l'utente ha eseguito l'accesso. Assicurati che il flusso di accesso imposti l'intestazione HTTP Set-Login: logged-in o chiami navigator.login.setStatus("logged-in").
  • Cookie di sessione bloccati (SameSite=None): il browser recupera i tuoi accounts_endpoint e issuance_endpoint da una relying party su un sito diverso. Poiché si tratta di richieste cross-site, il cookie di sessione deve includere SameSite=None; Secure. Un cookie SameSite=Lax funziona durante il test su un verificatore dello stesso sito, ma viene omesso nelle richieste cross-site.
  • Rilevamento o mancata corrispondenza dell'account: verifica che _email-verification.<email-domain> restituisca un singolo record TXT (iss=<issuer-domain>, senza https://), che entrambi gli endpoint .well-known restituiscano Content-Type: application/json e che la risposta accounts_endpoint includa un account il cui email corrisponda all'indirizzo inserito.

La convalida della richiesta di emissione non riesce

  • Ortografia dell'intestazione Sec-Fetch-Dest: Chrome 154 e versioni successive inviano Sec-Fetch-Dest: email-verification (con un trattino), mentre Chrome 153 inviava emailverification (senza trattino). Accetta entrambi i valori durante l'implementazione.
  • Mancata corrispondenza della firma del messaggio HTTP (@authority) dietro un proxy: durante la verifica della firma RFC 9421, il componente @authority riflette l'host pubblico. Se il server si trova dietro un proxy inverso o un bilanciatore del carico, ricostruisci l'URL di verifica utilizzando X-Forwarded-Host (o l'origine pubblica) anziché il nome host interno e calcola Content-Digest sui byte del corpo della richiesta non elaborati prima dell'analisi JSON.

Il browser rifiuta il issuance_token restituito

  • Rivendicazione email modificata o canonica: a partire da Chrome 156, il browser verifica che la rivendicazione email EVT corrisponda al email richiesto byte per byte. Se il backend normalizza l'indirizzo in un formato di account canonico (ad esempio restituendo First.Last@example.com quando è stato richiesto first.LAST@example.com), Chrome elimina il token. Corrispondi alla richiesta con l'account dell'utente, ma restituisci la stringa email esatta ricevuta nel corpo della richiesta.
  • Tilde finale mancante (~): anche con zero divulgazioni, il issuance_token deve essere un SD-JWT valido che termina con una tilde finale (<Issuer-signed-JWT>~) in modo che il browser possa aggiungere <KB-JWT>.
  • iss o cnf.jwk non corrispondenti: assicurati che l'attestazione EVT iss sia l'origine HTTPS esatta (https://<issuer-domain>, senza barra finale) corrispondente ai metadati .well-known/email-verification e che cnf.jwk incorpori la chiave pubblica effimera del browser dall'intestazione Signature-Key.