OAuth 2.0 Implicit Flowをユーザー認証に利用する際のリスクと対策方法について #idcon

おはようございます、ritouです。
今回は、一部で先週話題なりましたOAuth 2.0のImplicit Flowについてのエントリになります。

(2012/2/7 いろいろと修正しました。)

今回は以下の内容について整理したいと思います。

OAuth 2.0 Implicit Flowとは

OAuth 2.0ではサードパーティーアプリケーションが保護リソースへのアクセス権限を得るためのいくつかのフローが定義されています。
(仕様中ではFlowやGrant Typeなどという用語が入り混じっていますがここではフローという表現を利用します)

その中に、

  • 認可サーバー,サードパーティークライアント,エンドユーザーの3者が存在
  • サードパーティークライアントはユーザーの保護リソースへのアクセス権限を要求
  • ユーザーが認可サーバーが提供する画面上でアクセス権限委譲に関する同意処理を行う

というフローがあります。

例として、大手写真共有サイト(サーバー)への画像アップロードを行うアプリケーションを考えます。
それらのアプリケーションの設定時に一旦写真共有サイト上でエンドユーザーの同意を行います。この同意処理により、アプリケーションは画像のアップロード権限を取得できます。

仕様では、アプリケーションの特性により2つのフローに分かれています。

  • Authorization Code Flow
  • Implicit Flow

"Authorization Code Flow"は、Webサーバー上で動作するWebアプリケーションなど、サーバー-クライアント間の共有秘密鍵"Client Secret"を安全に保管できる(ことになっている)場合に利用するフローです。
仕様にある図を引用します。

リソースアクセスのためのチケットにあたるアクセストークンを取得する処理の中で、(D),(E)で示される認可サーバー-クライアント間の直接通信にClient Secretを利用します。

一方、iPhone/Androidアプリケーションなど、リバースエンジニアリングが可能でありClient Secretを安全に保持することができない場合にはImplicit Flowを利用します。
JavaScriptで記述され、Ajaxを利用して動作するアプリケーションなども同様にImplicit Flowを利用します。
こちらも、仕様にある図を引用します。

図中でWeb-Hosted Client Resourceとありますが、ユーザーの同意後にアクセストークンがURIフラグメント識別子に付加されてクライアントに渡されます。
Authorization Code FlowにあるようなClient Secretを用いた直接通信は存在しません。
後者のImplicit Flowを利用した"あるユースケース"に潜むセキュリティホールについて説明します。

OAuth 2.0をユーザー認証に利用するケース

世の中には"OAuth認証"と呼ばれる、OAuthのフローをユーザー認証機能に利用するアプリケーションが存在します。

さきほどの写真共有サイトの例でいうと、

  • 「○○でログイン」というリンクをクリックして毎回OAuth 2.0の処理を行い、アクセストークンを取得
  • 取得したアクセストークンをサーバーが提供するユーザープロフィールAPIに送り、ユーザー情報を取得
  • 取得したユーザー情報に基づき、アプリケーション上のユーザーとの紐付けを行いログイン状態とする

というようなものです。

このようなユースケースにおいて、"同意したユーザー"="プロフィールAPIが情報を返すユーザー"である場合は、プロフィールAPIの結果を利用してログイン状態としても問題ありません。
ただし、OAuthのフローにおいて、「エンドユーザー△△(user_id)がクライアント□□(client_id)に対してリソース☆☆(scope)についてのアクセス権限委譲に同意した」という結果はやりとりされず、クライアントはアクセストークンのみを取得します。
クライアントは「アクセストークンが自らに対して発行されたもので、同意処理をしたユーザーのリソースへのアクセス権限を持つ」ことが保障されている(確認できる)必要があります。

不正なアクセストークンを取得/利用してしまうリスク

クライアントが意図しないアクセストークンを取得してしまうと、別のユーザーのプロフィールAPIの結果を用いてログイン状態とする、つまりなりすましが可能になります。

Authorization Code Flowの場合

Authorization Code Flowにおいて意図しないアクセストークンを受け取るような攻撃を行うためには、Authorization Codeの値から別のユーザーのものに置き換えることが可能です。
アクセストークン取得までの間にClient Secretを用いた直接通信を行いますので、別のクライアント向けに発行されたAuthorization Codeに置き換えた場合は認可サーバー側のチェックでエラーとなります。
同じクライアント向けに別ユーザーが同意処理を行い発行されたAuthorization Codeに置き換えることで、「エンドユーザー△△が」という部分を別のものに置き換えることが可能です。

ほとんどのユースケースでAuthorization Codeの受け渡しはリダイレクトURIに付加され自動で行われます(デバイスをまたぐ場合など画面に表示して手動でコピペなどもあります)。
認可サーバーはAuthorization Codeの有効期限を短く設定する、ワンタイムのものとして何度も利用しないなどの対策でこのリスクを低減するような実装をする必要があり、実際にたいていの認可サーバーはそのように実装しているようです。

