Implementacja dostawcy poczty e-mail (wydawcy)

Więcej informacji znajdziesz w przykładzie kodu demonstracyjnego dostawcy poczty e-mail oraz w propozycjach interfejsu Email Verification API i protokołu Email Verification.

Jako wydawca nie musisz rejestrować się w programie testów pochodzenia ani podawać tokena, ponieważ zachowanie przeglądarki jest wywoływane przez witrynę strony ufającej. Upewnij się, że punkty końcowe są skonfigurowane tak, aby odpowiadać na te żądania.

Konfigurowanie wykrywania wystawcy

Aby umożliwić przeglądarkom automatyczne wykrywanie punktów końcowych weryfikacji po wybraniu adresu e-mail należącego do Twojej domeny, udostępnij konfigurację za pomocą DNS i punktu końcowego HTTP .well-known.

Konfigurowanie rekordu delegowania DNS

Skonfiguruj rekord DNS typu TXT w domenie e-mail, który przekazuje uprawnienia do weryfikacji identyfikatorowi wystawcy. W zależności od infrastruktury identyfikatory te mogą używać tej samej domeny.

Format rekordu: _email-verification.<email-domain>

Przykładowy plik strefy:

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

Hostowanie punktu końcowego .well-known/email-verification

Umieść plik JSON z metadanymi w domenie wydawcy w ścieżce /.well-known/. Ten plik zawiera informacje o możliwościach wydawania i algorytmach podpisywania kryptograficznego obsługiwanych przez Twoją infrastrukturę.

Punkt końcowy: https://<issuer-domain>/.well-known/email-verification

Przykładowa odpowiedź:

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

Hostowanie punktu końcowego .well-known/web-identity

Być może masz już wdrożony dodatkowy.well-known zasób JSON jako część interfejsu Federated Credentials (FedCM) API. Ten zasób zawiera linki do punktu końcowego kont i adresu URL logowania.

Punkt końcowy: https://<domain>/.well-known/web-identity

Przykładowa odpowiedź:

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

Używanie punktu końcowego kont

Punkt końcowy kont z interfejsu FedCM API udostępnia listę zalogowanych kont w danym momencie. Poniższy przykład pokazuje minimalną odpowiedź. Więcej informacji znajdziesz w przewodniku po implementacji dostawcy tożsamości.

Punkt końcowy: zgodnie z informacjami w .well-known/web-identity

Oto przykładowa odpowiedź:

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

Integracja z interfejsem Login Status API

Użytkownik musi mieć aktywną sesję u dostawcy, a Ty musisz poinformować o tym przeglądarkę za pomocą interfejsu Login Status API.

Gdy użytkownik zaloguje się lub wyloguje, wyświetl odpowiedni nagłówek odpowiedzi HTTP:

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

Możesz też zaktualizować stan za pomocą JavaScriptu w kontekście aplikacji internetowej:

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

Obsługa próśb o wydanie

Twój issuance_endpoint otrzymuje żądanie application/json POST, które zawiera klucz email i nagłówki HTTP Message Signatures dla Signature, Signature-Input i Signature-Key.

Użyj biblioteki, która obsługuje nagłówki strukturalne i podpisy wiadomości HTTP w Twoim środowisku. W Node.js możesz używać structured-headers i http-message-sig.

Format pełnego żądania:

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

Przeanalizuj i zweryfikuj żądanie:

  • Uwierzytelnianie sesji: sprawdzanie poprawności własnych plików cookie sesji wysyłanych z żądaniem. Użytkownik musi być uwierzytelniony.
  • Nagłówek Sec-Fetch-Dest: ustaw na email-verification.
  • Podpisy wiadomości HTTP: zweryfikuj podpis żądania za pomocą efemerycznego klucza publicznego w Signature-Key i sprawdź Content-Digest.
  • Ładunek: treść JSON zawiera ciąg email wymagany do weryfikacji.

Odpowiedź dotycząca wydania

