📘

OAuth2.0 / 認可コードフロー / トークン / OAuthにおけるCSRF / 疑問点

2021/09/11に公開
2

自分用ノート。(この記事の内容をみて勉強しない方がいいです)

OAuth2.0とは

複数のWebサービスを連携して動作させる仕組み。

  • e.g.
    Googleログインできるサービス(notionとか)
    notionがコンシューマ
    Googleがサービスプロバイダ(リソースサーバ)にあたる

特徴

  • 従来のID、パスワード認証とは違ってトークンベースでの認証
    • notionでGoogleログインするときにID, パスワード入力しないよね(ログイン中なら)
    • コンシューマがサービスプロバイダ内のユーザデータを知る必要がない(当たり前だけどとても大切)
  • 上記のトークンにはサービスプロバイダに対する全ての権限が与えられている訳ではなく、必要な権限のみが与えられる
  • アクセストークンを取得する際には安全性の高いフローが利用される
    e.g.
    • 認可コードフロー(Webアプリケーションで用いられることが多い)
    • インプリシットフロー
    • クライアントクレデンシャルズフロー
    • リソースオーナーパスワードクレデンシャルズフロー
  • トークンには有効期限が設定される

コンシューマがサービスプロバイダの機能を扱う方法

コンシューマに必要となるのは「クライアントID」と「クライアントシークレット」
GoogleとかならGCPコンソールで取得できる。

アクセストークンが発行される流れ

ここではnotionのGoogleログインを例として認可コードフローについて考える。

  1. notionのログイン画面ではGoogleログインがあるので、ユーザはそれを選択する
  2. すると、Googleサイトにリダイレクトしログインを求められる(すでにログインしていたら求められない)
  3. 続いて、Googleサイト上でnotionに対して機能連携することに対する同意を求められる
    • ここで連携するサービスについてGoogleが判定するために クライアントIDが存在する
    • 具体的にはリダイレクト時のクエリパラメータにクライアントIDを含める
  4. 同意されるとGoogleは認可コードを発行しnotionサイトにリダイレクトさせる
    • この認可コードに関しても?code=xxという形でクエリパラメータに含められる
    • この認可コード自体は実際のアクセストークンではなく、後にアクセストークンを取得する際に必要となるもの
    • そのため有効期限が非常に短い
  5. notionサイトからGoogleに対して先程の認可コードを利用してアクセストークンを取得のリクエストを送る
    • Googleは認可コードの内容、有効期限に問題がなければアクセストークンを発行し、notionに返す
  6. これ以降notionがGoogleの機能を扱いたい時はアクセストークンを用いることで定められた権限内でGoogleの機能を扱うことができる
    • アクセストークンにも有効期限が存在し、期限が切れたらまた1からやり直す
    • 利用できる機能の権限は「クライアントID」、「クライアントシークレット」を取得する際に設定する

アクセストークンの中身について

実はOAuth2.0ではアクセストークンについての仕様は定めていない。

しかし一般的にアクセストークンには、情報を持たない一意な識別子としてのみ存在するものと
定められたルールによってユーザ情報をもとに生成されるものがある

情報を持たないアクセストークン

この形式のアクセストークンはサービスプロバイダがリソースの提供もアクセストークンの発行も同時に行う場合に利用できる。

  • サービスプロバイダはアクセストークンを発行する際に発行したアクセストークンを自身でも保存しておく
  • アクセストークンを認証する際には自身で保存したものと比較して一致するかを確かめれば良い

ユーザ情報などを含むアクセストークン

この形式のアクセストークンはリソースを提供するサービスプロバイダとアクセストークンの発行を行う認可サーバが別の場合に役に立つ
e.g.

  • クラウド上で利用できる認可サーバとしては「Azure Active Directory」などがある
    なお、実際のトークン形式の仕様として「JWT」などがよく用いられる

サービスプロバイダと認可サーバが別になると起こること

サービスプロバイダがアクセストークンについて知ることが出来なくなる
具体的にはアクセストークンを発行するのは認可サーバであるためサービスプロバイダは認可サーバ内のデータについて知ることはできない

そこでアクセストークンの生成にルールを持たせておくことでサービスプロバイダが認証する際に自身でアクセストークンを解析し、受け取ったアクセストークンが正しいかどうかを判定することができるようにする

JWT(JSON Web Token)

https://jwt.io/
JWTとは「ヘッダ」、「ペイロード」、「これら二つを電子署名したもの」の3つをピリオドで繋いで構成されるトークンの仕様である

サービスプロバイダが扱う際にはヘッダ、ペイロード、電子証明の復号方法についてを知っていれば、
前半2つの「ヘッダ」、「ペイロード」と最後の「電子署名されたデータ」の複合化後のデータが一致していることを確認し、認証とすることができる

リフレッシュトークン

ここまでの認可フロー、アクセストークンでも認証ロジックは動くが、それだとアクセストークンの期限切れ毎にいちいち認可フローを繰り返さなければならない

ただ実際にはアクセストークンの期限切れ時に認可フローを繰り返さなくても良いような仕組みが存在する
それがリフレッシュトークンである

リフレッシュトークンの動作

リフレッシュトークンはアクセストークンと同時タイミングで発行される
その際、リフレッシュトークンにはアクセストークンよりも長い有効期限を設定する

ユーザはアクセストークンを用いて認証を行うことになるが、その認証リクエスト時にリフレッシュトークンは含まれない

ではいつ使用するかというとアクセストークンの有効期限が切れたタイミングである

通常アクセストークンが切れた際にはもう一度アクセストークンを取得するために認可フローを繰り返さなければいけないが、
リフレッシュトークンを用いることで、認可フローなしにアクセストークンを再生成することができるのである