仕様では下記のようにあります。

認可コードは認可サーバーによって許可される. 漏洩のリスクを軽減するため, 認可コードは発行されてから短期間で無効にしなければならない (MUST). 認可コードの有効期限は最大でも10分を推奨する (RECOMMENDED). クライアントは2回以上認可コードを使用してはならない (MUST NOT). もし認可コードが2回以上使用された場合は, 認可サーバーはリクエストを拒否しなければならず (MUST), この認可コードを基に発行されたこれまでのすべてのトークンを無効化すべきである (SHOULD). 認可コードはクライアント識別子とリダイレクトURIに紐づく.

Implicit Flowの場合

Implicit Flowでは認可サーバーが発行してアクセストークンをリダイレクトURIフラグメント識別子として間接的を受け取るため、エンドユーザーのブラウザ含めて漏洩や置き換えのリスクが大きいと言えるでしょう。
アクセストークンの置き換えにより、「エンドユーザー△△がクライアント□□に対してリソース☆☆について」という項目を別のものに置き換えることが可能です。

仕様では下記で言及されている内容に相当します。

クロスサイトリクエストフォージェリ (CSRF) は, 攻撃者が犠牲となるエンドユーザーのユーザーエージェントに (例えば, ユーザーエージェントに誤解を招きやすいリンクやイメージ, 転送によって) 悪意のあるURIを閲覧させることにより (通常, 有効なセッション・クッキーの存在によって) 信頼が確立されたサーバーへ接続させる手法である.

クライアントのリダイレクトURIに対するCSRF攻撃は, 攻撃者が自身の認可コードやアクセストークンを紛れ込ませることを可能とし, クライアントに犠牲者の保護されたリソースではなく, 攻撃者のリソースに紐付いたアクセストークンを使わせることが出来てしまう (例えば, 犠牲者の銀行口座情報を攻撃者の管理しているリソースへ保存してしまう, といったことも可能となる).

CSRFへの対策が(MUST)になっていますが、(SHOULD)としてstateパラメータの利用が明記されています。

クライアントは自身のリダイレクトURIに対してCSRF保護対策を導入しなければならない (MUST). 一般的に保護対策は, リダイレクトURIのエンドポイントへ送られたすべての要求に対して, 要求とユーザーエージェントの認証状態を紐付けるための値を含めることにより実現する (例えば, ユーザーエージェントを認証するために使うセッションクッキーのハッシュなど). クライアントは認可要求の発行時, この値を認可サーバーへ伝搬するために state リクエストパラメーターを利用すべきである (SHOULD).

一旦エンドユーザーの認可が得られると, 認可サーバーはエンドユーザーのユーザーエージェントを state パラメーターに含まれる要求されたバインド値と共にクライアントへリダイレクトする. クライアントはバインド値とユーザーエージェントの認証状態を突合することによりリクエストの正当性を確認することが出来る. CSRFを防ぐために使用されるバインド値は推測不能な値を含まねばならず (MUST), ユーザーエージェントの認証状態 (例えば, セッションクッキーやHTML5のローカルストレージ) はクライアントおよびユーザーエージェントのみがアクセスできる状態に保たれなければならない (つまり, 同一起源ポリシーによる保護) (MUST).

しかし、stateパラメータではアクセストークンの置き換えを防ぐ手段にはなりえません。
ブラウザからクエリで送られたものをHTTP Headerを監視するアドオンなどで抜き出し、レスポンスのフラグメント識別子を生成することが可能です。
また、アクセストークンとstateパラメータの組み合わせの検証まではOAuth 2.0の仕様で定義されていません。

よって、Implicit Flowで受け取ったアクセストークンを用いてプロフィールAPIをたたき、その結果をユーザー認証に利用するサービスはアクセストークン置き換え攻撃により別のユーザーのログイン状態を生成されるリスクが存在することになります。

誰が攻撃者になれるのか

アクセストークンを扱える存在であれば、トークン置き換え攻撃が可能です。

  • Implicit Flowを利用有無にかかわらず、アクセストークンを扱っているクライアントの管理人
  • Implicit FlowのリダイレクトURIの履歴にアクセスでき、他人の上記条件のアクセストークンを取得できる人物

プロフィールAPIにアクセスさせる必要があるため、攻撃対象のクライアントからのリクエストと同じもしくはより広いScopeを持ったアクセストークンが必要です。

対策

Authorization Code Flowを利用

まず初めに、現在Implicit Flowを利用しているクライアントの開発者は、Authorization Code Flowを利用できないかを確認していただければと思います。
Authorization Code Flowを使えそうな例として、iOS/AndroidアプリとバックエンドのWebサーバー間でアプリ固有のデータをやりとりするような場合です。
アプリ上で認可サーバーとのやり取りを行わずにAuthorization CodeをバックエンドのWebサーバーに送り、Webサーバー側でアクセストークンの取得処理を実装することができれば、Implicit Flowのリスクを回避することができます。

