OAuth 2.0/OpenID Connectで使われるBindingの仕組みについて整理する

f:id:ritou:20191202001445p:plain

おはようございます、OAuth警察を装っている ritou です。

qiita.com

認証認可技術 Advent Calendar 2019 2日目の記事です。

今日もやっていきましょう。

(2020/3/9追記)本投稿の内容をさらにわかりやすく整理された本を @authyasan さんが書かれています。

私もレビューをさせていただきました。本投稿を読んで興味を持たれた方は読んでみてください!

今回の内容

OAuth 2.0/OpenID Connect(OIDC)の新しい拡張仕様の記事などを書くときに

  • 「OAuth 2.0のこのフローにはこういう攻撃が考えられる」... ●●●が置き換え可能
  • 「対策としてこれがあるがこのケースでは使えない、もしくは対策がなくて詰んでいた」
  • 「なので考えたのがこの拡張仕様だ」

という流れにすることがあります。というかそもそも新しいユースケースなどに対して未定義の部分や脆弱な部分にパッチを当てるような拡張仕様の場合、Abstract/Introductionの流れがそうなってたりします。 んで、そういうのを見た人がOAuth 2.0やOIDC全体に対して「攻撃や脅威を整理するところから始めて対策を完全に理解しよう!」となったりするんですが、ちゃんとやろうと思うと RFC6819 みたいになってしまい、特に初学者がやるのはボリューム的にもあんまり筋が良くないと思います。

そこで、今回は 個別の事案じゃなく、OAuth 2.0/OIDCを利用する上で最も重要だと思っている "Binding" に注目して各パラメータや拡張仕様を整理します。

ここで言う "Binding" とは OAuth 2.0/OIDC でWebアプリケーションからよく利用されている認可(認証フロー)において

  • ある値をセッションなどと紐付けて
  • どこかでパラメータとして指定して
  • どこかで検証されることで
  • 一連の流れが同一セッションで行われたとみなせる

と言うのを実現するための仕組みです。

この辺りが理解できると

あたりの話も少し楽になるのではないかと思っています。

とはいえ、特に新しい話ではないですし過去に何度も記事を書いてるので、普通に詳しい人はこんなの読んでないで自分のお仕事に集中してください

セッションとの紐付けについて

この後の説明で「セッションと●●を紐づける」という表現を多用しています。 これはレガシーなWebアプリケーションでいうところの

  • HTTP CookieにセッションIDをもち、それに紐づく値をデータストアに格納する
  • HTTP Cookieそのものに紐づく値を署名つきエンコードや暗号化などをして保持する

というような処理のことです。 アドカレ1日目の記事でも取り上げたセッション管理の話ですが、この記事においても本質ではないので細かい実装などには言及しません。

対象となる OAuth 2.0 / OIDC のフロー

この記事では、OAuth 2.0の各種フロー(Grant Type)のうち、最も一般的な Authorization Code Grant と OIDC の ID Tokenを絡めたあたりを扱います。

大まかな流れをシーケンスにするとこうなります。

f:id:ritou:20191118004418p:plain

最初にフロントチャンネルを用いたやりとりがあります。

  • AuthZ(AuthN) Request : Webブラウザなどのリダイレクトを使ってClient/RPからAS/OPにGETのリクエストが送られる
  • AuthZ(AuthN) Response : AS/OPからClient/RPへの戻りの部分で、Webブラウザなどのリダイレクトを使ったGETもしくはPOSTのリクエストで(Authorization Codeなどの)パラメータが送られる

その後にバックチャンネル、いわゆるサーバー間でのやりとりが行われます。

  • Access Token Request : Client/RPからAS/OPへのPOSTリクエストで(Authorization Codeなどの)パラメータが送られる
  • Access Token Response : AS/OPからのJSONレスポンスでAccess Tokenなどが返される

この一連の流れの中で、バックチャンネルのサーバー間通信ではTLS(SSL)の利用やクライアント認証によってリクエストがClient/RPから送られ、AS/OPから返されたことが検証されます。

それに対して、フロントチャンネルのやりとりのところはいくつかのパラメータの受け渡し方が定義されています。

AuthZ(AuthN) Request では JWT を利用することで AS/OP はリクエストが Client/RP により生成された値であることを検証できます。

f:id:ritou:20191202012355p:plain

AuthZ(AuthN) Response でも同様に、レスポンス自体を JWT にしたりハッシュ値を含む JWT を渡すことで、Client/RP はレスポンスが AS/OP により生成された値であることを検証できます。

f:id:ritou:20191202013519j:plain

ここから紹介するパラメータの中には、 AuthZ(AuthN) Response に JWT を利用するものが出て来ますので意識しておくと良いと思います。

state パラメータ by OAuth 2.0

