解せぬ日記

雑な話をする

Web APIにおける認可の現状確認 2019 春

最近Web APIの認可について思いを馳せる機会があったのだけど、どの方法で認可を実装するのがスタンダードなのか、そもそもそんなものは最初からないのか、よく分からなくなったので自分なりの考えをまとめることにした。

ファーストパーティのアプリが使うWeb API(プライベートなWeb API)の場合はセッションを使うケースもあるだろうが、セッションを使うケースでは悩むことも少なかったので、ここではアクセストークンを用いた認可にフォーカスして考えてみたい。ケースとしてはユーザー毎にアクセストークンが発行され、Browser-BasedアプリやNativeアプリからWeb APIが叩かれることを想定している。

追記 2019/03/04

認証や認可周りについてよく拝見させてもらっている方にブログで言及していただきました。合わせて読むことをお勧めします。

ritou.hatenablog.com

特にResource Owner Password Credentialsの説明はよく目を通してください。僕が混同している可能性があります。頭の中で整理できたら記事を訂正したいと思ってますが、まだできておらずそのままになっています。すいません。

認証/認可の現状について

普段、開発をしていると色々なサービスがサードパーティへ公開しているWeb API(パブリックなWeb API)をよくさわる都合上、OAuth 2.0を用いた認可の実装、OpenID Connect 1.0を用いた認証/認可の実装の様子を目にすることが多い。それぞれOAuth2とOpenID Connectとここからは呼ぶことにするが、パブリックなWeb APIを作るシナリオを考えている場合はOAuth2、または OpenID Connectのプロバイダになるのがスタンダードだと認識している。ただ多くのサービスで最初に用意するのはプライベートなWeb APIだろう。

ここで一つの疑問が湧いてきた。プライベートなWeb APIの認可はどう実装していくのがスタンダードなのか?ファーストパーティのアプリのためだけにOAuth2やOpenID Connectを実装するのか?

例えばSPAのようなBroswer-Basedアプリに対してのOAuth2について書かれたDraftには以下のようなことが書かれている。

While OAuth and OpenID Connect were initially created to allow third-party applications to access an API on behalf of a user, they have both proven to be useful in a first-party scenario as well.

https://tools.ietf.org/html/draft-ietf-oauth-browser-based-apps-00#section-5

Draftでかつ00ではあるが、OAuth2とOpenID Connectがファーストパーティにとっても有用であるという主張は分かるし、現にAuth0などでもOAuth2を使うことで自分たちのアプリケーションへAPIアクセスを提供できるとドキュメントで言及されている。

auth0.com

その一方で、それなりの割合で独自の認可方式を実装していたりするのを見かける。僕自身も独自の認可方式を実装したことがある。OAuth2の仕様を調べれば調べるほどプロバイダの実装が大変であることが分かり、独自の認可方式を用意するほうが楽なように思えたからだった。そしてOAuth2を実装するライブラリが揃ってきたとは言え、2019年現在でも実装は楽ではないなと思っている。だからこそ認証/認可のマネージドサービスが生まれているんだろう。

独自の認可方式を実装することは楽なのか?

上記で述べたようにOAuth2を実装することはそれなりに大変だなと思ったので、独自の認可方式を実装するならどうなるのかということを改めて考えてみた。例えば以下のようなものがパッと思いつくプリミティブな実装だろう。

  • OAuth2で言うところのResource Owner Password CredentialsのようなSomethingでusername/passwordを渡すとアクセストークンのみが払い出される

OAuth2のResource Owner Password Credentialsは以下のようなフローでアクセストークンが発行される。

     +----------+
     | Resource |
     |  Owner   |
     |          |
     +----------+
          v
          |    Resource Owner
         (A) Password Credentials
          |
          v
     +---------+                                  +---------------+
     |         |>--(B)---- Resource Owner ------->|               |
     |         |         Password Credentials     | Authorization |
     | Client  |                                  |     Server    |
     |         |<--(C)---- Access Token ---------<|               |
     |         |    (w/ Optional Refresh Token)   |               |
     +---------+                                  +---------------+

要はこれと同じようなものを実装する。これはめちゃめちゃ実装が楽だと言える。ただOAuth2のResource Owner Password Credentialsの説明を眺めていると、どうやらこれだけでは足りないということが分かってくる。セキュリティ的な問題をクリアするためには以下のようなものを実装したほうがいいだろう。

さらに、Browser-Basedアプリでアクセストークンを使う場合は、セキュアにログイン状態を維持するための仕組みが必要になる。例えばユーザーの認証を通してアクセストークンを更新する仕組みだ。OpenID Connectを実装してSilent Authenticationのような仕組みを用意したり、それに近しい仕組みが必要になる。

auth0.com

