dpop-adoption について
Google OAuth 2.0 で DPoP を導入する開発者向けのセキュリティ実装ガイドです。鍵の扱い、Proof JWT のクレーム、Nonce 再試行、SPA の BFF 構成に加え、WebCrypto の署名形式を規定します。
主な機能
- 01P-256 鍵の生成と JWK 出力
- 02DPoP Proof JWT のクレーム規則
- 03WebCrypto の署名形式制約
- 04Nonce チャレンジの単回再試行
- 05SPA 向け BFF 構成の指針
活用シーン
- 01OAuth トークン更新への DPoP バインド追加
- 02認可コード交換用 DPoP 証明の実装
- 03WebCrypto の署名形式エラーの調査
MCP 対応
Google の開発者ドキュメント検索に Google Developer Knowledge MCP Server を利用できると記載されています。このスキル自体が提供する MCP インターフェースについては説明されていません。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ google/ skills --skill dpop-adoptionインストール後、Claude Code で/dpop-adoptionを入力して読み込まれたか確認します。
詳細
DPoP の導入と ID セキュリティアーキテクチャ
Proof-of-Possession(DPoP、RFC 9449)は、OAuth 2.0 のリフレッシュトークンを、クライアントだけが保持する秘密鍵に暗号学的に結び付けることで、傍受やリプレイ攻撃から保護します。Google の OAuth 2.0 プラットフォームでは、DPoP はトークンエンドポイントでリフレッシュトークンを結び付けます。一方、Google API 用に発行されるアクセストークンは、標準の Bearer トークンです(token_type: "Bearer")。
1. 暗号およびアーキテクチャにおける基本不変条件
DPoP ヘルパーを実装したり、HTTP クライアントをアップグレードしたりする際は、以下の厳格なセキュリティ不変条件を必ず守ってください。
A. WebCrypto とランタイムの普遍的な互換性
- 最新の ES6 JavaScript(Node 18 以降およびブラウザーでは
"type": "module")では、環境コンテキストを確認した後、必ずglobalThis.cryptoに直接アクセスしてください。 require('node:crypto')によるレガシー CommonJS モジュールのインポートや、ブラウザーのスコープに属するwindow.cryptoの参照は絶対に行わないでください。 これらはハイブリッドランタイムでモジュールの初期化時にクラッシュを引き起こします。
B. ハードウェアで保護された抽出不可能な鍵の永続化
- SECP256R1(
P-256)曲線上で、次の楕円曲線鍵ペアを生成してください:{ name: 'ECDSA', namedCurve: 'P-256' }。 - 重大なセキュリティ上の保護策: 秘密鍵は抽出不可能(
extractable: false)に設定する必要があります。これにより、秘密鍵がハードウェア暗号境界(Secure Enclave、Android KeyStore、または JS サンドボックスのメモリ)から持ち出されることはなく、XSS や依存関係を介したトークン窃取攻撃を防ぎます。 - JWK(JSON Web Key)の出力に使えるよう、公開鍵は抽出可能(
extractable: true)にしておく必要があります。
C. 公開 JWK の書式規格
- 公開鍵を DPoP Proof JWT ヘッダーに含めるためにエクスポートする際は、以下の要素だけを含む簡潔な JWK 辞書を構成してください。
"kty": "EC""crv": "P-256""x":末尾の等号パディング(=)を除いた、Base64URL でエンコードされた x 座標。"y":末尾の等号パディング(=)を除いた、Base64URL でエンコードされた y 座標。
- 秘密鍵のパラメーター(
"d")や不要なメタデータを公開してはいけません。
D. IEEE P1363 と ASN.1 DER 署名の判別
- DPoP Proof JWT では、IEEE P1363 および RFC 7518 に従い、連結された座標の生署名($R \parallel S$、P-256 では正確に 64 バイト)が必要です。
- WebCrypto のネイティブな動作: 標準の WebCrypto(
crypto.subtle.sign)では、ECDSA 署名はネイティブに IEEE P1363 の生形式(32 バイトのrバッファーとsバッファーを連結した合計 64 バイト)で出力されます。crypto.subtle.signの出力に対して DER から生形式への変換を試みてはいけません。64 バイトの生バッファーを ASN.1 DER として解析すると、実行時に即座に例外(Invalid DER sequence)が発生します。生の ArrayBuffer をそのまま Base64URL でエンコードしてください。 - レガシー API のフォールバック: レガシー Java/Android(
java.security.Signature)または Node CommonJS(crypto.createSign)で実装する場合に限り、Base64URL でエンコードする前に ASN.1 DER 出力を IEEE P1363 の生形式(64 バイト)に変換してください。
E. SPA と Backend-for-Frontend(BFF)アーキテクチャ
- シークレットレス SPA の制約: バックエンドを持たない純粋なクライアント側のシングルページアプリケーション(SPA)は、サーバーエンドポイントで
client_secretが必要であること、およびブラウザーの CORS におけるDPoP-Nonceレスポンスヘッダーの制約により、Google API で DPoP を直接使用できません。 - BFF パターン: DPoP で SPA を保護するには、認可リクエストとトークンのリフレッシュリクエストを Backend-for-Frontend(BFF)のサーバー側クライアント経由にします。BFF は
access_type=offlineを設定し、サーバー側で DPoP を使用してリフレッシュトークンを結び付け、フロントエンドとは安全なセッションクッキーを維持します。
2. 実装ルールと必須のパブリック API
新しいモジュールを作成する場合は、CI/CD 検証ハーネスや自動プローバーと適切に連携できるよう、以下のすべての関数をモジュールから明示的にエクスポートする必要があります。既存のコードベースを調査またはリファクタリングする場合は、同等の暗号処理ロジックと RFC 9449 のロジックが存在することを確認してください。すべての場合において、厳密なクレーム導出ロジックに従ってください。
A. DPoP Proof JWT クレーム導出ルール(createDPoPProof)
createDPoPProof で DPoP Proof JWT を生成する場合:
1. JOSE ヘッダー(typ、alg、jwk):
// Header
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": await exportPublicJWK(publicKey)
}
2. ペイロードクレーム:
"htm":大文字の HTTP メソッド(トークンリクエストでは"POST")。"htu":sanitizeHTU(htu)を使ってクエリパラメーターとハッシュフラグメントを取り除いたターゲット URI。トークンリクエストの場合は、https://oauth2.googleapis.com/tokenです。"iat":現在の整数エポックタイムスタンプ(秒単位)(Math.floor(Date.now() / 1000))。"jti"(重要な不変条件):createDPoPProofに明示的なjti引数が渡された場合は、他の値より優先してその文字列をそのまま使用します。- それ以外の場合、
authCode引数が渡された場合(初回のコード交換時)は、jti = await calculateAuthCodeJti(authCode)を設定します。calculateAuthCodeJtiはbase64url(sha256(authCode))を計算し、DPoP proof が認可コードに暗号学的に紐付けられるようにします。 jtiもauthCodeも渡されていない場合に限り、generateRandomString()を使って新しい暗号学的乱数文字列を生成します(例:crypto.getRandomValues(new Uint8Array(24))を base64url エンコードしたもの)。
"ath"(任意):RFC 9449 のリソースリクエスト用にaccessToken引数が渡された場合は、calculateATH(accessToken)を使ってbase64url(sha256(accessToken))を計算し、ペイロードに追加します(RFC 9449 第 6.1 節)。"nonce"(任意):nonce引数が渡された場合は、その値をペイロードに直接追加します。
B. 明示的なエクスポートシグネチャ
// 1. Key generation & JWK export
export async function generateDPoPKeyPair() // -> { publicKey, privateKey } (private key extractable=false)
export async function exportPublicJWK(publicKey) // -> { kty: 'EC', crv: 'P-256', x, y }
// 2. Proof generation & validation
export async function createDPoPProof({ privateKey, publicKey, htm, htu, nonce, accessToken, authCode, jti }) // -> signed JWT string
export async function verifyDPoPProof(dpopProofJwt) // -> { isValid: boolean, header, payload, error }
export function sanitizeHTU(htu) // -> URL stripped of query and hash: const u = new URL(htu); return `${u.origin}${u.pathname}`;
// 3. Cryptographic & encoding utilities
export function base64UrlEncode(buffer) // -> Uint8Array/ArrayBuffer to base64url string without '=' padding
export function base64UrlDecode(str) // -> base64url string to Uint8Array/Buffer
export function stringToBase64Url(str) // -> UTF-8 string to base64url
export function base64UrlToString(str) // -> base64url to UTF-8 string
export function generateRandomString(byteLength = 32) // -> cryptographic random base64url string
export async function calculateATH(accessToken) // -> base64url(sha256(accessToken)) per RFC 9449 Sec 6.1
export async function calculateAuthCodeJti(code) // -> base64url(sha256(code))
export async function generatePKCE() // -> { codeVerifier (>=43 chars), codeChallenge, codeChallengeMethod: 'S256' }
3. トークンエンドポイントとリソースリクエストのワークフロー
Google の OAuth 2.0 プラットフォームと連携する場合:
- トークンエンドポイントへのリクエスト(
oauth2.googleapis.com/token):- コード交換(
grant_type=authorization_code)とトークンの更新(grant_type=refresh_token)のPOSTリクエストでは、DPoP Proof JWT をDPoPHTTP ヘッダーに設定します:`DPoP: ${proofJwt}`。
- コード交換(
- リソース API リクエスト:
- Google のトークンエンドポイントは
"token_type": "Bearer"を返します。Google API(例:Calendar、Drive、Gmail)への後続のリクエストでは、DPoP ヘッダーを使わず、標準の`Authorization: Bearer ${accessToken}`ヘッダーを使用します。
- Google のトークンエンドポイントは
- 1 回限りのリトライによる nonce チャレンジループとワークフローの分離:
- Google のトークンエンドポイントが HTTP
400 Bad Requestとerror: "use_dpop_nonce"、および"DPoP-Nonce"レスポンスヘッダーを返した場合:- ワークフローの分離: Google の認可サーバーは、認可コードの交換とトークンの更新をワークフローごとに分離し、新しい nonce 名前空間を確立するために HTTP
400 use_dpop_nonceチャレンジを返します。これは RFC に準拠した標準のプロトコル動作であり、サーバー障害ではありません。 - 新しい nonce をクライアントの状態(
this.dpopNonce)にキャッシュします。 - 更新された
nonceクレームと新しいjtiを含む DPoP Proof JWT を直ちに生成します。 - 失敗したトークンリクエストを1 回だけ再試行します。再試行したリクエストが失敗した場合は、無限再帰を防ぐため、直ちにエラーで終了します。
- ワークフローの分離: Google の認可サーバーは、認可コードの交換とトークンの更新をワークフローごとに分離し、新しい nonce 名前空間を確立するために HTTP
- Google のトークンエンドポイントが HTTP
4. 簡潔なエージェント送信プロトコル
このスキルの下でコード成果物の生成や出力を求められた場合は、冗長な会話形式の前置きや繰り返しの埋め草を避け、すぐにインポートできる簡潔なコードブロックを優先して返してください。概念やアーキテクチャに関する問い合わせには、通常どおり率直に回答してください。
5. 参考資料と補足ドキュメント
開発者向けドキュメント(Google for Developers)
- DPoP 導入ガイド — 認可コードの交換とトークンの更新における DPoP の実装について解説する、 Google Identity の公式ガイドです。
- ウェブサーバーアプリケーションでの OAuth 2.0 の使用 — オフラインアクセス、リフレッシュトークン、サーバー側の認可フローについて解説します。
- OAuth 2.0 のベストプラクティス:送信者制約付きトークン — トークンの保存、ローテーション、送信者制約に関する推奨事項です。
開発者向けナレッジ MCP サーバー
- Model Context Protocol(
MCP)を備えたエージェントは、 Google Developer Knowledge MCP Server (npx -y @google/mcp-developer-knowledge-server)を介して、developer_knowledge:search_documentsおよびdeveloper_knowledge:get_documentsを使用し、Google の開発者向けドキュメントを リアルタイムで検索できます。