이메일 제공업체 (발급자) 구현

자세한 내용은 모의 이메일 제공업체 데모 코드를 살펴보고 이메일 인증 API 및 이메일 인증 프로토콜 제안서의 발급자 단계를 참고하세요.

발급자는 원본 트라이얼에 가입하거나 토큰을 제공할 필요가 없습니다. 신뢰 당사자 사이트에서 브라우저 동작을 트리거하기 때문입니다. 엔드포인트가 이러한 요청에 응답하도록 구성되어 있는지 확인합니다.

발급기관 검색 구성

도메인에 속한 이메일 주소가 선택될 때 브라우저가 인증 엔드포인트를 자동으로 검색하도록 허용하려면 DNS와 .well-known HTTP 엔드포인트를 사용하여 구성을 노출하세요.

DNS 위임 레코드 구성

인증 권한을 발급자 식별자에 위임하는 이메일 도메인에 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"}

요청을 파싱하고 검증합니다.

  • 세션 인증: 요청과 함께 전송된 퍼스트 파티 세션 쿠키를 검증합니다. 사용자가 인증되어야 합니다.
  • Sec-Fetch-Dest 헤더: email-verification로 설정합니다.
  • HTTP 메시지 서명: Signature-Key의 임시 공개 키를 사용하여 요청 서명을 확인하고 Content-Digest을 검증합니다.
  • 페이로드: JSON 본문에는 인증을 위해 요청된 email 문자열이 포함됩니다.

발급 응답

세션 및 요청 토큰의 유효성 검사가 완료되면 플랫폼에 적합한 라이브러리를 사용하여 JSON으로 반환되는 서명된 선택적 공개 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")를 호출해야 합니다.
  • 세션 쿠키 차단 (SameSite=None): 브라우저가 다른 사이트의 신뢰 당사자로부터 accounts_endpoint 및 issuance_endpoint를 가져옵니다. 이는 크로스 사이트 요청이므로 세션 쿠키에 SameSite=None; Secure가 포함되어야 합니다. SameSite=Lax 쿠키는 동일한 사이트 확인 도구에서 테스트할 때는 작동하지만 크로스 사이트 요청에서는 생략됩니다.
  • 검색 또는 계정 불일치: _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 문자열을 반환합니다.
  • 후행 물결표 (~) 누락: 공개 정보가 0개인 경우에도 브라우저가 <KB-JWT>를 추가할 수 있도록 issuance_token는 후행 물결표(<Issuer-signed-JWT>~)로 끝나는 유효한 SD-JWT여야 합니다.
  • iss 또는 cnf.jwk 불일치: EVT iss 클레임이 .well-known/email-verification 메타데이터와 일치하는 정확한 HTTPS 출처 (https://<issuer-domain>, 후행 슬래시 없음)이고 cnf.jwk가 Signature-Key 헤더에서 브라우저의 임시 공개 키를 삽입하는지 확인합니다.