Implementacja strony ufającej

Aby wdrożyć weryfikację adresu e-mail w swojej witrynie, zaktualizuj kod formularza, aby wysyłać żądania tokena, i dodaj weryfikację po stronie serwera dla przychodzących tokenów.

Rejestracja w testowaniu origin

Witryny, które mają zostać zweryfikowane, muszą mieć skonfigurowaną w swojej domenie wersję próbną origin.

Od wersji 154 Chrome obsługuje testy pochodzenia innych firm z ważnym zastrzeżeniem: zarejestrowane pochodzenie testu musi być w tej samej witrynie co wydawca. Na przykład:

  • Domena wystawcy: issuer.example
  • Rejestrujący OT: https://issuer.example
  • Źródło JavaScript: https://issuer.example (lub https://app.issuer.example z dopasowywaniem subdomen)

Konfigurowanie pól formularza

Dodaj do formularza przesyłania e-maili ukryte pole tokena:

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

Wymagania dotyczące pola:

  • Pole adresu e-mail: ustaw type="email" i autocomplete="email", aby Chrome mógł autouzupełniać i rozpoznawać adres.
  • Atrybuty pola tokena:
    • Ustaw autocomplete="email-verification-token": Chrome identyfikuje to pole, aby wypełnić token podczas przesyłania.
    • Ustawienie nonce="<VALUE>": witryna musi udostępniać niepowtarzalny, powiązany z sesją identyfikator nonce, aby zweryfikować przesłanie formularza.

Sprawdź token potwierdzania adresu e-mail (EVT).

Gdy użytkownik prześle formularz, Twój serwer otrzyma adres e-mail i token z ukrytego pola. Puste pole tokena oznacza, że przeglądarka lub dostawca nie obsługuje weryfikacji EVP albo użytkownik pominął weryfikację. W takim przypadku wróć do dotychczasowego procesu weryfikacji, np. wysyłania hasła jednorazowego lub magicznego linku.

Jeśli token jest obecny, sprawdź go w ten sposób:

  1. Przeanalizuj token za pomocą biblioteki SD-JWT.
  2. Sprawdź oczekiwane wartości i deklaracje sesji.
  3. Sprawdź delegowanie DNS.
  4. Odkrywanie metadanych wydawcy i pobieranie JWKS.
  5. Weryfikacja podpisów kryptograficznych i powiązania kluczy.

1. Analizowanie tokena

Token jest zgodny z formatem RFC 9901: Selective Disclosure JWT (SD-JWT+KB). Użyj odpowiednich bibliotek dla swojej platformy, aby przeanalizować i zweryfikować token. Na przykład w przypadku Node możesz użyć @sd-jwt/core i jose. W formie surowej wygląda to tak: JWT podpisany przez wystawcę, po którym następuje zero lub więcej ujawnień, a na końcu JWT powiązania klucza. Poszczególne komponenty są rozdzielone tyldą:

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

W obecnej implementacji token nie zawiera żadnych informacji o ujawnieniu (<Issuer-signed EVT>~<Key Binding JWT>). Może się to jednak zmienić w przyszłości.

Zdekoduj token za pomocą biblioteki:

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;

Jeśli verifier.example zweryfikuje demo@provider.example, zdekodowany token będzie wyglądać mniej więcej tak:

{
  "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. Weryfikowanie oczekiwanych wartości i roszczeń dotyczących sesji

Sprawdź, czy podstawowe wartości w ładunku odpowiadają podanym i oczekiwanym wartościom:

  • email_verified: musi mieć wartość true.
  • email: musi być zgodny z adresem e-mail podanym w formularzu.
  • aud (odbiorcy): musi pasować do pochodzenia witryny.
  • nonce: musi być zgodny z wartością nonce podaną w formularzu.
  • iat (issued at) i exp (expiry): sprawdź, czy token jest w okresie ważności i nie wygasł.

3. Sprawdzanie delegowania DNS

Sprawdź _email-verification rekord DNS domeny adresu e-mail. Na przykład w przypadku demo@gmail.com wyślij zapytanie o rekord TXT _email-verification.gmail.com. W przypadku tego dostawcy zapytanie zwraca lokalizację dostawcy konta, czyli accounts.google.com.

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

Sprawdź, czy schemat wystawcy to https:// i czy https://<domain> pasuje do roszczenia iss w EVT.

4. Weryfikowanie podpisu EVT

Pobierz metadane wykrywania wydawcy z 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"]
}

Pobierz zestaw kluczy internetowych JSON z jwks_uri.

