RailsでDeviseとOmniAuth を使い、通常フォームのユーザー登録・サインインとOAuth認証を併用する仕様を考えた

スポンサーリンク

Rails4 にて Devise でユーザー登録・ログイン認証・認可の機能を追加 しました。さらに OmniAuth を使って OAuth 認証も併用したいと思います。

OAuth 認証は、Facebook と Twitter を利用します。通常フォームでのユーザー登録・サインインと OAuth 認証を併用したいので、ちょっと頭を使って仕様をどうするか考えました。ググったけど、両者を併用する場合の情報を見つけられませんでした。

公式Wiki(OmniAuth: Overview)を参考にしましたが、併用する場合は、書いてある作業だけでは上手く行きません。

— 環境 —
rails 5.0.0.1
devise 4.2.0

【追記 2016/11/22】
Rails 5 + Devise 4 の環境でも同様の仕様で実装できます。
【追記ここまで】

【お知らせ】 英単語を画像イメージで楽に暗記できる辞書サイトを作りました。英語学習中の方は、ぜひご利用ください!
画像付き英語辞書 Imagict | 英単語をイメージで暗記
【開発記録】
英単語を画像イメージで暗記できる英語辞書サービスを作って公開しました
スポンサーリンク

— 記事初回公開時の環境 —
Rails 4.0.1
Devise 3.2.2

User モデルに OmniAuth で利用するカラムを追加するマイグレーション

Rails4 にて Devise でユーザー登録・ログイン認証・認可の機能を追加 の作業を終えた後、OmniAuth で使用するカラムを追加するマイグレーションを作成します。

生成されたマイグレーションファイルを確認して編集。

