メール プロバイダ(発行者)の実装

詳細については、メール プロバイダのモックのデモコードを確認し、メール検証 API と メール検証プロトコルの提案で発行者の手順を参照してください。

発行者は、オリジン トライアルに登録したり、トークンを提供したりする必要はありません。依拠当事者サイトがブラウザの動作をトリガーするためです。エンドポイントがこれらのリクエストに応答するように構成されていることを確認します。

発行者の検出を構成する

ドメインに属するメールアドレスが選択されたときに、ブラウザが検証エンドポイントを自動的に検出できるようにするには、DNS と .well-known HTTP エンドポイントを使用して構成を公開します。

DNS 委任レコードを構成する

メール ドメインに、検証権限を発行者 ID に委任する DNS TXT レコードを構成します。これらの識別子は、インフラストラクチャに応じて同じドメインを使用できます。

レコード形式: _email-verification.<email-domain>

ゾーンファイルの例:

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

.well-known/email-verification エンドポイントをホストする

発行者のドメインの /.well-known/ パスに JSON メタデータ ファイルをホストします。このファイルには、発行機能と、インフラストラクチャがサポートする暗号署名アルゴリズムが記載されています。

エンドポイント: https://<issuer-domain>/.well-known/email-verification

レスポンスの例:

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

.well-known/web-identity エンドポイントをホストする

Federated Credentials(FedCM)API の一部として、追加の .well-known JSON リソースをすでに実装している可能性があります。このリソースは、アカウント エンドポイントとログイン URL へのリンクを提供します。

エンドポイント: https://<domain>/.well-known/web-identity

レスポンスの例:

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

アカウント エンドポイントを使用する

FedCM API のアカウント エンドポイントは、現時点でログインしているアカウントのリストを提供します。次の例は、最小限のレスポンスを示しています。詳しくは、ID プロバイダの実装ガイドをご覧ください。

エンドポイント: .well-known/web-identity で指定されているとおり

レスポンスの例を次に示します。

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

ログイン ステータス API と統合する

ユーザーはプロバイダとのアクティブなセッションを保持している必要があり、ログイン ステータス API を使用してブラウザにそのことを通知する必要があります。

ユーザーがログインまたはログアウトに成功したら、一致する HTTP レスポンス ヘッダーを配信します。

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

または、ウェブ アプリケーション コンテキストで JavaScript を使用してステータスを更新します。

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

発行リクエストを処理する

issuance_endpoint は、email キーと Signature、Signature-Input、Signature-Key の HTTP メッセージ署名ヘッダーを含む application/json POST リクエストを受信します。

環境で構造化ヘッダーと HTTP メッセージ署名をサポートするライブラリを使用します。Node.js では、structured-headers と http-message-sig を使用できます。

完全なリクエスト形式:

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

リクエストを解析して検証します。

  • セッション認証: リクエストとともに送信されたファーストパーティ セッション Cookie を検証します。ユーザーは認証されている必要があります。
  • Sec-Fetch-Dest ヘッダー: email-verification に設定します。
  • HTTP メッセージ署名: Signature-Key のエフェメラル公開鍵を使用してリクエスト署名を検証し、Content-Digest を検証します。
  • ペイロード: JSON 本文には、検証用にリクエストされた email 文字列が含まれています。

発行レスポンス

セッション トークンとリクエスト トークンの検証に成功したら、プラットフォームに適したライブラリを使用して、JSON として返される署名付きの Selective Disclosure JWT(SD-JWT)を生成します。たとえば、Node の場合は @sd-jwt/core と jose を使用できます。

未加工のペイロードの形式は次のようになります。

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

トークンを作成、署名して返します。

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);

レスポンス本文は次のようになります。

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

メール確認トークンを作成して署名すると、ブラウザはこのトークンを検証サイトに渡して検証します。

トラブルシューティング

ブラウザがエンドポイントに接続しない場合や、発行されたトークンが拒否された場合は、次の一般的な問題を確認してください。

ブラウザが accounts_endpoint または issuance_endpoint を呼び出さない

  • ログイン ステータスが設定されていない: Chrome は、ユーザーがログインしていることがわかっている場合にのみ、エンドポイントをクエリします。ログインフローで Set-Login: logged-in HTTP ヘッダーが設定されているか、navigator.login.setStatus("logged-in") が呼び出されていることを確認します。
  • セッション Cookie がブロックされた(SameSite=None): ブラウザが別のサイトの証明書利用者から accounts_endpoint と issuance_endpoint を取得します。これらはクロスサイト リクエストであるため、セッション Cookie に SameSite=None; Secure を含める必要があります。SameSite=Lax Cookie は、同じサイトの検証ツールでテストする場合は機能しますが、クロスサイト リクエストでは省略されます。
  • 検出またはアカウントの不一致: _email-verification.<email-domain> が単一の TXT レコード(iss=<issuer-domain>、https:// なし)を返し、両方の .well-known エンドポイントが Content-Type: application/json を返し、accounts_endpoint レスポンスに入力されたアドレスと email が一致するアカウントが含まれていることを確認します。

発行リクエストの検証に失敗する

  • Sec-Fetch-Dest ヘッダーのスペル: Chrome 154 以降では Sec-Fetch-Dest: email-verification(ハイフンあり)が送信されますが、Chrome 153 では emailverification(ハイフンなし)が送信されていました。ロールアウト中に両方の値を受け入れます。
  • プロキシの背後にある HTTP メッセージ署名(@authority)の不一致: RFC 9421 署名を確認するときに、@authority コンポーネントが公開ホストを反映します。サーバーがリバース プロキシまたはロードバランサの背後にある場合は、内部ホスト名ではなく X-Forwarded-Host(または公開オリジン)を使用して確認用 URL を再構築し、JSON 解析の前に未加工のリクエスト本文のバイトに対して Content-Digest を計算します。

ブラウザが返された issuance_token を拒否する

  • 変更または正規化された email クレーム: Chrome 156 以降では、ブラウザは EVT の email クレームがリクエストされた email とバイト単位で一致することを確認します。バックエンドがアドレスを正規アカウント形式に正規化する場合(first.LAST@example.com がリクエストされたときに First.Last@example.com を返すなど)、Chrome はトークンを削除します。リクエストをユーザーのアカウントと照合しますが、リクエストの本文で受け取った email 文字列をそのまま返します。
  • 末尾のチルダ(~)がない: 開示が 1 つもない場合でも、issuance_token は末尾にチルダ(<Issuer-signed-JWT>~)が付いた有効な SD-JWT でなければなりません。そうすることで、ブラウザが <KB-JWT> を追加できるようになります。
  • iss または cnf.jwk の不一致: EVT の iss クレームが .well-known/email-verification メタデータと一致する正確な HTTPS オリジン(https://<issuer-domain>、末尾のスラッシュなし)であり、cnf.jwk が Signature-Key ヘッダーからブラウザの一時公開鍵を埋め込んでいることを確認します。