Użyj biblioteki SD-JWT, aby zweryfikować pakiet tokenów. Biblioteka koordynuje weryfikację:

  1. Weryfikacja podpisu wystawcy na dokumencie EVT na podstawie pobranych kluczy JWKS.
  2. Weryfikacja podpisu przeglądarki w KB-JWT za pomocą efemerycznego klucza publicznego w cnf.jwk.
  3. Weryfikacja powiązania klucza (aud, nonce i wartość skrótu sd_hash).

Przykładowa logika weryfikacji w 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;

Jeśli wszystkie kroki zostaną wykonane prawidłowo, adres e-mail zostanie zweryfikowany u dostawcy. Jeśli weryfikacja się nie powiedzie, wróć do wysyłania e-maila z potwierdzeniem do użytkownika za pomocą normalnego przepływu.

Rozwiązywanie problemów

Jeśli weryfikacja się nie powiedzie lub przeglądarka nie dostarczy tokena, sprawdź te typowe problemy:

Pole tokena jest puste podczas przesyłania

  • Rejestracja w programie testów origin: sprawdź, czy na stronie jest wyświetlany nagłówek Origin-Trial lub tag <meta>. W przypadku testów origin innych firm (Chrome 154 i nowsze) zarejestrowany origin testu musi być w tej samej witrynie co wydawca (https://<issuer-domain>). Konfigurację testu origin w witrynie możesz sprawdzić w Narzędziach deweloperskich w sekcji Aplikacja > Ramki > (wybierz odpowiednią ramkę) > Testy origin.
  • Znaczniki formularza: zarówno <input type="email" autocomplete="email">, jak i <input type="hidden" autocomplete="email-verification-token" nonce="..."> muszą znajdować się w tym samym elemencie <form> (nie mogą być odseparowane przez granice Shadow DOM), a nonce nie może być pusty.
  • Wcześniejsze przesłanie lub ponowne użycie strony: przeglądarka pobiera token w tle po wpisaniu lub automatycznym wypełnieniu adresu e-mail. Przesłanie przed zakończeniem żądania spowoduje, że token będzie pusty. Może się tak zdarzyć, jeśli użytkownik po wpisaniu adresu e-mail naciśnie klawisz Enter, aby przesłać formularz.
  • Wymagania dotyczące przeglądarki i dostawcy: użytkownik musi być zalogowany u dostawcy biorącego udział w programie w tym samym profilu przeglądarki i mieć włączoną zweryfikowaną pocztę e-mail w ustawieniach Chrome (chrome://settings/contactInfo).

Nie udało się zweryfikować podpisu wydawcy

  • Brak nagłówka kid: roszczenie kid (identyfikator klucza) w nagłówku EVT i JWKS jest opcjonalne (np. Gmail pomija kid). Jeśli kid nie występuje, przeiteruj wszystkie klucze kandydujące w jwks_uri wystawcy, zamiast kończyć działanie z powodu niepowodzenia wyszukiwania identyfikatora klucza.
  • Identyfikatory algorytmów (EdDSA i Ed25519): wystawcy i biblioteki mogą określać EdDSA lub Ed25519 (wraz z ES256). Upewnij się, że logika importowania i weryfikacji JWK akceptuje oba identyfikatory.
  • Format pochodzenia wystawcy (iss): rekord TXT DNS (_email-verification.<domain>) zawiera samą nazwę hosta (iss=accounts.issuer.example), a deklaracja EVT iss to pełne pochodzenie HTTPS (https://accounts.issuer.example bez ukośnika na końcu). Przed porównaniem dodaj prefiks https:// do wartości rekordu DNS.

Nie udało się zweryfikować powiązania klucza (KB-JWT)

  • Niezgodny lub wygasły nonce: upewnij się, że wartość nonce wyrenderowana w <input> jest zgodna z nonce aktywnej sesji na serwerze i nie została zastąpiona przez inną kartę ani wykorzystana przez poprzednie żądanie.
  • Niezgodność odbiorców (aud): roszczenie aud to pochodzenie HTTPS weryfikatora (https://verifier.example, bez ścieżki ani ukośnika na końcu).

Porównanie deklaracji e-maila (email) nie powiodło się

  • Wielkość liter i kanonizacja: Chrome w wersji 156 lub nowszej zwraca deklarację email w postaci bajt po bajcie, tak jak została wpisana w formularzu, ale starsze wersje przeglądarki lub dostawcy mogą zwracać adres w postaci kanonicznej (np. First.Last@example.com zamiast first.last@example.com). Podczas dopasowywania deklaracji email tokena do wartości przesłanej w formularzu używaj porównania bez uwzględniania wielkości liter.