"#state警察" でおなじみ、OAuth 2.0 でCSRF対策を目的として定義されているパラメータです。

  1. Client/RP は セッションとstateパラメータの値を紐付ける
  2. Client/RP は AuthZ(AuthN) Request に state パラメータを含む
  3. AS/OP は AuthZ(AuthN) Request に含まれた state パラメータをそのまま AuthZ(AuthN) Response に含む
  4. Client/RP は AuthZ(AuthN) Response に含まれた state パラメータがセッションと紐付けられた値と一致することを検証する

f:id:ritou:20191119102956p:plain

HTMLフォームのCSRF対策トークンを Client/RP と AS/OP の間のやりとりで使うイメージで、AuthZ(AuthN) Request と AuthZ(AuthN) Response が同一のセッションに紐づいていることを検証できます。

ここで、AuthZ(AuthN) Responseの検証に注目します。

  • クエリパラメータ/フラグメントとして指定
  • response_mode=post としてPOSTを利用

この場合、state と同時に AuthZ(AuthN) Response に含まれる Authorization Code の値が AS/OP が生成したものであることを検証できません。 悪意のあるユーザーが第3者に向けて発行された Authorization Code の値を取得できた場合、自分の AuthZ(AuthN) Response に指定することが可能です。

それに対して、

  • response_type=code id_token のように JWT 形式の ID Token と一緒に返す : Authorization Code のハッシュ値を含む
  • response_mode=jwt もしくは response_mode=query.jwt のようにJWT形式で返す : state, Authorization Code を Payload に含む

と言う風にJWTを利用する場合は、AuthZ(AuthN) Response 内の Authorization Code が AS/OP により生成されたことを検証できます。

ちなみに FAPI では ID Token に state パラメータのハッシュ値を含む ことも定義されており、その場合は

  • AuthZ(AuthN) Response : AS/OP が生成したものであり、AuthZ(AuthN) Request と紐づいている
  • Access Token Request : AuthZ(AuthN) Request と紐づいている Authorization Code を指定
  • Access Token Response : AuthZ(AuthN) Request と紐づいている

と全体が安全であることを確認できる状態になります。

state パラメータの特徴としては

  • AuthZ(AuthN) Request が AuthZ(AuthN) Response と紐づいていることを確認できる
  • OIDC の ID Token と FAPI の拡張によって、フロー全体をより安全にできる

となります。

nonce パラメータ by OpenID Connect

"nonce があれば state いらず" なんて言われてたり言われてなかったりする、OIDCで定義されているパラメータです。 こちらはリプレイアタック対策のためのパラメータとして定義されています。

この nonce ですが、OIDC の仕様によりAuthZ(AuthN) Request で scope に openid が指定された場合は Access Token Response に ID Token が含まれ、その Payload に nonce の値が含まれます。

まずは AuthZ(AuthN) Request で response_type に id_token を指定しなかった場合 を見ていきます。

  1. Client/RP は セッションとnonceパラメータの値を紐付ける
  2. Client/RP は AuthZ(AuthN) Request に nonce パラメータを含む
  3. AS/OP は AuthZ(AuthN) Request に含まれた nonce パラメータを認証イベントに紐づけておき、Access Token Response に含まれる ID Token に nonce の値を含む**
  4. Client/RP は Access Token Response の ID Token に含まれた nonce パラメータがブラウザセッションと紐付けられた値と一致することを検証する**

f:id:ritou:20191129053811p:plain

この場合、AuthZ(AuthN) Request と Access Token Response が同一セッションに紐づいていることが確認できます。

  • Access Token Response : AS/OP が生成したものであり、AuthZ(AuthN) Request と紐づいている
  • Access Token Request : AuthZ(AuthN) Request と紐づいている Authorization Code を指定
  • AuthZ(AuthN) Response : AuthZ(AuthN) Request と紐づいている(Authorization Code が含まれてた)

というようにフローを逆から見ていくような形で全体の安全性を確認できます。

また、AuthZ(AuthN) Request で response_type に id_token を指定した場合 については

  1. Client/RP は セッションとnonceパラメータの値を紐付ける
  2. Client/RP は AuthZ(AuthN) Request に nonce パラメータを含む
  3. AS/OP は AuthZ(AuthN) Request に含まれた nonce パラメータを認証イベントに紐づけておき、AuthZ(AuthN) Response に含まれる ID Token に nonce の値を含む**
  4. Client/RP は AuthZ(AuthN) Response の ID Token に含まれた nonce パラメータがセッションと紐付けられた値と一致することを検証する**

f:id:ritou:20191129061101p:plain

となり、全てのレスポンスが AS/OP が生成したものであり、AuthZ(AuthN) Request と紐づいていることを確認できます。

  • AuthZ(AuthN) Response : AS/OP が生成したものであり、AuthZ(AuthN) Request と紐づいている
  • Access Token Request : AuthZ(AuthN) Request と紐づいている Authorization Code を指定
  • Access Token Response : AS/OP が生成したものであり、AuthZ(AuthN) Request と紐づいている

