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 との違い
| 方式 | リクエスト例 | 検証の考え方 |
|---|---|---|
Bearer | Authorization: Bearer <access_token> | トークンそのものの署名や有効期限を検証 |
DPoP | Authorization: DPoP <access_token> と DPoP: <proof_jwt> | トークン検証に加えて、proof と公開鍵の対応も検証 |
PocketSign Link v2 での対応範囲
| エンドポイント | DPoP の扱い |
|---|---|
POST /api/oidc/v1/par | DPoP proof から jkt を取り出し、必要に応じて dpop_jkt を保存します |
POST /api/oidc/v1/token | DPoP proof を検証し、DPoP バインドされたトークンを発行できます |
GET/POST /api/oidc/v1/userinfo | DPoP バインドされたアクセストークンで呼び出せます |
POST /api/oidc/v1/introspect | token_type=DPoP や cnf.jkt を返します |
/.well-known/openid-configuration | dpop_signing_alg_values_supported を返します |
基本概念
token_type
DPoP バインドされたトークンが発行されると、トークンレスポンスや introspection では token_type に DPoP が返ります。
通常の 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 の現在の実装では、少なくとも次を満たす必要があります。
- ヘッダー
typがdpop+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 ヘッダーを付けます。
典型的な流れ
- クライアントが端末・アプリ上で DPoP 用の鍵ペアを生成する
- 公開鍵から
jktを計算する - 必要なら PAR や認可リクエストで
dpop_jktを指定する tokenリクエストごとに、その秘密鍵で proof JWT を署名して送る- 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: DPoP と DPoP 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=GEThtu=https://id.mock.klon.you/api/oidc/v1/userinfoath=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_type | DPoP |
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 つを確認してください。
AuthorizationがDPoP <access_token>になっているかDPoPヘッダーに proof JWT を付けているか- proof の
htmとhtuが実リクエストと一致しているか - proof の鍵とトークン内の
cnf.jktが一致しているか - proof を使い回していないか
関連ページ
- セキュリティ全般は OIDC / OAuth2 セキュリティ
- トークンの発行とレスポンスは トークン
- トークンの継承関係は トークンの仕様とライフサイクル
- UserInfo と introspection の基本は UserInfo とトークン照会