OAuth2.0におけるCSRF問題

OAuth2.0に存在する脆弱性としてCSRFが挙げられるが、一般的なWebアプリケーションとは偽装の方向が異なる

一般的なWebアプリケーションでのCSRF

e.g.

  1. 悪意のあるユーザが自身の口座に対して送金されるような特定サービスでのリクエストを含むURLを生成する
  2. 生成したURLを被害者となるユーザに対してメール等で送信する
  3. もし被害者ユーザが特定サービスにログイン中かつURLを踏んでしまった場合不正なリクエストが成立し、送金されてしまう

アプリケーションでの対策方法

いくつか存在するが有名なものとして、ワンタイムパスワードなどを利用するものがある。

ワンタイムパスワードのようなトークンをサーバ、クライアント間でやりとりしておいて、
クライアントがリクエストにそのトークンを含めることででサーバ側ではリクエストが正しいものであるかを判別できるようになる

OAuth2.0でのCSRF

上記で示したように一般的なアプリケーションでのCSRFでは悪意のあるユーザが被害者ユーザのアカウントを不正に利用するようなものであった
しかしOAuth2.0でのCSRFでは反対に被害者ユーザが意図せず悪意のあるユーザのアカウントを利用してしまうような問題が発生する

e.g.

よく挙げられる例として認可コードフローを用いたストレージサービスとの連携がある

  1. 悪意のあるユーザが対象のストレージサービスに認可コードが発行されるまでの動作を行う(アクセストークンを取得する手間)
    1. で取得した自身のアカウント用のアクセストークンを取得できるURLを被害者ユーザに送る
  2. 被害者ユーザが2. のURLをクリックし認可コードフローを完了させると被害者ユーザは悪意のあるユーザのアカウントでストレージサービスを利用できるようになる
  3. これ以降、被害者ユーザがストレージにアップロードしたファイルなどは悪意のあるユーザも閲覧可能な状態になる

アプリケーションでの対策方法

OAuth2.0がRFCで推奨している対策として認可コードを取得するリクエストにユーザ情報を示すstateパラメータを含めるものがある

stateパラメータを含めると認可コードを取得し、生成されるリダイレクト先のURLにもstateパラメータが含まれるようにある
そのURLを実際に踏んだユーザのセッション情報がもつstateとURLのstateパラメータを比較することで不正なアクセスを見つけ中断することができるようになる

その他

昔の自分の疑問

  • リフレッシュトークンの有効期限がすごく長いしこのトークンがあればアクセストークンの再生性もできて危険じゃない?
    • これについては単純にリフレッシュトークンの有無ケースを比べるよりもリフレッシュトークンを設定せずに有効期限の長いアクセストークンを設定した場合とリフレッシュトークンを用いた場合を比べるべき
    • アクセストークンと異なりリフレッシュトークンがネットワークを流れる機会はとても少ない
    • ユーザのログイン期間を安全に長く設定したいというユースケースを理解すべき

Discussion

SiketyanSiketyan

リフレッシュトークンの有効期限がすごく長いしこのトークンがあればアクセストークンの再生性もできて危険じゃない?

アクセストークンが JWT だったときのことを考えると分かりやすいです. JWT を検証するとき, DB 等へは基本的にアクセスせず,署名の検証で行うことが多いです.ここで,有効なアクセストークンを取得してからユーザを削除(退会)することを考えます.このとき DB 上のユーザ情報は削除されますが, JWT の有効期限内であればリソースへのアクセスができてしまいます(認可も JWT ベースで行うという仮定).これは DB 等へのアクセスなしでの検証をする以上回避できません.

そこで,リフレッシュトークンを使ってアクセストークンを再発行することを考えます.この再発行は何らかの API エンドポイントで行いますが,そのタイミングでユーザの存在やその権限を再確認します.そうして改めてアクセストークンを発行するのです.有効期限を短くすればするほど,この再確認までの時間が小さくなるのでリソースへのアクセス可能な時間は削減されます.ただし 0 ではないので,センシティブな操作を行う際には内部でリフレッシュを行うことも多いのではないかと思います.

リフレッシュトークンおよびアクセストークンの有効期限が存在する理由は,パスフレーズを定期的に変えるみたいな発想ではないことが分かります.もしそうなら,リフレッシュトークンの存在は無意味でしょう.

ichigoichigo

リフレッシュトークンおよびアクセストークンの有効期限が存在する理由は,パスフレーズを定期的に変えるみたいな発想ではないことが分かります

確かに JWT の検証を考えるととても腑に落ちました。
リフレッシュトークンっていう命名的にも無効なアクセストークンを排除していく仕組みは正しいように思えます。

ただ少し気になっているのが、私が記事に記述した

ここまでの認可フロー、アクセストークンでも認証ロジックは動くが、それだとアクセストークンの期限切れ毎にいちいち認可フローを繰り返さなければならない

ただ実際にはアクセストークンの期限切れ時に認可フローを繰り返さなくても良いような仕組みが存在する
それがリフレッシュトークンである

というユーザビリティ的な目的も同時に存在している気がしています。
例えば、あるサービスにログインして通常なら2週間後にセッションが切れて再ログインが必要になるところをユーザが継続してアクティブだった場合にセッション期間が延長されていくようなときです。
(この話はアクセストークンが JWT だった時に限定せずに話してます。ID / パスワード認証なども含みます。)

OAuth2.0 の記事を書きながら話の域がアクセストークン、リフレッシュトークンまで広がっている感は否めませんが気になりました。
まだまだ自分の勉強不足もあると思うので時間がある時に調べてみようと思います。