Implementatie van de Relying Party

Als u e-mailverificatie op uw site wilt implementeren, updatet u de formuliermarkering om de token aan te vragen en voegt u validatie aan de serverzijde toe voor inkomende tokens.

Registreren voor de origin trial

Voor de verificatie van sites moet de origin trial op de site zijn ingesteld.

Vanaf Chrome 154 worden origin trials van derden ondersteund met een belangrijk voorbehoud: de geregistreerde oorsprong voor de trial moet dezelfde site zijn als de uitgever. Voorbeeld:

  • Kaartuitgevende domein: issuer.example
  • OT-registrant: https://issuer.example
  • JavaScript-oorsprong: https://issuer.example (of https://app.issuer.example met overeenkomende subdomeinen)

Formuliervelden instellen

Voeg een verborgen tokenveld toe aan je e-mailinzendingsformulier:

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

Veldvereisten:

  • E-mailveld: Stel type="email" en autocomplete="email" in zodat Chrome het adres automatisch kan invullen en herkennen.
  • Kenmerken van tokenvelden:
    • Instellen autocomplete="email-verification-token": Chrome herkent dit veld om het token in te vullen bij de indiening.
    • Instellen nonce="<VALUE>": De site moet een unieke, aan de sessie gebonden nonce bieden om de formulierinzending te verifiëren.

Valideer het e-mailverificatietoken (EVT).

Als de gebruiker het formulier indient, krijgt uw server het e-mailadres en de token uit het verborgen veld. Een leeg tokenveld geeft aan dat de browser of provider EVP niet ondersteunt of dat de gebruiker de verificatie heeft overgeslagen. Als dit gebeurt, val je terug op je bestaande verificatieproces, zoals het sturen van een OTP of een magische link.

Als er een token aanwezig is, valideer je deze zo:

  1. Parse de token met een SD-JWT-bibliotheek.
  2. Valideer verwachte waarden en sessieclaims.
  3. Controleer de DNS-delegatie.
  4. Metadata van de uitgever ontdekken en JWKS ophalen.
  5. Cryptografische handtekeningen en sleutelbinding verifiëren.

1. De token parseren

De token gebruikt de indeling RFC 9901: Selective Disclosure JWT (SD-JWT+KB). Gebruik de juiste bibliotheken voor je platform om de token te parseren en te valideren. Voor Node kun je bijvoorbeeld @sd-jwt/core en jose gebruiken. In de onbewerkte vorm ziet dit er zo uit: een door de uitgever ondertekende JWT, gevolgd door 0 of meer kennisgevingen en eindigend met een JWT voor sleutelbinding, waarbij elke component wordt gescheiden door een tilde:

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

In de huidige implementatie bevat het token geen kennisgevingen (<Issuer-signed EVT>~<Key Binding JWT>). Dit kan in de toekomst veranderen.

Decodeer de token met de bibliotheek:

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;

Als verifier.example demo@provider.example verifieert, ziet de gedecodeerde token er ongeveer zo uit:

{
  "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. Verwachte waarden en sessieclaims valideren

Controleer of de basiswaarden in de payload overeenkomen met de door u verstrekte en verwachte waarden:

  • email_verified: Moet true zijn.
  • email: Moet overeenkomen met het e-mailadres dat in het formulier is ingediend.
  • aud (doelgroep): Moet overeenkomen met de oorsprong van je site.
  • nonce: Moet overeenkomen met de nonce die u in uw formulier heeft ingevoerd.
  • iat (uitgegeven op) en exp (vervaldatum): Bevestig dat het token binnen de geldige periode valt en niet is verlopen.

3. DNS-delegatie verifiëren

Verifieer de _email-verification DNS-record voor het domein van het e-mailadres. Vraag bijvoorbeeld voor demo@gmail.com het _email-verification.gmail.com TXT-record op. Voor deze provider retourneert de query de locatie van de accountprovider, namelijk accounts.google.com.

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

Controleer of het uitgifte-schema https:// is en of https://<domain> overeenkomt met de iss-claim in de EVT.

4. De EVT-handtekening verifiëren

Haal de metadata voor ontdekking van de uitgever op uit 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"]
}

Haal de set json-websleutels op uit jwks_uri.

