話題の記事

OAuth 認証を真面目に考える

認証とログインは別と捉えることで、その状態の維持に着目してその手法を考えましょう。OAuth 認証は2018年現在、必要悪としか言いようがありません。ぐぬぬ。ソーシャルログインであっても、Web サーバーで自前のセッションを管理してください。

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

永遠の生魚おじさん、都元です。学生時代、ロックバンド Harem ScaremMood Swings (1993年) っていうアルバムが好きでよく聞いてたんですけど、この前 Google Play Music で検索してみたらMood Swings II (2013年) っていうアルバムが出ていることに気づきました。なんと曲目が全て一緒で、妙なアレンジのリミックスになっていない、まさに「オリジナルのまま20年磨き続けたらこうなりました」みたいな仕上がりが最高でした。聴き比べて楽しんでいます。

さて、弊社は本日を最終営業日として、これから冬季休業となります。 今年も一年、どうもありがとうございました。というわけで書き納め三本締めの三本目。

認証とログイン

さて、認証認可おじさんとして OAuth やログインについて Harem Scarem を聞きながら考える日々を過ごしてています。で、これは世の中の定説というわけではなく筆者都元の解釈でしかないのですが、細かく言えば認証ログインは別のものであると捉えています。

  • 一般的な認証というのは、「システムを操作している人物が確かにこの ID の持ち主であると確認すること」を指します。あえて言えば、認証というのはその「瞬間」のことを指します。
  • 一方でログインは、「その結果をいつまで信頼するのかというポリシーを適用し、認証に期間を与えること」を指します。

Web アプリにおける普通のログインの実現方法

Web アプリケーションにおける普通のログインでは、まぁ ID とパスワードを確認することによって 認証 (瞬間) を行います。これができ次第、「セッション」という cookie ベースの仕組みを使ってログイン (期間)を実現します。

これは各人の肌感覚かもしれませんが、セッションは比較的サーバーのリソースを消費するため、あまり長い期間を設定することはおすすめできません。長くても数時間程度を上限にしておきたいと思っています。自分の理想としては最後の操作から 30 分くらいかな。このセッションが期限切れを起こした場合、ログアウトとみなして再度認証を求めます。

では、数時間を超えてログイン状態を維持したい場合はどうしましょうか。よくあるのはログインフォームの下についている「次回は自動でログインする」とか「ログインしたままにする」「Remember me」というチェックボックスです。これは session ID とは別の比較的長寿な cookie を発行し、その cookie を持ったブラウザで再訪した場合はログイン画面を省略して自動的にログインセッションを再作成 (復元) するといった仕組みです。

禁断の? OAuth 認証

OAuth は認証のためのプロトコルではない、と各所で口酸っぱく言われているなか、今日もどこかで OAuth によるソーシャルログインができるアプリケーションが生まれています。

OAuth 認証の何が問題なのかは各所でよく言われていることなのでひとまず割愛します。正しく外部連携認証を実装するのであれば OpenID Connect を使いなさい、と言うのもよく言われることです。では OAuth によるソーシャルログインを実装してはいけないのか?と言うとそう言い切ることもできず、色々状況を自覚した上であれば、まぁ、世情に鑑みて許されざる悪事というわけでもない、といったところでしょう。

要は、だいたい Twitter か Facebook でログインしたい訳ですよね (暴論)。でも、どちらも OpenID Connect に対応してない (多少語弊がありますが、ここは雑に言い切ってます) んです。じゃあもう OAuth 認証するしかないじゃないですか。

Web アプリにおける OAuth ログインの実現方法

Facebook などの OAuth プロバイダーに対して所定の手続きを踏むと、Web アプリはアクセストークン (以下、AT) を手に入れます。この AT を使って、Facebook 等のプロフィール API 的なものを叩けば、その人の ID が確定し、認証 (瞬間)ができます。本来 OAuth プロバイダーは認証を提供するわけではないので ID プロバイダー (IdP) とは呼ばないのですが、ここでは認証として使っているので IdP と呼んでしまうことにします。

次に考えるべきはログイン (期間)、つまりいつまでこの認証を有効とするかです。まぁ AT には有効期限がありますので、現実的にはこれをログイン状態の期限と解釈するのが一般的ではありましょう。(注: あとで軽くちゃぶ台返しします)

ただし。AT は認証のためのトークンではないので、この有効期限がログインの有効期間である、と解釈するのはアプリの勝手です。AT の有効期限よりも前にログアウトしても良いですし、(もし有効な AT が手元になくても、必要最低限のサービス提供が続けられるのならば) AT の有効期限が訪れてもログイン状態を継続する、というのも自由です。

そして。最後に考えなければならないのが、毎回のアクセス時に「今ログイン状態であるかどうか」を判断する方法です。今回、お話したいのは、ココです。注目ですよ、ちゃんと拾ってください。

結論から言います。普通のログインと同じように、認証できた段階でセッション ID cookie を発行してセッションを確立してください。そして、そのセッションの有効期限を AT の有効期限と一致するように設定しましょう。

決して、自前のセッション管理を放棄しないでください。セッション管理を OAuth サーバーに押し付けないでください。リクエストの度に AT がまだ有効かどうかを確かめるためだけにプロフィール API を叩きに行ったりしないでください。お願いです。許してください。AT は、リソースにアクセスするための鍵であって、何かの有効性を管理するためのものではありません…。