アクセストークンの内容を確認できるAPIを利用

自分が利用している認可サーバーが"アクセストークンが発行された対象のクライアントなどの情報を確認できるAPI"を持っていれば、アクセストークン置き換え攻撃を検知可能でしょう。
OAuth.jpの記事によると、Facebookでは発行元のクライアントを取得することが可能なようです。
http://oauth.jp/oauth-20-implicit-flow
ただし、OAuth 2.0にこのような機能を持つエンドポイントや処理の定義はありませんので、利用するためには認可サーバー側で独自に実装を行う必要があります。

OpenID ConnectのID Tokenを利用

まだ実際のプロダクション環境でOpenID Connectを実装しているサービスはありませんが、認可サーバーがもし対応した場合にはID Tokenを利用することによりアクセストークン置き換え攻撃に対応することが可能です。
OpenID ConnectのID Tokenについての仕様は"Messages"の"2.1.1. ID Token"にあります。
http://openid.net/specs/openid-connect-messages-1_0.html#id_token : "2.1.1. ID Token"

サーバー、クライアント、ユーザー識別子、リプレイアタック防止のための検証に利用できる文字列が含まれたJSONを文字列として表し、署名がつけられて渡されます(JWT,JWSという仕様です)。

  • iss : Server Identifier
  • user_id :
  • aud : Client Identifier
  • exp : (有効期限)
  • acr : 認証レベルなど
  • nonce : リプレイアタック防ぐための文字列。Authorization Requestにnonceパラメータが必須で追加される

OAuth 2.0の仕様に与える影響を抑えるため、ID Tokenとはアクセストークンとは別に提供され、クライアントはリソースアクセスに利用しません。
このID Tokenをクライアントが適切に検証し、ユーザー認証のロジックに利用することで、アクセストークン置き換えによるなりすましのリスクを防ぐことが可能です。

クライアントのID Tokenの処理

ID Tokenは署名つきのJSONオブジェクトですので、署名検証を行って有効性が確認できた場合はJSONオブジェクトを複合して利用可能です。
また、それらの検証をサーバーに任せられるCheck ID Endpointと呼ばれるAPIにID Tokenを送ることで内容が返されます。

クライアントは、下記のようなJSONオブジェクトの内容の検証を実装する必要があります。

  • aud : 自らのクライアント識別子であることを確認
  • iss : サーバーが意図したものであることを確認
  • exp : 有効であることを確認
  • nonce : Authorization Requestに含んだ文字列と等しいことを確認

OpenID ConnectのID Tokenの検証についての記述はこちらにあります。
http://openid.net/specs/openid-connect-messages-1_0.html#anchor23 : "5.4. Check ID Response Verification"

クライアント側のアクセストークンとの組み合わせの検証について

現状のID Tokenにはアクセストークンの値が含まれません。そのため、厳密なアクセストークン置き換えの検知は不可能です。
Connect仕様策定メンバーの中でID Tokenにアクセストークンのハッシュ値を含むことを検討しています。

まとめ

  • OAuth 2.0 Implicit Flowをユーザー認証に利用する際に、アクセストークン置き換えによる別ユーザーへのなりすましリスクが存在する
  • 同じ認可サーバーに対して発行され、同じもしくは広いScopeを付加されたアクセストークンを取得できれば攻撃者になりえる
  • Authorization Code Flowを利用可能な場合は利用するよう修正する
  • アクセストークンの内容が確認できるAPIが提供されている場合はそれを利用する
  • 認可サーバー、クライアントが互いにOpenID ConnectのID Tokenの発行/チェックロジックを実装することでも対応可能

私はOpenID Connectを推していますので最後の方法をお勧めさせていただきます。

デモが見たい場合

先週末にIdentity ConferenceというIdentityやセキュリティに詳しい人間が集まる勉強会がありました。
そこで軽い気持ちで緩い感じのデモをしたところ、

  • OpenID Connectを啓蒙していく立場として、実装面で気をつけることをより細かく説明するよう気をつけるべき」
  • 「こんな説明ではわからない。しっかり問題となるユースケースとリスクを整理して対応方法を紹介・・・」

という旨の指摘をいただきましたので、今回のエントリは自分なりに仕様の言及箇所など整理しなおしたものです。

idcon #11でのデモ用スライドはこちらになります。
Idcon11 implicit demo

これだけではわかりませんが、Implicit Flowを実装したクライアント×2、OpenID ConnectのID Tokenの処理を実装しているクライアント×1を用意しました。
コピペで手が震えたのか、デモにはかなり苦戦しましたが。。。

ではまた!