db/migrate/***_add_omniauth_columns_to_users.rb

Twitter/Facebook と複数の OAuth 認証を併用するため、OAuth の認証キー用に uid と provider でユニークな複合キーインデックスを作成しておきます。万が一 Twitter と Facebook の uid が同一の場合に provider が違えばユニークになるようにするため。こうしておけば、他の provider による OAuth 認証を追加した場合も大丈夫ですので。

のように、user を検索します。

続いてマイグレート。

データベース確認。

uid, provider, name の3つのカラムが追加されました。uid と provider でユニークな複合インデックスを作成したため、uid の Key は MUL(値が重複可能)となっています。

MySQL にログイン後上記のクエリで uid と provider で、ユニークな複合インデックスが作成されていることを確認できます。

通常フォームでのユーザー登録・サインインと OAuth 認証を併用する場合の問題点

通常フォームからのサインアップ・サインインと、Twitter/Facebook でのOAuth認証を併用する場合、いくつか問題があります。

uid, provider, name は OmniAuth を使った OAuth 認証から取得する情報となります。両者を併用する場合に問題となるのは、email カラム(ユニークキー)と、複合キーインデックスでユニークになっている uid + provider の組み合わせのカラム。

email カラムは通常フォームでの、ユーザー登録・サインイン時の認証用のユニークキーとして使われます。
uid + provider カラムは OAuth 認証での、ユニークな複合インデックスとして使われます。

通常のフォームでのユーザー登録・サインインでは、以下の事情を考慮しないといけない。

・OAuth 認証による uid + provider はユニーク制約なので、通常フォームからのユーザー登録の場合も、少なくとも uid は何かしらの値を保存しないといけない。provider を空にするなら、適当にランダム生成した一意である uid を保存する。

そして、Twitter/Facebook の OAuth 認証では以下の制約があります。

・TwitterのOAuth 認証では、Email情報を取得できない。

・FacebookのOAuth認証では、Email情報を取得できるが、すでに通常のフォームからユーザーが登録されていた場合、Emailが重複する可能性がある。

・逆に、ユーザーがFacebookでOAuth認証済みの場合、新たに通常フォームからのユーザー登録時に、Emailが重複する可能性がある。

以上を考慮して、通常のフォームからのユーザー登録・ログインとOAuth認証によるログインを併用するための仕様を、以下の4通り考えました。

仕様案1: ユニークインデックスは上述の通りで、ダミーのuid, Emailアドレスを利用する方法

・OAuth認証を使わず通常の登録フォームからユーザー登録でサインインさせる場合、適当な一意のランダムな値をusersテーブルのuidカラムに格納しておく(provider は空にする、default “”)。uidの格納は、通常フォームからのユーザー登録時に行いたいので、サインアップ時に独自に追加する。

・TwitterでのOAuth認証の場合、ダミー用の一意でかつ通常ありえないEmailアドレスをランダムに生成して、usersテーブルのemailカラムに格納。OAuth認証によるセッション開始時に格納する。

・FacebookでのOAuth認証の場合に取得したEmailアドレスが、通常フォームからのユーザー登録によりすでにレコードに存在している場合は、FacebookでのOAuth認証を許可しない。

・逆に、通常フォームからのサインアップ時のEmailアドレスが、FacebookでのOAuth認証によりすでにレコードに存在している場合は、通常フォームからのサインアップを許可しない。

あとこの実装の場合、email カラムの取り扱い時には注意が必要。ランダム生成するTwitter OAuth認証時のEmailはでたらめなので、email カラム出力専用のヘルパーを作る。

Twitter で OAuth 認証したユーザーが、その後アカウント情報を正しい自分のEmailに変更した場合など、もう少し考えなければならないことがある。これは、最初にランダム生成するEmailアドレスの@部分以降を 〜@example.com にしておいて、example.com じゃないドメインになっていたら、ユーザーが自分のEmailアドレスに変更済みなどとチェックできる。(example.com は例示で使われる予約ドメイン。)

仕様案2: email と uid の複合インデックスにユニークキーを設定する

マイグレーションを以下のように設定。

いったん、email と uid + provider のインデックスを削除して、email と uid で複合インデックスを作成し、それをユニークキーに設定する。

この場合、通常フォームでのユーザーのサインイン時には、uid が格納されているレコードは検索対象にしないようにする。逆に OAuth 認証時には、uid が空っぽのレコードは検索対象にしない、などといったモデル検索時の細かい操作が必要になります。DBはすっきりしそうだけど、実装側が色々と面倒くさそうなので却下。

仕様案3: email に代わる認証キーを用意して、email をユニークキーにしない

以下のページに書いてありますが、email フィールド以外を認証キーに使うこともできるようです。デフォルトでは、email フィールドが認証用のユニークキー。

How To: Allow users to sign in with something other than their email address

例えば、

などとして、userid をマイグレーションで追加してユニークキーに設定する。emailアドレスを認証用のユニークキーにしたくない場合は、この方法が使えそう。ただし、この場合 Twitter/Facebook による OAuth 認証時に、仕様案1で email フィールドに対して考えたのと同じことを userid に対して行う必要がある。

仕様案4: 通常の User と OmniAuth 認証による OmniUser とモデルを2つ作成する

通常のフォームからのサインアップ・サインインユーザーのモデルを User とし、OmniAuth認証のユーザーは OmniUser などとしてモデルを区別する方法。DBはすっきりしそうだけど、コード実装はごちゃごちゃしそう。あと Devise で複数のモデルを使う方法がよく分かっていないのでパスします。

どれを採用するか?と対象カラムに値を保存する実装方法を考えた

仕様案2,4のほうが、DBテーブルは綺麗になりそうですが、コントローラー・モデルの実装は面倒くさそうです。仕様案3は結局やることは仕様案1と同じことになりそうですし、今回は email をデフォルトのまま認証用のユニークキーとして使いたい。仕様案1は、DBテーブルの email, uid のレコードがちょっと汚いことになりそうですけど、コード実装側は楽そうです。

ということで仕様案1で進めることにします。仕様案1の実装方法を考えたところ以下の方法でできそう。

通常サインアップ時の uid フィールド設定の実装を考えた

実装すべき仕様は以下。(仕様案1のもの)

・OAuth認証を使わず通常の登録フォームからユーザー登録+サインインさせる場合、適当な一意のランダムな値をusersテーブルのuidカラムに格納しておく(provider は空にする、default “”)。uidの格納は、通常フォームからのユーザー登録時に行いたいので、サインアップ時に独自に追加する。

※以下は参考非推奨です。
上記項目の実装は、Rails の認証プラグイン Devise での Strong Parameters について | EasyRamble に書いた、追加のパラメータを許可する方法で実装できます。ユーザー登録フォームに uid の hidden field を含ませます。uid を strong parameters の機能で許可し、また uid に対するモデルでの validation も追加します。
※参考非推奨ここまで。

【追記:2013/12/14】
当初、上記打ち消し線の実装(参考非推奨)を考えたのだけどやはり、フォームを由来せずに Devise::RegistrationsController の create アクション時に、uid を設定したほうが良いです。Devise のソースを読んで調べていたら Devise::RegistrationsController の build_resource(hash=nil) アクションをオーバーライドする方法を見つけました。ということで、それを継承した Users::RegistrationsController < Devise::RegistrationsController で build_resource(hash=nil) をオーバーライドして実装する方法で進めます。 How To: Override build_resource({})
Devise > Custom fields during registration – Google グループ

OAuth 認証時の email フィールド設定の実装を考えた

実装すべき仕様は以下。(仕様案1のもの)

・TwitterでのOAuth認証の場合、ダミー用の一意でかつ通常ありえないEmailアドレスをランダムに生成して、usersテーブルのemailカラムに格納。OAuth認証によるセッション開始時に格納する。

・FacebookでのOAuth認証の場合に取得したEmailアドレスが、通常フォームからのユーザー登録によりすでにレコードに存在している場合は、FacebookでのOAuth認証を許可しない。

・逆に、通常フォームからのサインアップ時のEmailアドレスが、FacebookでのOAuth認証によりすでにレコードに存在している場合は、通常フォームからのサインアップを許可しない。

上記項目を実装するには、OmniAuth: Overview の公式Wikiを参考にして、Userモデル(app/models/user.rb)で User.find_for_facebook_oauth クラスメソッドを定義する。

Emailの重複があった場合は、email カラムのユニークキーによる制約と Devise により面倒を見てくれるみたいです。自分の Twitter/Facebook アカウントで試してみました。まあ email がユニークな認証用キーになるので当たり前でしょうけど。

以上で仕様を考えるのは終了、頭が疲れた。具体的な実装コードは次回のエントリーで。

他に考えられる仕様

ググって見つけたのが以下のページ。

[Rails] OmniAuth + Device で認証する① : ノンプログラマーブログ

上のページに書いてあるように、通常のフォームでの登録時に Twitter/Facebook のアカウントを必須にするという方法もある。今回は、Twitter/Facebook のアカウントを持っていない人も登録できるようにしたかったので、この仕様については考えませんでした。

スポンサーリンク
パーフェクト Ruby on Rails は、最近読んだ Rails 本の中では一番役に立った本です。Chef や Capistrano など Rails と共によく使用される技術にも触れてあります。Ruby on Rails 4 アプリケーションプログラミングは、入門的な内容で Rails の機能全体を網羅されています。
 
スポンサーリンク

Leave Your Message!