Po pomyślnej weryfikacji sesji i tokena żądania wygeneruj podpisany token JWT z wybiórczym ujawnianiem (SD-JWT), który zostanie zwrócony w formacie JSON przy użyciu odpowiednich bibliotek dla Twojej platformy. Na przykład w przypadku Node możesz użyć @sd-jwt/core i jose.

Format nieprzetworzonego ładunku powinien wyglądać podobnie do tego:

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

Utwórz, podpisz i zwróć 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);

Wynikowa treść odpowiedzi wygląda mniej więcej tak:

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

Po utworzeniu i podpisaniu tokena potwierdzania adresu e-mail przeglądarka przekazuje go do witryny weryfikującej w celu sprawdzenia.

Rozwiązywanie problemów

Jeśli przeglądarka nie kontaktuje się z punktami końcowymi lub odrzuca wydane tokeny, sprawdź, czy nie występują te typowe problemy:

Przeglądarka nigdy nie wywołuje funkcji accounts_endpoint ani issuance_endpoint

  • Stan logowania nie jest ustawiony: Chrome wysyła zapytania do punktów końcowych tylko wtedy, gdy wie, że użytkownik jest zalogowany. Sprawdź, czy proces logowania ustawia nagłówek HTTP Set-Login: logged-in lub wywołuje funkcję navigator.login.setStatus("logged-in").
  • Pliki cookie sesji zablokowane (SameSite=None): przeglądarka pobiera Twoje accounts_endpoint i issuance_endpoint od podmiotu polegającego na tożsamości na innej stronie. Ponieważ są to żądania z innych witryn, plik cookie sesji musi zawierać SameSite=None; Secure. Plik cookie SameSite=Lax działa podczas testowania w weryfikatorze tej samej witryny, ale jest pomijany w przypadku żądań z innych witryn.
  • Wykrywanie lub niezgodność konta: sprawdź, czy _email-verification.<email-domain> zwraca pojedynczy rekord TXT (iss=<issuer-domain>, bez https://), oba punkty końcowe .well-known zwracają Content-Type: application/json, a odpowiedź accounts_endpoint zawiera konto, którego email pasuje do wpisanego adresu.

Nie udało się zweryfikować prośby o wydanie

  • Pisownia nagłówka Sec-Fetch-Dest: Chrome 154 i nowsze wysyłają Sec-Fetch-Dest: email-verification (z myślnikiem), a Chrome 153 wysyłał emailverification (bez myślnika). Podczas wdrażania akceptuj obie wartości.
  • Niezgodność podpisu wiadomości HTTP (@authority) za serwerem proxy: podczas weryfikacji podpisu RFC 9421 komponent @authority odzwierciedla hosta publicznego. Jeśli serwer znajduje się za odwrotnym serwerem proxy lub systemem równoważenia obciążenia, zrekonstruuj URL do weryfikacji, używając X-Forwarded-Host (lub publicznego źródła), a nie wewnętrznej nazwy hosta, i oblicz Content-Digest na podstawie surowych bajtów treści żądania przed analizą JSON.

Przeglądarka odrzuca zwrócony element issuance_token

  • Zmodyfikowane lub znormalizowane roszczenie email: od wersji Chrome 156 przeglądarka sprawdza, czy roszczenie EVT email jest identyczne z żądanym roszczeniem email. Jeśli backend normalizuje adres do kanonicznego formatu konta (np. zwraca First.Last@example.com, gdy zażądano first.LAST@example.com), Chrome usuwa token. Dopasuj żądanie do konta użytkownika, ale zwróć dokładny ciąg znaków email otrzymany w treści żądania.
  • Brak tyldy na końcu (~): nawet w przypadku braku ujawnień issuance_token musi być prawidłowym tokenem SD-JWT zakończonym tyldą (<Issuer-signed-JWT>~), aby przeglądarka mogła dołączyć <KB-JWT>.
  • Niezgodność iss lub cnf.jwk: upewnij się, że roszczenie EVT iss jest dokładnym źródłem HTTPS (https://<issuer-domain>, bez ukośnika na końcu) zgodnym z metadanymi .well-known/email-verification, a cnf.jwk zawiera efemeryczny klucz publiczny przeglądarki z nagłówka Signature-Key.