Gebruik je SD-JWT-bibliotheek om het tokenpakket te verifiëren. De bibliotheek coördineert de validatie:

  1. De handtekening van de uitgever op de EVT valideren aan de hand van de opgehaalde JWKS.
  2. De handtekening van de browser voor de KB-JWT valideren met de tijdelijke openbare sleutel in cnf.jwk.
  3. De sleutelbinding verifiëren (aud, nonce en de hash van het digest sd_hash).

Voorbeeld van verificatielogica in 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;

Als alle stappen zijn gelukt, heb je het e-mailadres laten verifiëren door de provider. Als de verificatie mislukt, valt u terug op het sturen van een bevestigingsmail naar de gebruiker via uw normale proces.

Problemen oplossen

Als de verificatie mislukt of de browser geen token levert, check je de volgende veelvoorkomende problemen:

Tokenveld is leeg bij indiening

  • Origin trial-registratie: Bevestig dat de Origin-Trial-header of de <meta>-tag op de pagina wordt weergegeven. Voor origin trials van derden (Chrome 154 en hoger) moet de geregistreerde origin trial same-site zijn voor de uitgever (https://<issuer-domain>). Je kunt de configuratie van de origin trial op een site inspecteren in DevTools onder Application > Frames > (selecteer het relevante frame) > Origin trials.
  • Formulieropmaak: Zowel <input type="email" autocomplete="email"> als <input type="hidden" autocomplete="email-verification-token" nonce="..."> moeten in hetzelfde <form>-element staan (niet geïsoleerd over Shadow DOM-grenzen) en nonce mag niet leeg zijn.
  • Vroege inzending of hergebruikte pagina: De browser haalt de token op de achtergrond op nadat het e-mailadres is ingevoerd of automatisch is ingevuld. Als je het verzoek indient voordat het is afgerond, blijft de token leeg. Dit kan gebeuren als de gebruiker op Enter drukt om het formulier in te dienen nadat die het e-mailadres heeft ingevoerd.
  • Vereisten voor browser en provider: De gebruiker moet in hetzelfde browserprofiel zijn ingelogd bij een deelnemende provider en Geverifieerd e-mailadres aan hebben staan in de Chrome-instellingen (chrome://settings/contactInfo).

Verificatie van de handtekening van de uitgever mislukt

  • Ontbrekende kid-header: De claim kid (sleutel-ID) in de EVT-header en JWKS is optioneel (Gmail laat kid bijvoorbeeld weg). Als kid ontbreekt, itereer je door alle kandidaatsleutels in de jwks_uri van de uitgever in plaats van te mislukken bij een zoekopdracht naar een sleutel-ID.
  • Algoritme-ID's (EdDSA en Ed25519): Uitgevers en bibliotheken kunnen EdDSA of Ed25519 (naast ES256) specificeren. Zorg dat uw logica voor JWK-import en -verificatie beide ID's accepteert.
  • Oorsprongsindeling van uitgever (iss): De DNS TXT-record (_email-verification.<domain>) bevat een kale hostnaam (iss=accounts.issuer.example), terwijl de EVT-claim iss een volledige HTTPS-oorsprong is (https://accounts.issuer.example, zonder afsluitende slash). Voeg het voorvoegsel https:// toe aan de waarde van de DNS-record voordat je deze vergelijkt.

Validatie van sleutelbinding (KB-JWT) mislukt

  • Niet-overeenkomende of verlopen nonce: Zorg dat de nonce die wordt gerenderd in de <input> overeenkomt met de actieve sessie-nonce op je server en niet is overschreven door een ander tabblad of is gebruikt door een eerder verzoek.
  • Niet-overeenkomende doelgroep (aud): De claim aud is de HTTPS-oorsprong van de verifieerder (https://verifier.example, zonder pad of slash aan het einde).

Vergelijking van e-mailclaims (email) mislukt

  • Hoofdletters en kleine letters en normalisatie: Chrome 156+ retourneert de claim email byte voor byte zoals ingevoerd in het formulier, maar eerdere browserversies of providers kunnen een genormaliseerd adres retourneren (bijvoorbeeld First.Last@example.com voor first.last@example.com). Gebruik een vergelijking die niet hoofdlettergevoelig is als je de claim email van het token vergelijkt met de ingediende formulierwaarde.