今からRailsでSNSログインを実装したい人へ
TL;DR;
- omniauth-* gemを使ったログインの実装がOpenID Connectに準拠しているとは限らない
- OpenID Connect準拠なログインを実装しよう
- あるいは準拠している実装を使おう(例えばAmazon Cognitoとか)
- Deviseについては知らない
前置き: SNSログインとネット上の情報について
emailとpasswordを二度入力させている間に貴重な新規ユーザーを離脱させてしまう代わりに、LINEやFacebookの見慣れたUIとホカホカに温まった(line.meやfacebook.comの)cookieを使ってポチポチサクサクと認証に必要な情報を集めたい。
そんな要望を実現するために精一杯頑張るのもエンジニアの仕事です。(スタートアップにいればそんな要望も実装側の都合も頑張りもまるっと自分事ってこともあるでしょう。)
つい最近私にもそんな機能を提供する必要が生じ、ひとまず手法を一通り調べてみようと"rails line login"でググってみました。
すると、(シークレットセッションで検索しなおした場合の少なくとも)上位5件は、
- omniauth-lineを使ってLINEのアクセストークンを取得し、
- アクセストークンを使ってユーザープロフィールエンドポイント(例えば v2/profile)からuserIdを取得し、
- 以上によりユーザーを「#{userId}さん」として認証する!(簡単!)
といった手法を紹介するものでした。
一方で、「OAuth使ってログイン実装しちゃまずいでしょ」という風のうわさも耳にします。(恥ずかしながら私の認識もその程度でした。)
このような状況で、いざ自分で、一般ユーザーに公開するサービスに、LINEログインを実装する、という必要に迫られてみるとなかなかに心細い。
そこで、OAuthだとかOIDCだとかLINEでログインだとかについて、それを認証機構としたRailsアプリケーションを一般ユーザー向けに公開するのに十分な自信を持てる程度まで調べました。以下は調べた結果得られた知識のうち、ネット上の情報とのダイバージェンスが大きいものを抜粋してまとめます。
omniauth-* gemを使った認証(≒ログイン)が安全だとは限らない
omniauthは、実装をプラグイン出来るRack middlewareで、プログラミング言語のようなものです。
冗談はさておき、omniauthはREADME.md冒頭によれば、複数の参加者(例えばあなたのサーバーとLINE)による 認証(大事) をいい感じにするフレームワークとして動作するRack middlewareです。 つまり、omniauthのstrategyというプラグイン機構を使い、異なる様々なRailsサーバー外のサービス(LINE, Facebook, GitHub等)と連携して認証を行う方法を、omniauthを使う開発者側からは幾分透過的に扱えるといったものです。(各種デベロッパーコンソールからIDとSecret持ってきてconfig/initializer/omniauth-*.rbにぶち込んでウェーイ、が出来る)
例えば、LINEでログイン(omniauth-line)の場合、 - ログインしたい(例えば GET /loginに来た)ユーザーをLINEの認証エンドポイント(https://access.line.me/oauth2/v2.1/authorize)にリダイレクトし、 - 結果得られたauthorization codeを利用してアクセストークンを取得し、 - ユーザープロファイルエンドポイント(v2/profile)から取得したユーザー情報(uid, email等)を添えてログイン実行エンドポイント(例えば POST /login)にリダイレクトして - POST /loginに来た情報を使ってRailsがよしなにやる
ようなstrategyが実装してあり、ユーザーはただ『omniauth-lineを使う』という設定をするだけで、GET /login は見慣れたLINEの何かしらログインを行う雰囲気の画面にリダイレクトされ、気がつくとPOST /loginを通してユーザーのID(A1B2C3)が取得でき、それをよしなにやって(ユーザーを作ってセッションを作って…)に保存して「A1B2C3さんようこそ」となることが出来ます。便利。
だがしかし、風のうわさではこれはまずいらしい。それは単純には、
「『OAuth{,2}に基づいた 認可 手続きで得たトークンを使ってユーザープロファイルエンドポイント越しにuserIdを取得した(出来た)』という事実は『"userIdさん"として 認証 出来た」ということにはならない」
ということです。
意味が違うからダメというだけで、神経質なエンジニアだけが気にする問題だ、という認識はやや危険であり、OAuth{,2}が認可(〇〇してよいかを決める)を行うためのものである以上は、それに基づいた実装が認証に必要なスペックを満たしているという筋合いも保証もないわけです。実際にこのような認証を行っていたことで生じた脆弱性(OAuthのせいではないが)があり、それをサポートするためにOAuthの拡張が(何度か)行われました 。
どうしてもOAuthの仕組みと似たような形で認証を行いたい、ということであれば、OpenID Connectに従うべきです。これは紛れもなく認証のための標準化された仕様であり、OAuthの仕組みを利用し、その上にいくつかのスペックを追加することで認証のスペックとして成立させています。
細かい説明は参考文献に任せるとして、簡単には、OAuthによってアクセストークンを得る際にIDトークンが併せて返却され、その中身のsub
という要素がユーザーの(IdP(LINE等)側でローカルに一意な、再利用されない)識別子であるということになります。そしてこれを以て(IdPへの信頼を前提として)、ユーザーを認証することが出来ます。
さてomniauthに話題を戻すと、omniauth-lineもomniauth-facebookもomniauth-twitterも、ユーザー情報エンドポイントのような場所からidとかuserIdとかを持ってきて、uid(つまりこの名前で認証をしたいということだ)として認めています。
これはやはりOpenID Connectに準拠しておらず、認証の方法としてOpenID Connectの名のもとに正しいとは言えません。
一般ユーザーに届けるサービスを作るのであれば、外部サービスを用いたログイン機能はOpenID Connectに準拠した認証を以て行うようにしましょう。
(念の為ですが、omniauth-*の実装が悪いと言いたいのではなく、これを認証機構として取り入れてしまうことがまずくないとはいえない、だからみんな自分でちゃんと気をつけよう!ということです。(しかし、omniauthが『複数の参加者による認証のフレームワーク』なのだとすれば、その下にOAuth的なstrategyがいろいろとブラブラとぶら下がっているのは如何なものか…))
準拠した実装をしよう
OpenID Connectに準拠したLINEログインをRails上で実装するのに手っ取り早いのは、OmniAuth::Strategies::OAuth2 を継承した自前のstrategyを書いてやることでしょう。
ちなみに、試しに読んだり書いたりしてみた個人的な感想では、OpenID Connectに準拠したクライアント側の実装はそんなに難しくはありません(IdP側は大変そう)。OAuth部分ができていれば、
- Authorization Scopeにopenidを追加して、
- アクセストークンと一緒に返ってきたid_tokenをjwtデコードして
- jwtのsignatureを検証して
- issとaudとexpを検証して
- subをuidとする
というだけです(と思う…)。
準拠した実装を使おう(例えばAmazon Cognitoとか)
それも不安(あるいは面倒)ということであれば、準拠した実装を使うことにしましょう。
Amazon CognitoはOpenID Connectに準拠したIdPとの連携をサポートしています。当然Cognito自体もOpenID Connectに準拠しており、我々はCognitoを設定して、「認証して」だとか「IDをオクレ」だとかいうだけで、認証情報として認めることの出来るIDが降ってきます。
認証の実行に掛かるオーバーヘッド(トークン取ったりほどいたり)をクラウドに逃がせる利点もあるし、今どきのやり方としてはおすすめ出来ると思います。細かいやり方はどうかドキュメントにおまかせします。(力尽きた)
以上、各自頑張りましょう。