nonce vs state

ここまでみて、state と nonce ってだいたい同じじゃねーかと思ったりするわけですが、一般的なWebアプリケーションのセキュリティのお話で

  • CSRF対策 : セッションに一意に紐づく値を利用
  • リプレイアタック対策 : 毎回異なる値を利用

という違いがあるため、仕様では state パラメータはセッションに紐づく値、nonce はワンタイムの値を使うように定義されています。

そして、

  • 実装として nonce をセッションに紐づけておいて検証することが一般的
  • nonce についてAS/OPはFAPIのような拡張を入れなくても OIDC の仕様に沿っていれば上記の検証が可能

という面から、"OIDC を調べて nonce の検証したら state の検証いらないって気づいちゃった" 派が一定数出てくるでしょう。

state と同じ、フロントチャンネルで AuthZ(AuthN) Response に含まれる ID Token の nonce を検証するならば state は不要でしょう 。 ただし、フロントチャンネルでは ID Token を扱わず、バックチャンネルでの Access Token Response までやって ID Token の検証をするのであれば、"もっと早い段階でチェックできるものはする(stateのゆるい検証と併用する)" という考え方もありかなと思います。

code_challenge, code_verifier by PKCE

PKCE は "Proof Key for Code Exchange by OAuth Public Clients" です。

  • Secret を安全に管理できない Public Client(ネイティブアプリやブラウザベースアプリなど)は Implicit Grant ではなく AuthZ Code Grant + PKCE 使え!
  • Confidential Client(Webアプリ)も PKCE でよりセキュアに!

という方向性で広く知られるようになりましたが、今回も他のパラメータの説明に揃えてWebアプリケーションでの利用を前提として見ていきます。

  1. Client/RP は ブラウザのセッションとcode_verifierパラメータの値を紐付ける
  2. Client/RP は AuthZ(AuthN) Request に code_verifier のハッシュ値(もしくはそのまま)である code_challenge パラメータを含む
  3. AS/OP は AuthZ(AuthN) Request に含まれた code_challenge パラメータを認証イベントに紐づけておく
  4. Client/RP は Access Token Request にブラウザのセッションと紐づけておいた code_verifier の値を含む
  5. AS/OP は 認証イベントに紐づけられた code_challenge と Access Token Request に含まれた code_verifier の値を比較(ハッシュ値の検証)し、正しい組み合わせであれば Access Token Response を返す

f:id:ritou:20191129152046p:plain

という流れで、PKCE は AS/OP が AuthZ(AuthN) Request と Access Token Request が同一セッションに紐づいていること を検証します。

全体で見たときには

  • AuthZ(AuthN) Response : Client/RP が確認できるものはない
  • Access Token Request : AuthZ(AuthN) Request と紐づいている code_verifier を指定
  • Access Token Response : AS/OP が生成したものであり、AuthZ(AuthN) Request と紐づいている

となるでしょう。 AuthZ(AuthN) Response の検証ができないということは、指定された Authorization Code を使って毎回 Access Token Request を送るということになりますが、悪意のあるAuthZ(AuthN) Responseを大量に生成されることにより、Client/RPとAS/OPどちらにも負荷がかかってしまうという事態もありえます。 ここは state や nonce(フロントチャンネルのID Token利用)と併用することで、"早い段階でチェックできるものはする" 設計にする方が良いのではないかと思います。

state, nonce, PKCE を導入しても効果が出ないパターン

ちなみに、Client/RP が state, nonce の検証をサボっても処理を進められますが、PKCEは正しいパラメータを AS/OP に送らなければ OAuth/OIDC の処理は進みません。 この点は AS/OP 側としてはフロー全体のセキュリティのことを考えると安心できる仕組みだと思いますが、気にしなければならないのは、state, nonce, PKCE 共にパラメータを生成するのは Client/RP であることです。 以前、Client/RP のパラメータ生成がよくないと意味がなくなるって話をブログ記事に書きましたのでお時間がありましたらどうぞ。

ritou.hatenablog.com

まとめ

OAuth/OIDCの警察ネタでおなじみ、 state, nonce, PKCE を "セッションとの紐付け" という視点から整理しました。

個人的には Client/RP 開発者は AS/OP がサポートするパラメータはなるべく使って検証もがっつりしとけや と言うところですが、 この記事を読んでそれぞれの処理がどう繋がっていて、途中でパラメータを置き換えられた時にどこで検知できるかを意識してもらえたら幸いです。

だいぶ暖まってきたところで、明日(3日目)の 「認証認可技術 Advent Calendar 2019」は...またまた私です。くどいですね。 今回紹介した "Binding" の仕組みを利用してバックエンドサーバーがいるネイティブアプリの場合にどう認証フローを組み立てていけば良いのかを紹介します。DroidKaigi 2020 に応募して散ったネタであります。

ではまた明日。