いやまぁ… GAFA レベルのサーバーと運用体制が揃ってるのであれば誤差な話かもしれないのですが。もしプロジェクト内部で OAuth サーバーを運用するような環境では、まずコイツから過負荷で死んでいくことになります。

モバイルアプリにおける OAuth ログインの実現方法

次にモバイルアプリケーションについて考えてみます。

ネイティブなモバイルアプリケーションでは、一般的に 「セッション」という概念がありません。サーバーサイドに存在するこのアプリ固有の機能を提供する API (以下、独自 API) へのアクセスにあたっては、大抵リクエスト毎に何らかのクレデンシャル (認証情報) を送信します。つまり、API アクセスにはログイン (期間) がなく、認証 (瞬間) だけを毎回行っているとも言えます。

ただしこれはモバイルアプリと API サーバー間の話であり、エンドユーザー (人間) とモバイルアプリの間には、やはりログイン (期間) が存在します。

ここは Web アプリと同様で、アプリの勝手として AT の有効期限がログインの有効期間である、と解釈しても構いません。AT の有効期限よりも前にログアウトしても良いですし、期限以降もログイン状態を続けても構いません。というかむしろ、エンドユーザーとモバイルアプリ間のログイン (期間) は無期限であるのが理想ではないでしょうか。みなさん、スマホの Twitter アプリで最後にログインしたのはいつですか? 初回起動時以来ログインしてないんじゃないですか? …この辺りは最後に議論しましょう。

モバイルアプリ独自 API に対するアクセス制御

さて先程触れた通り、モバイルアプリには対になるサーバーサイドがあり、独自 API へのアクセスには何らかのクレデンシャルが必要です。IdP が提供する API に対するアクセスであれば OAuth で手に入れた AT で構わないのですが、このような独自 API に対するクレデンシャルには何を使いましょうか?

まず思いつくのは IdP から受け取った AT そのものを、自分たちの API の AT として流用するというパターンです。この AT を受け取った独自 API は、毎回 IdP のプロフィール API を叩いて有効性を確認することになります。これは IdP に結構ものすごい負荷を掛けることになりますが…まぁ、GAFA レベルの IdP であればそれも良いのかもしれません。このパターンの是非は正直よくわかりません。もしかしたら規約で禁止されていたりレートリミット等の制限があるかもしれませんので、その辺りはよく調査検証の上で設計しましょう。

このパターンを採用した場合、AT の有効期限とログインの有効期限は一致させざるを得ません。もしこれを別物として扱いたいのであれば、別の手を取る必要があります。

例えば IdP から受け取った AT を入力として、独自 API 用のクレデンシャルに引き換える API を用意しておきます。IdP に対する AT の検証は、引き換えの時に一回だけ行います。こうすれば、独自 API 用クレデンシャルの有効期限は自由にコントロールできますし、IdP に無駄な負荷をかけてしまうこともありません。

自前 IdP の宿命

一方で、プロジェクト内部で OAuth サーバーを運用するような環境だった場合は、前述のようなクレデンシャルの引き換えはナンセンスです。というかそもそも自前の OAuth サーバーが発行した AT というのは、独自 API のリソースアクセスを許可するための鍵ですよね。

この場合、独自 API から自前 OAuth サーバーに対して「この AT は正しいか?」という問い合わせの集中砲火を受ける (アーティファクトトークンの場合) ことになりますが、それは宿命です。というかそのための自前 OAuth サーバーじゃないですか。

ただ現実問題として負荷が集中しまくるのは確実なので、独自 API 側である程度のキャッシュを持つなど、トレードオフを見ながら対策を講じてください。

ログインと AT の有効期限は一致させないほうがいい?

ここまで、ログインと AT の期限は一致させておけばいいという論調で来ましたが。

繰り返し述べた通り、そもそも (Web, モバイルにかかわらず) アプリのログイン有効期限と、リソースへのアクセスが許される AT の期限というのは無関係の概念です。モバイルアプリで行き着いた「無期限ログインが理想」というのが、如実にそれを表しています。

アプリがログイン切れだと判断した場合、AT の有効期限が残っていたとしても OAuth のフローを再度回せばいいのです。逆に、アプリがまだログインを継続したいと判断するのであれば、AT の有効期限が切れていたとしても OAuth のフローを回す必要はありません。

ただし、OAuth 本来の目的である「リソースへのアクセス」がサービス提供のために必要なのであれば、AT の有効期限切れに伴って再度 OAuth のフローを回す必要が出てきます。でも、みんな、Facebook API を叩きたくて OAuth しているわけではなく、ただ認証したいだけで OAuth してるんですよね?(失礼)

まとめ

  • 認証とログインは別と捉えることで、その状態の維持に着目してその手法を考えましょう。
  • OAuth 認証は2018年現在、必要悪としか言いようがありません。ぐぬぬ。
  • ソーシャルログインであっても、Web サーバーで自前のセッションを管理してください。(最重要)
  • 独自 API のアクセス制御の責任を外部の IdP に負わせるのは少々理不尽です。規約などに注意してください。
  • 独自 API のアクセス制御の責任を内部の OAuth サーバーが負うのは当然です。諦めてください。
  • ログインの有効期限と AT の有効期限は、雑に考えれば一致させておくのが無難です。
  • ただし、アプリの要件やポリシーに応じて (そして自分でリスクをコントロールする前提と覚悟をもって)、それを乖離させるのもまた一つの作戦です。

みなさま、良いお年をお迎えください。