メインコンテンツまでスキップ
プレビュー版
PocketSign Link v2 は現在プレビュー版です。正式提供までに仕様が変更される可能性があります。

DPoP(Demonstrating Proof of Possession)

DPoP は、アクセストークンを単なる Bearer トークンとして扱うのではなく、クライアントが対応する秘密鍵を持っていることを証明しながら使うための仕組みです。 PocketSign Link v2 では、DPoP バインドされたアクセストークンの発行と、そのトークンを使った token / userinfo / introspect の取り扱いに対応しています。

DPoP で解決したいこと

Bearer トークンは、トークン文字列そのものを持っていれば使えてしまいます。 DPoP では、トークン利用時に proof JWT を毎回添付し、その proof に対応する鍵を持っていることを示すことで、盗まれたトークンの転用リスクを下げます。

鍵ペアはどこで作るか

DPoP を使うときは、通常はユーザー端末上のアプリやクライアント実装の中で、DPoP proof 用の秘密鍵・公開鍵ペアを生成します。

  • 秘密鍵は、その端末・アプリの中で保持します
  • 秘密鍵は、OS や実行環境が提供するセキュアな領域に保存することを推奨します
  • 公開鍵は proof JWT の jwk ヘッダーや、必要に応じて dpop_jkt としてサーバーに伝わります
  • 秘密鍵自体を PocketSign Link v2 に送ることはありません

つまり DPoP は、このトークンを持っている だけではなく、このトークンに対応する鍵を今も持っている ことまで確認する仕組みです。

なぜ盗用リスクが下がるのか

DPoP を使わない Bearer トークンでは、アクセストークン文字列を盗まれると、その文字列だけで API を呼べてしまいます。 一方 DPoP では、アクセストークンを使うたびに、同じ鍵ペアの秘密鍵で署名した proof JWT も必要です。

そのため、攻撃者がアクセストークン文字列だけを盗んでも、対応する秘密鍵を持っていなければ利用できません。

特に次の点が効いています。

  • proof が毎回の HTTP リクエストに結び付く
  • proof には HTTP メソッドや URI が入る
  • リソースアクセス時は ath でアクセストークン自体にも結び付く
  • サーバーは proof の公開鍵と、トークンに埋め込まれた cnf.jkt の一致を確認する

Bearer との違い

方式リクエスト例検証の考え方
BearerAuthorization: Bearer <access_token>トークンそのものの署名や有効期限を検証
DPoPAuthorization: DPoP <access_token>DPoP: <proof_jwt>トークン検証に加えて、proof と公開鍵の対応も検証
エンドポイントDPoP の扱い
POST /api/oidc/v1/parDPoP proof から jkt を取り出し、必要に応じて dpop_jkt を保存します
POST /api/oidc/v1/tokenDPoP proof を検証し、DPoP バインドされたトークンを発行できます
GET/POST /api/oidc/v1/userinfoDPoP バインドされたアクセストークンで呼び出せます
POST /api/oidc/v1/introspecttoken_type=DPoPcnf.jkt を返します
/.well-known/openid-configurationdpop_signing_alg_values_supported を返します

基本概念

token_type

DPoP バインドされたトークンが発行されると、トークンレスポンスや introspection では token_typeDPoP が返ります。 通常の Bearer トークンでは Bearer が返ります。

cnf.jkt

DPoP バインドされたアクセストークンには cnf.jkt が入ります。 これは DPoP proof に使う公開鍵の JWK Thumbprint です。

dpop_jkt

認可リクエストや PAR では、dpop_jkt を使って「この鍵にバインドされたトークンを発行してほしい」と事前に指定できます。 KLON は proof から得た jkt と、この dpop_jkt が一致することを確認します。

DPoP proof

DPoP proof は DPoP ヘッダーに入れる JWT です。 PocketSign Link v2 の現在の実装では、少なくとも次を満たす必要があります。

  • ヘッダー typdpop+jwt であること
  • 署名アルゴリズムが ES256 であること
  • ヘッダーに公開鍵 jwk を含むこと
  • htm が実際の HTTP メソッドと一致すること
  • htu が実際のリクエスト URI と一致すること
  • リソースアクセス時は ath がアクセストークンのハッシュと一致すること
  • jti を含むこと
  • iat / exp が妥当であること

proof のイメージ

proof JWT の実体は署名付き文字列ですが、概念的には次のような内容を含みます。

{
"htm": "POST",
"htu": "https://id.mock.klon.you/api/oidc/v1/token",
"jti": "8d4e8d88-0e8b-41ab-9801-d6c1f1e9f8b4",
"iat": 1710000000,
"exp": 1710000300
}

リソースアクセス時は、これに加えて ath が入ります。

{
"htm": "GET",
"htu": "https://id.mock.klon.you/api/oidc/v1/userinfo",
"ath": "BASE64URL(SHA256(access_token))",
"jti": "19f8d5a3-e5c6-4ef1-b1eb-0d42f3a82cb8",
"iat": 1710000600,
"exp": 1710000900
}

