🐹

【RFC 9449】OAuth のトークン利用者を制限する DPoP について

2024/04/09に公開

昨年9月にインターネット標準となった『RFC 9449 : OAuth 2.0 Demonstrating Proof of Possession (DPoP)』を読み、大体理解できましたのでその内容を共有します。

(社内ブログ向けに書いたものですが、もったいないので Zenn にも投稿します。)

DPoP 概要

RFC 9449 で標準化された DPoP は、OAuth 2.0 のフローを拡張する仕様であり、認可サーバから発行されたトークンが何らかの方法で盗まれてしまったとしてもその悪用を防ぐための仕様です。

もともと OAuth 2.0 のアクセストークンは Bearer トークンであり、トークンを所持していることによってリソースに対する権限の保持を示すものでした。しかしそれでは、悪意者がトークンを盗めば権限も悪用できてしまうという問題があります。そこで、クライアント固有の別の要素も加えることでトークンの悪用を防ぐための仕様として、DPoP が標準化されました。

より具体的には、クライアントがキーペアを所有し、その公開鍵とトークンの紐付けを認可サーバが保持することにより、公開鍵に対する秘密鍵を持つクライアントのみトークンを利用できるようにするというものです。RFC 9449 では、主にクライアントと認可サーバやリソースサーバとの間で送受信される HTTP リクエストおよびレスポンスについて定められています。

トークンを特定のクライアントのみが利用できるようにするという意味で、Sender Constrained トークンとも呼ばれます。

同様の仕様として RFC 8705 の Mutual-TLS もあります。Mutual-TLS では TLS のレイヤでクライアント証明書による認証も用い、クライアント証明書とトークンをバインドすることによって実現されます。一方、DPoP は Mutual-TLS を利用できない (あるいは利用が適切でない) ケースのための仕組みであり、HTTP のレイヤで公開鍵とトークンをバインドするという点で異なります。多くのシステムにおいてクライアント証明書を導入することはハードルが高いため Mutual-TLS はなかなか適用しづらいですが、DPoP は比較的容易に低コストで導入できそうです。

DPoP プロトコルについて

DPoP のプロトコルについてもう少し詳しく説明します。

登場人物は認可サーバ、リソースサーバ、クライアントの3者であり、RFC 6749 の OAuth 2.0 の定義と同じです。また、DPoP の基本的なフローは RFC の "3. Cencept" に記載されている次図の通りです。

まずクライアントは、認可サーバのトークンエンドポイントにトークンリクエストを送信する際、"DPoP" ヘッダを追加します。DPoP ヘッダは JWS (JSON Web Signature) であり、それを生成するためにクライアントはキーペアを用意しておく必要があります。JWS のアルゴリズム部にはクライアントの公開鍵を JWK (JSON Web Key) として含めます。JWS のペイロード部は今まさに行おうとしている HTTP リクエストに関する情報 (URI、HTTP メソッド) に加え、リプレイ攻撃を防ぐためのランダムなIDやタイムスタンプを含めます。JWS の署名部は当然クライアントの秘密鍵を用いて行った署名です。

この DPoP ヘッダを受信した認可サーバは、トークンリクエストのパラメータの検証に加えて、JWS の署名およびペイロードも検証し、正しく検証できたらトークンを生成してクライアントに返します。その際、DPoP ヘッダに含まれる公開鍵 (通常は JKT (JWK Thumbprint) でいい) とトークンの紐付けを何らかの方法で保存しておきます。

その後、クライアントはリソースサーバにリクエストを送信する際、Authorization ヘッダでアクセストークンを指定するだけでなく、DPoP ヘッダも加えます。

そして DPoP ヘッダを受信したリソースサーバは、DPoP ヘッダを検証したあと、DPoP ヘッダに含まれる公開鍵とトークンの紐付けが正しいことも確認します。紐付けの確認方法は認可サーバしだいです。認可サーバが発行するアクセストークンが JWT 形式で、その中に JKT を含めるようになっていれば、リソースサーバは単独で紐付けを確認できます。そうでない場合、認可サーバのトークンイントロスペクションエンドポイントに確認しに行くという方法が考えられますし、認可サーバとリソースサーバが密に関連しているのであれば認可サーバのデータベースを直接参照するという方法もあるかもしれません。

トークンに紐付く公開鍵で検証できる署名を作成できるのは秘密鍵を持つクライアントだけなので、秘密鍵を持たないクライアントによるアクセストークンの悪用を防げるというわけです。

なお、認可サーバはトークンリクエストともに送られてきた公開鍵を覚えるだけであり、その公開鍵が誰の持ち物かまでは意識することはなく、ましてやユーザの識別を行うこともありません。そういったことがしたいのであれば、Mutual-TLS を採用すべきですね。

もっと細かい仕様も定められていたり、上記シーケンスより前に送信する認可リクエストに JKT を含めることができたりもしますので、詳細を知りたい方は RFC 9449 をお読みください。

DPoP 対応として実装すべきこと

