OAuth入門 - 認可の仕組みとPKCEによるセキュリティ対策を理解する
概要
最近、個人開発しているアプリにOAuthを導入するため、OAuthについて勉強しました。
自分の頭の整理と、人に説明できるようにするために、学んだことをまとめておきます。
あくまで初心者が学習した内容のまとめなので、もし間違っている箇所があれば優しくご指摘いただけると嬉しいです。
専門的な厳密さよりも、自分なりにしっくりくるイメージ重視で書いてみます。
OAuthの概要
OAuthは一言でいうと、アプリにユーザーのパスワードを教えなくても、代わりに短期間かつ権限を限定したアクセスを提供できる認可の仕組みです。
例えば、あるWebサービスAがあって、そのWebサービスAがユーザーのGoogleドライブにアクセスしたいとします。
ちなみにここでOAuthの用語でいうと、
- ユーザーはリソースオーナー
- WebサービスAはクライアント
- Googleドライブはリソースサーバー
- Googleは認可サーバー
にあたるようです。
すごくシンプルに考えるなら、WebサービスAにGoogleのユーザーIDとパスワードを渡しておけば、WebサービスAはユーザーの代わりにGoogleドライブにログインして操作できます。
ただ、これだとセキュリティ的にとても危険です。
ユーザーのパスワードを教えるということは、ユーザーと同等の権利をこのWebサービスに与えてしまうことになるからです。
Googleドライブの中のデータを編集閲覧する権限に加えて、そもそもGoogleアカウント自体のパスワード変更もできてしまうので、ユーザーを締め出すようなことも可能です。また、Webサービス自体に悪意がなくても、Webサービスからパスワードが漏洩して、悪意のある人に渡ってしまうリスクもあります。
そこでOAuthの登場です。
OAuthを使うとパスワードを教える代わりに、アクセストークンというものを発行してもらいます。このアクセストークンは有効期間が設定されていて、かつ権限も制限されています。例えば、Googleドライブのファイルに対して、閲覧だけできる権限を一時間与える、みたいな感じです。
OAuthの仕組み
例えばWebサービスAでユーザーが「Googleドライブと連携する」みたいなボタンを押したとき、どんな流れになるか、自分なりに整理してみました。
- WebサービスがGoogleの認可エンドポイントにリダイレクトする(認可リクエスト)
- このリダイレクトするときにはクライアントID、リダイレクトURI、state、scope、response_typeなどをクエリパラメータとして含めておく。
- リダイレクトURIはGoogle側の処理が終わった後、WebサービスAに戻ってくるためのURLです。WebサービスAの処理がそこから再開します。
- stateはCSRF対策のためのランダムな文字列。あとで説明します。
- scopeはどんな権限を要求するかを示すパラメータ。アプリごとに何の権限を要求するかを事前に決めておいてそれを要求します(例えばGoogleドライブのファイルを閲覧する権限とか)
-
response_typeには
codeを指定します(認可コードフローであることを示す)。
- リダイレクトによりユーザーにはGoogleのログイン画面が表示される
- このリダイレクトするときにはクライアントID、リダイレクトURI、state、scope、response_typeなどをクエリパラメータとして含めておく。
- ユーザーがGoogleにログインする(認証)
- ユーザーがログインすると今度はWebサービスAにGoogleドライブへのアクセスを許可するかどうかを尋ねる(権限移譲の確認画面)
- ユーザーが許可すると、Googleが認可コードを生成する
- Googleの認可サーバーは1のリクエスト時に設定されていたリダイレクトURIにリダイレクトする(WebサービスAに戻る)
- このときクエリパラメータに認可コードとstateが含まれる
- WebサービスAがGoogleのトークンエンドポイントにアクセストークンを要求する(トークンリクエスト)
- このときクエリパラメータに認可コード、クライアントID、クライアントシークレットなどを含める
- Googleがアクセストークン、リフレッシュトークン、アクセストークンの有効期限などを返す
- WebサービスAがアクセストークンを使って、Googleドライブにアクセスする
図にする以下のようになります。
ポイントになるのは1でWebサービスAがGoogleの認可エンドポイントにリダイレクトした後、ユーザーは認証や権限移譲の確認をGoogle認可サーバーとやり取りしていることです。WebサービスAではありません。
②のログインがパスワード認証だとすると、ここでユーザーはパスワードを入力しますが、これはWebサービスAにではなく、Google認可サーバーに対して入力しています。そのため、WebサービスAはユーザーのパスワードを知ることはありません。
権限移譲の確認も同様で、認可コードを生成した後、ようやくWebサービスAがでてきます。これにより、WebサービスAはユーザーのパスワードを知らなくても、Googleドライブにアクセスするためのアクセストークンを取得できます。
そして、このアクセストークンには①でリダイレクトするときにscopeパラメータで要求した権限だけが含まれるため、WebサービスAはその範囲内でしかGoogleドライブにアクセスできません。
⑦でアクセストークンが返るときに有効期限も返されるため、一定期間が経つとアクセストークンは使えなくなります。
これで、パスワードではない、権限が限定されて有効期限のあるアクセスが可能になります。
OAuthの補足説明
stateパラメータについて
途中で出てきたstateパラメータについて説明します。これはCSRF(Cross Site Request Forgery)対策のためのものです。CSRFとは、悪意のある第三者がユーザーに対して不正なリクエストを送信させる攻撃手法です。
OAuthで特に問題になるのが**ログインCSRF(アカウント紐付けCSRF)**です。
これは被害者のアカウントに攻撃者のリソースを勝手に紐付けられてしまう攻撃で、今回の例だとユーザーはGoogleドライブ連携で勝手に攻撃者のGoogleドライブに連携されてしまうことです。「あれ?なんかGoogleドライブの連携やった覚えないけど完了してるな、まぁいいや」で自分のGoogleドライブだと思ってデータを入れると、攻撃者のGoogleドライブに入ってしまうみたいなことが起こります。
どういう原理で起こるかというと以下です。
- 攻撃者が自分のGoogleアカウントでOAuth連携を途中まで進め、認可コードを含むリダイレクトURIを取得
- 攻撃者は罠サイト(画像タグやJavaScriptなど)で自分が取得した認可コードを含めたリダイレクトURIをユーザーに踏ませる
- 被害者のブラウザが自動的にWebサービスAのリダイレクトURIにリクエストを送る(被害者のセッションCookieが自動で付く)
- WebサービスAは被害者のセッションで処理してしまい、攻撃者のGoogleアカウントを被害者に紐付けてしまう
前提事項として、ユーザーがWebサービスAにログインしている状態である必要があること、Googleドライブ連携をしていないことが必要で確率低くないか?という気もしますが、それなりに起こりうるようです。
stateはこれを防ぐための仕組みです。
OAuth開始時、WebサービスAはランダムなstateを生成し、ユーザーのセッションに保存しておきます。
認可コードが返ってきたときに一緒にstateが返ってくるので、それと最初に自分が生成したstateを比較。一致すれば処理続行、不一致なら拒否です。
OAuthは認証ではない
OAuthのフローを見ていると、②で認証しているからOAuthは認証、認可両方を提供していると思いますが、OAuthで提供しているのは認可のみです。
じゃあ、認証ってなんなんだとなりますが、認証とはWebサービスAがユーザーが誰であるかを知ることを指します。
そもそも最初WebサービスAを開いたとき、ユーザーのIDとパスワードを使ってログインする画面があるとします。ユーザーがここにログインすると、WebサービスAは今ログインしているユーザーが仮に「user123」だとわかります。これが認証です。
で、WebサービスAはそのユーザーをGoogleドライブにアクセスさせたいとします。
そのときに発動するのがOAuthで、これを使ってユーザーにGoogleドライブへのアクセスを許可してもらいます。これが認可にあたります。
②でGoogle認可サーバーがユーザーに対して認証を行っていますが、これはあくまでGoogleドライブにアクセスするための認可を与えるための前提条件としての認証です。
で、ただOAuthの仕組みを使えば、認証もできてしまいそうなのが話をややこしくしています。
例えば、scopeにユーザーのプロフィール情報を要求するスコープを指定しておけば、アクセストークンを使ってそのユーザーのプロフィール情報を取得できます。プロフィール情報にはユーザーIDも含まれるので、これを使って認証もできてしまいます。
PKCEについて
個人的にPKCEがすぐ忘れてしまうので追記します。
先ほどstateでCSRF対策を説明しましたが、PKCEはOAuthフローにおいて認可コードを盗まれても、アクセストークンに交換されないようにする仕組みです。特にモバイルアプリやシングルページアプリ(SPA)などのパブリッククライアントで問題になります。
今回はモバイルアプリの例でいきます。
モバイルアプリのOAuthフロー
モバイルアプリ(iOS / Android)でOAuthを使う場合のフロー図は以下のようになります。Webアプリとの大きな違いは、アプリとブラウザの2stepがあることです。
モバイルアプリ版OAuthで考えられる攻撃
⑦で認可コードが返るとき、本来ならば正規のモバイルアプリが起動しますが、悪意のあるアプリを起動させることができる可能性があります。
モバイル端末でアプリを起動するときはカスタムURLスキーム(最初にアプリ名が付くcom.example.app://callbackのようなURL)を使いますが、これは他のアプリと同じものを設定することも可能なようです。
つまり正規のアプリと同じURLスキームを持つ悪意のあるアプリがインストールしてあると、⑦で認可コードが返るときに悪意のあるアプリのほうが起動して、認可コードを横取りされてしまう可能性があります。
そして攻撃者は横取りした認可コードを使って、自分のアプリから認可サーバーにトークンリクエストを送ることで、被害者のアクセストークンを取得できてしまいます。これが本当の問題です。
PKCEの仕組み
これを防ぐためのPKCEの仕組みを説明します。
①で認可エンドポイントを開くとき
- アプリ側はランダムな
code_verifierとこれをハッシュ化したcode_challengeを作成する -
code_challengeのほうを含めて認可リクエストを送信する - 認可サーバー側は
code_challengeを保存しておく
⑨でトークンリクエストを送信するとき
- アプリ側は
code_verifierを含めてトークンリクエストを送信する - 認可サーバー側は受け取った
code_verifierをハッシュ化して、最初に保存したcode_challengeと比較する。同じならOK、違うなら拒否する
これで攻撃者が認可コードを横取りしても、認可リクエスト時に生成したcode_verifierがなければ検証を突破できないため、攻撃を防げます。
フローで表すと以下になります。
なかなか複雑になってきましたね。
参考資料
↑これむちゃくちゃいい本でした、、そもそもOAuth実装してみようと思ったきっかけもこれです。
↑フォロワーさんに教えてもらいました。かわいいイラストでわかりやすい!特に「18. 認可コードを用いてアクセストークンを不正に取得する攻撃と防御(Public Client 編)」と「05 OAuth認可コードフローの簡易的解説」の部分参考にさせてもらいました。ありがとうございます。
おわりに
OAuthについて自分が勉強したことをまとめてみました。OIDCやパスキーについても一緒にまとめたいと思っていましたが、OAuthだけでかなり長くなってしまったので区切りました。
なかなか複雑ですね。時間がたつと忘れそうなのでこうやって自分なりにまとめておくのは大事だなと思いました。
もし認識が間違っている箇所があれば、コメント等で教えていただけると幸いです。
Discussion