これはBrowser-BasedアプリがPublicなクライアントなのでリフレッシュトークンは発行すべきではないからだ。そろそろ役目を終えそうなImplicitでもリフレッシュトークンは発行すべきではなく、もしどうしても発行したいなら頻繁にリフレッシュトークンを変えるような方法を取るように書いてある。もっともファーストパーティのBrowser-Basedアプリではセッションを使うという方法も取れるので必須ではないと思う。

この他に実装上の問題点も分かってくる。

  • ファーストパーティのクライアントのなりすましに気づくポイントが少ないことにどう対応するか
  • 自然と中間者攻撃の様相を呈していないか
  • 独自の実装で生まれうる脆弱性にどう気づくか

これらを考慮しながらアクセストークンの期限、リフレッシュトークン、クライアントの特定方法まで実装してくるとほとんどOAuth2の実装に足を踏み入れ始めているのではないかと思う。これに加えて独自の実装で生まれる脆弱性についても思いを馳せる必要があるが、それを考慮しきれるのかという不安がとにかくヤバイ。Bearトークンを採用しようがJWTを採用しようが考えないといけないことだと思っていて、JWT便利でしょといってリフレッシュの仕組みも用意せず、生存期間の長いアクセストークンを用意しているとかだとかなり問題があるのではないかと思っている。つまり独自の認可方法を用意するのは全然楽ではない。認可それ自体がセキュリティとセットで意味をなすので、そもそも楽な方法などなく歯を食いしばって実装する類のものなのだろう。

ちなみに、OAuth2のRFC6749の中でResource Owner Password Credentialsは以下のように説明されている。

The credentials should only be used when there is a high degree of trust between the resource owner and the client (e.g., the client is part of the device operating system or a highly privileged application), and when other authorization grant types are not available (such as an authorization code).

https://tools.ietf.org/html/rfc6749#section-1.3.3

大事なのは「他のGrantが利用できないとき」という部分で、これはAuthorization CodeがBroswer-BasedなアプリやNativeアプリに対してほとんど場合で適用できることを考えると選択しないほうが好ましいということだと思うし、Nativeアプリに対するOAuth2について書かれたBCP212はAuthorization Code前提で書かれている。

https://tools.ietf.org/html/bcp212#section-4.1

したがって簡単に実装できると思っていたResource Owner Password Credentials方式はOAuth2では限定利用に留める方向になっていて、独自に実装する際はこの限定利用の壁を壊す俺の最強武器を用意する必要があり、セキュリティに明るくない僕には無理ゲーな上に新規プロダクトのコアにほとんどなりえない認証/認可に時間を吸われる結果になるのではという思いに至った。

まとめ

ここまで考えてみると、実装はそれなりに大変だが認可のフローや脆弱性をたくさんの目を使って議論しているOAuth2やOpenID Connectなら仕様は既にあるし、プロバイダになるにあたって気をつけるべきことは論じられているので、ファーストパーティが使うWeb APIだとしてもOAuth2やOpenID Connectを採用しつつ、Authorization CodeやPKCEなど必要最低限の実装を持ったプロバイダを実装するのが2019年春現在考えうる最良の認可なのではないかと思う。あくまで最初に想定しているケースの場合なので、Confidentialなクライアントだけしか存在しない場合だったりするとClient Credentialsのような仕組みで十分なこともあり得るだろう。

途中で湧いた疑問

ちなみにOAuth2を採用してAuthorization Codeを使う場合、ファーストパーティのアプリに対して認可の画面が出てくるのはもしかしたら少し違和感があるかもしれない。ファーストパーティのアプリのみ認可の画面をスキップするとかという話も耳にしたことがあるのだけど、正しい方法はちょっと分からないので知りたい。Client IDが漏れる可能性を考えても、Redirect URIの完全一致を検証することにより、スキップしても問題ないような気がするがどうなのだろうか?

最後に

僕は認証/認可に明るいわけではないので、間違っていたりおかしい箇所についてはぜひアドバイスをくださいと思っている。OAuth2やOpenID Connectを実装しようという結論にしてもめちゃめちゃ自信があって言ってるわけではないので、より良い方法があるならぜひ知りたい。サードパーティが使うWeb APIならOAuth2なり実装するでしょという感じだと思うのだが、ファーストパーティに関してもOAuth2で実装するのが当たり前すぎるのかあまり言及されてない気もする。たぶん当たり前なのかも知れない。あるいは既に当たり前な別の方法があるのかもしれない。基本的に最初から認証/認可系のマネージドサービス使っててハッピーですとか。正直なところ、認証/認可の話をブログに書いたりするのは個人的にめちゃめちゃ勇気が必要だった。知り合いしか見てないと分かっていても。