MS_OAuthDPoP - NetDevInfraWGinOSSConsortium/NetDevInfraWiki GitHub Wiki

OAuth2.0 DPoP

概要

記名式切符(トークン)作成に関する仕様。

  • TLS に依存せず、アプリケーション・レイヤで処理可能にした
    セキュリティ拡張仕様(RFC 9449 として標準化)。

  • フロントエンドから使用可能なプラットフォーム上の
    公開鍵・暗号化アルゴリズムを使用する。

  • DPoP = Demonstration of Proof-of-Possession at the Application Layer

  • 持参人切符から記名式切符へ切り替えることでよりセキュアにする
    OAuth 2.0 のトークンを参照)。

  • 方法としては、OAuth 2.0 のアクセストークンを
    特定のクライアント(秘密鍵)に紐づける

  • このため、Token リクエストと Resource リクエストにおいて、
    PoP Proof JWT を使用する。

  • Token Bindingのやりたかったことを
    TLS に依存せず実現した後継の位置付け。

特徴

  • SPA などの Public Clients 全般をターゲットとして仕様が作成されている。
  • 名前の通り、アプリケーション・レイヤで、記名式切符を発行することができる。
  • 「アプリケーション・レイヤで」とは mTLS と異なり
    インフラ・レイヤを必要としないの意味。

※ mTLS も Token Bindingも TLS に依存した設計となっている。
※ ただし、後者はブラウザ・サポートによりインフラ構築不要になる予定だったが、
実現せずに、廃案と成った。

補足(3 方式の比較): 記名式トークンの実現手段は 3 つあったが、
現在残っているのは 2 つである。

mTLS(RFC 8705) Token Binding DPoP(RFC 9449)
依存する層 TLS + PKI TLS アプリ層のみ
必要なもの クライアント証明書 ブラウザの実装 無し
ブラウザから使えるか 実質不可 予定だったが頓挫 可能
状態 現役 廃止 現役(推奨)
向く場面 サーバー間・金融基幹 SPA / モバイル / CLI

「TLS に依存しない」という一点が DPoP の存在意義であり、
Token Bindingが QUIC との相性で頓挫したことへの
直接的な答えになっている。

課題

秘密鍵を何処に保存すりゃいいのか?などの説明が不足している
(明示的な規定を避けている)が、一般的な回答としては、
エクスポート不可の Web Crypto API(extractable: false)で
インメモリ生成する
のが推奨。

補足(この「課題」が核心): DPoP の安全性は
**「秘密鍵が盗まれないこと」**に全面的に依存する。
ところが SPA では XSS があれば JavaScript が何でもできてしまう。

保管場所 XSS で盗めるか
localStorage に鍵の文字列 盗める(意味が無い)
CryptoKeyextractable: false 鍵自体は取り出せない
同上 + IndexedDB に永続化 取り出せないが、署名の実行は可能

重要なのは、extractable: false でも
「鍵を使って署名させる」ことは XSS から可能という点である。
つまり DPoP は

  • トークンを持ち出して別の場所で使う攻撃 → 防げる
  • その端末・そのページ内で不正に使う攻撃 → 防げない

XSS 対策の代替にはならない、という理解が必要である。

詳細

クライアントが公開鍵・秘密鍵ペアを生成

  • PrivateKey(秘密・保持)
  • PublicKey(DPoP Proof JWT に埋め込む)
プラットフォーム API
ブラウザ crypto.subtle(Web Crypto API、extractable: false
Node.js webcrypto.subtle
iOS / Android ネイティブ Secure Enclave / Android Keystore

Tokenリクエスト時に DPoP Proof JWT を送信

POST /token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2Iiwiandr...

grant_type=authorization_code&code=xxxxx&...

DPoP Proof JWT の構造。

// Header
{
  "typ": "dpop+jwt",
  "alg": "ES256",
  "jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." }
}
// Payload
{
  "htm": "POST",                             // HTTPメソッド
  "htu": "https://server.example.com/token", // エンドポイントURL
  "iat": 1623420800,                         // 発行時刻(リプレイ攻撃対策)
  "jti": "unique-id"                         // 一意なID(リプレイ攻撃対策)
}
// Signature (秘密鍵で署名)

補足(htm / htu が肝): Proof JWT に
HTTP メソッドと URL が含まれ、署名されている点が重要である。

これにより、Proof を横取りしても
別のエンドポイントに転用できないhtu が一致しない)。
iatjti でリプレイも防ぐ。

つまり Proof は「このリクエストのために作られた使い捨ての証明」であり、
リクエストごとに新しく生成する必要がある。

Authorization Server がトークンを発行

  • DPoP Proof JWT の署名を検証
    • htmhtu の一致確認で当該処理を目的としているか確認。
    • jtiiat を用いてリプレイ攻撃チェックを行う。
  • Access トークンの発行
    • 公開鍵のハッシュ(cnf.jkt)を Access トークンに埋め込む
// アクセストークン(JWT)のペイロードに含まれる
{
  "sub": "user123",
  "cnf": {
    "jkt": "0ZcO...(公開鍵のSHA-256ハッシュ)"
  }
}

Resource Serverへのリクエスト

毎回、新しい DPoP Proof JWT を生成する。

GET /resource HTTP/1.1
Authorization: DPoP eyJhbGciOi...   ← Bearer ではなく DPoP スキーム
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIs...  ← Proof JWT

Resource Serverが検証

  • Access トークンの cnf.jkt(公開鍵ハッシュ)を取得
  • DPoP Proof の署名を検証
    • htmhtu の一致確認
    • jtiiat を用いてリプレイ攻撃チェック
    • DPoP Proof JWT の jwk のハッシュと cnf.jkt との一致確認
      (トークンと鍵の紐づけ確認)

補足(Authorization: DPoP スキームの意味): Bearer ではなく
DPoP というスキーム名を使う点も設計上重要である。

  • Resource Server はスキーム名を見た時点で「Proof の検証が必要」と判断できる。
  • 誤って Bearer トークンとして扱う(=検証を飛ばす)事故を防げる。

逆に言えば、Resource Server が DPoP に対応していないと意味が無い
Authorization Server だけ対応しても効果が出ない点に注意が要る。

移行メモ(最新化:RFC として標準化された): 元ページは
draft-fett-oauth-dpop-01 を参照しているが、
2023年9月に RFC 9449 として標準化された。

内容
RFC 9449 OAuth 2.0 Demonstrating Proof of Possession (DPoP)
FAPI 2.0 mTLS と DPoP の両方を Sender-Constrained の手段として認める
実装 Keycloak / Okta / Auth0 / Entra ID などが対応を進めている

名称も「Demonstration of Proof-of-Possession at the Application Layer」から
「Demonstrating Proof of Possession」 に整理されている。

参考

関連


Tags: 移行, IT国際標準, 認証基盤, クレームベース認証, OAuth

⚠️ **GitHub.com Fallback** ⚠️