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のアクセストークンを取得し、
- (先にネタバレと弁明をしておくと、omniauth-line(及びその他のomniauth-* gem)自体の良し悪しではなく、omniauth-lineで実行したOAuthの手続きの結果を使って認証を行うことについて話している)
- アクセストークンを使ってユーザープロフィールエンドポイント(例えば
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
に来た情報を使ってよしなにやる
といったような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が降ってくる。
認証の実行に掛かるオーバーヘッド(トークン取ったりほどいたり)をクラウドに逃がせる利点もあるし、今どきのやり方としてはおすすめ出来ると思う。細かいやり方はどうかドキュメントを読んでほしい(ここで力尽きた)
以上、各自頑張ろう。
Disclaimer
OpenID Connect仕様を全部は読んでません!(Coreを全部とDiscoveryをちょっと)