次に、DPoP を導入するために実施すべきことを整理してみます。ここではエンタープライズシステムを想定し、OAuth 2.0 のフローは既に実装済であることを前提としています。

クライアント

クライアントがやることとしては、概ね次の4点です。

  1. キーペアの生成
  2. リクエストに応じたペイロードの生成
  3. JWS の生成
  4. リクエストやレスポンスの処理

キーペアの生成や JWS の生成は、ブラウザ上で動作するクライアントであれば Web Crypto API を使えばいいでしょう。それ以外のクライアントであれば、各開発言語に応じた方法やライブラリを使えばできるかと思います。生成したキーペアはクライアント側で安全に保存しておく必要もあります。

また、RFC に記載はないですが、新規のトークンの発行を試みるたびに新しくキーペアを生成するのが良いと思います。RFC では P-256 EC のキーペアが例示されていますが、別のパラメータ・アルゴリズムのキーペアでも良いはずです。

リクエストに応じたペイロードは、"4.2. DPoP Proof JWT Syntax" に書かれている通りです。必須の4項目に加えて、リソースサーバへのリクエストにはアクセストークンのハッシュ値を含めたり、認可サーバが nonce を返してきたときはそれを含めたりする必要もあります。

リクエストの処理として、DPoP ヘッダに JWS を含めたり、Auhorization ヘッダのトークンタイプを DPoP にしたりする必要があります。また、レスポンスの処理として、認可サーバが DPoP に対応しているとは限らないため、トークンレスポンスに含まれるトークンタイプが DPoP か Bearer の確認といったことも必要です。

なお、これらは全てフロントエンドのライブラリが担ってくれるようになるでしょうから、クライアント側では上記実装を行わずともライブラリの導入と設定だけで済むようになるのだろうと思います。

認可サーバ

認可サーバはクラウドサービスや Keycloak などの OSS を使っているかと思いますが、まずはそれらが DPoP をサポートしているか確認しましょう。Keycloak はバージョン 23.0.0 から DPoP が使えるようになっています。Amazon Cognito や Microsoft Entra ID ではまだ使えないようです。

認可サーバで DPoP が使えるようになっていれば、DPoP も使える状態にしましょう。クライアントやリソースサーバが DPoP 対応を完了するまでは、Bearer トークンを利用できないようにしてしまわないように注意しましょう。

もしも認可サーバを自前で開発しているならば、DPoP ヘッダを検証したり、発行したトークンを保存している箇所に JKT を含めたり、リプレイ攻撃を防ぐためのIDを一時的に保持したり、リソースサーバが JKT を確認できるようにしたり、やることはいろいろあります。

リソースサーバ

リソースサーバがやることとしては、概ね次の4点です。

  1. リクエストの確認
  2. JWS の署名の検証
  3. JWS のペイロードの検証
  4. JKT の計算・確認

まず、リクエストの Authorization ヘッダを見て、トークンが DPoP なのか Bearer なのか確認が必要です。DPoP タイプならば、DPoP ヘッダが存在するかどうかの確認も必要になります。Bearer タイプを拒否するのであれば、DPoP ヘッダが存在しない場合はこの時点で401エラーを返す必要があります。

DPoP ヘッダの JWS の署名の検証は当然のことで、ライブラリを使えばできるでしょう。

JWS のペイロードの検証として、リプレイ攻撃の確認、タイムスタンプの確認 (一定のずれは許容する必要があるはず)、トークンのハッシュ値の計算と確認などが必要となります。どれもRFC をしっかり読んで実装する必要があります。

JKT の計算はライブラリで簡単にできるとして、確認方法は前述の通り認可サーバしだいです。とはいえ、OAuth 2.0 フローを実装している時点でアクセストークンを確認する方法は実装済のはずなので、JKT の確認はその仕組みに少し付け足すだけかと思います。

DPoP 導入に向けて

DPoP を導入できるかどうかは認可サーバしだいですが、DPoP が利用可能かつ Mutual-TLS を導入できていないなら、DPoP の導入を検討してみるのが良いと思います。展開フェーズで考慮すべきことはあるものの、運用の負荷をほとんど増やさずにソフトウェア実装で実現でき、導入コストが比較的小さいというのが魅力的です。PKCE が当たり前に普及してきているように、将来 DPoP も同様に普及するようにも感じています。

DPoP にどの程度の効果があるのかについては、定量的に示せません。対象のシステムがインターネットからアクセスできず、内部に悪意者が居ないという仮定が成り立つのであれば、DPoP の効果はほとんどないかもしれません。

展開フェーズでは、ある時点で一斉に Bearer から DPoP に切り替えるというのはリスクが大きいため、一定期間は両方が利用できる状態を設ける必要があるかと思います。そのためには、クライアントやリソースサーバではその状態を考慮した実装が必要となってきます。最終的に DPoP のみが使える状態に持っていくのであれば、途中の状態は展開フェーズにおけるオーバーヘッドとも言えます。もちろんリスクを取って最小の実装で一斉に切り替えるという考えもありかもしれませんが。

Discussion