認可からトークン取得までの流れ

トークンエンドポイントでの使い方

DPoP バインドされたトークンを取得する場合、token エンドポイントには通常のフォームパラメータに加えて DPoP ヘッダーを付けます。

典型的な流れ

  1. クライアントが端末・アプリ上で DPoP 用の鍵ペアを生成する
  2. 公開鍵から jkt を計算する
  3. 必要なら PAR や認可リクエストで dpop_jkt を指定する
  4. token リクエストごとに、その秘密鍵で proof JWT を署名して送る
  5. PocketSign Link v2 が proof と dpop_jkt の整合性を確認し、token_type=DPoP のアクセストークンを返す

具体例

たとえば、認可コード交換時には次のような token リクエストになります。

curl -X POST "https://id.mock.klon.you/api/oidc/v1/token" \
-u "7f3b41e2-a81c-4e35-b08d-2c7e60a4d073:client-secret" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "DPoP: <proof_jwt>" \
-d "grant_type=authorization_code" \
-d "code=SplxlOBeZQQYbYS6WxSbIA" \
-d "redirect_uri=https://rp.example.com/callback" \
-d "code_verifier=random-verifier"

このとき DPoP: <proof_jwt> に入る proof は、https://id.mock.klon.you/api/oidc/v1/token 向けに、その端末・アプリ内の秘密鍵で署名して生成します。

レスポンスの考え方は次のとおりです。

{
"token_type": "DPoP",
"access_token": "<access_token>",
"expires_in": 1710003600,
"refresh_token": "<refresh_token>",
"id_token": "<id_token>"
}

ここで返る access_token 自体には cnf.jkt が入り、以後はその jkt に対応する秘密鍵で署名した proof を付けないと使えません。

主な挙動

  • クライアントが DPoP 必須設定の場合、proof がないと invalid_dpop_proof になります
  • 認可コードや PAR に dpop_jkt が紐づいている場合、proof の jkt と一致する必要があります
  • 発行されるアクセストークンには cnf.jkt が入り、token_type=DPoP が返ります

Userinfo エンドポイントでの使い方

DPoP バインドされたアクセストークンを使う場合は、Authorization: DPoPDPoP proof の両方が必要です。

具体例

curl "https://id.mock.klon.you/api/oidc/v1/userinfo" \
-H "Authorization: DPoP <access_token>" \
-H "DPoP: <proof_jwt>"

この proof_jwt は、token リクエストのときとは別物です。 userinfo 用に新しく生成し、少なくとも次を反映させます。

  • htm=GET
  • htu=https://id.mock.klon.you/api/oidc/v1/userinfo
  • ath=BASE64URL(SHA256(access_token))
  • 新しい jti
  • 現在時刻に対応する iat / exp

つまり、同じ鍵ペアを使う一方で、proof 自体はリクエストごとに毎回作り直します。

主な挙動

  • Authorization: Bearer ではなく Authorization: DPoP を使います
  • proof の ath がアクセストークンのハッシュと一致する必要があります
  • proof の jkt がアクセストークン内の cnf.jkt と一致する必要があります

introspect で見えること

DPoP バインドされたトークンを introspection すると、通常の active / exp / client_id に加えて、次が返ります。

フィールド意味
token_typeDPoP
cnf.jktバインドされた公開鍵の JWK Thumbprint

Discovery で確認できること

Discovery Document では、DPoP に関するサポート情報として dpop_signing_alg_values_supported を確認できます。 現在の実装では ES256 のみをサポートします。

現時点の制限事項

  • DPoP proof の署名アルゴリズムは ES256 のみ対応です
  • proof の jti は必須ですが、サーバー側のリプレイキャッシュは現状ありません
  • リフレッシュトークン自体を個別に DPoP で表現するというより、token family の DPoP バインドを継承して扱います
  • クライアント登録や運用手順そのものは、このガイドでは扱いません

RP 実装上のポイント

  • DPoP 用の鍵ペアは、ユーザー端末、アプリ、サービスバックエンドなどのクライアント実装上で生成し、安全に管理する
  • 秘密鍵は端末・アプリ外へ出さず、OS や実行環境が提供するセキュアな領域に保存する
  • proof はリクエストごとに生成する
  • token_type=DPoP が返ったら、以後のアクセストークン利用も Authorization: DPoP で統一する
  • userinfo や後続 API では ath を含む proof を必ず付ける
  • 互換性確認のために Discovery の dpop_signing_alg_values_supported を確認する

トラブルシュート

DPoP で失敗するときは、まず次の 5 つを確認してください。

  1. AuthorizationDPoP <access_token> になっているか
  2. DPoP ヘッダーに proof JWT を付けているか
  3. proof の htmhtu が実リクエストと一致しているか
  4. proof の鍵とトークン内の cnf.jkt が一致しているか
  5. proof を使い回していないか

関連ページ