- 更新日: 2016年11月22日
- Devise
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 の環境でも同様の仕様で実装できます。
【追記ここまで】
— 記事初回公開時の環境 —
Rails 4.0.1
Devise 3.2.2
User モデルに OmniAuth で利用するカラムを追加するマイグレーション
Rails4 にて Devise でユーザー登録・ログイン認証・認可の機能を追加 の作業を終えた後、OmniAuth で使用するカラムを追加するマイグレーションを作成します。
1 2 3 |
$ bundle exec rails generate migration AddOmniauthColumnsToUsers provider uid name |
生成されたマイグレーションファイルを確認して編集。
db/migrate/***_add_omniauth_columns_to_users.rb
1 2 3 4 5 6 7 8 9 |
class AddOmniauthColumnsToUsers < ActiveRecord::Migration def change add_column :users, :uid, :string, null: false, default: "" add_column :users, :provider, :string, null: false, default: "" add_column :users, :name, :string add_index :users, [:uid, :provider], unique: true end end |
Twitter/Facebook と複数の OAuth 認証を併用するため、OAuth の認証キー用に uid と provider でユニークな複合キーインデックスを作成しておきます。万が一 Twitter と Facebook の uid が同一の場合に provider が違えばユニークになるようにするため。こうしておけば、他の provider による OAuth 認証を追加した場合も大丈夫ですので。
1 |
user = User.where(:provider => auth.provider, :uid => auth.uid).first |
のように、user を検索します。
続いてマイグレート。
1 2 3 |
$ bundle exec rake db:migrate |
データベース確認。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
mysql> desc users; +------------------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------------------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | email | varchar(255) | NO | UNI | | | | encrypted_password | varchar(255) | NO | | | | | reset_password_token | varchar(255) | YES | UNI | NULL | | | reset_password_sent_at | datetime | YES | | NULL | | | remember_created_at | datetime | YES | | NULL | | | sign_in_count | int(11) | NO | | 0 | | | current_sign_in_at | datetime | YES | | NULL | | | last_sign_in_at | datetime | YES | | NULL | | | current_sign_in_ip | varchar(255) | YES | | NULL | | | last_sign_in_ip | varchar(255) | YES | | NULL | | | created_at | datetime | YES | | NULL | | | updated_at | datetime | YES | | NULL | | | uid | varchar(255) | NO | MUL | | | | provider | varchar(255) | NO | | | | | name | varchar(255) | YES | | NULL | | +------------------------+--------------+------+-----+---------+----------------+ 16 rows in set (0.00 sec) |
uid, provider, name の3つのカラムが追加されました。uid と provider でユニークな複合インデックスを作成したため、uid の Key は MUL(値が重複可能)となっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
mysql> show index from users; +-------+------------+-------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+ | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | +-------+------------+-------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+ | users | 0 | PRIMARY | 1 | id | A | 0 | NULL | NULL | | BTREE | | | | users | 0 | index_users_on_email | 1 | email | A | 0 | NULL | NULL | | BTREE | | | | users | 0 | index_users_on_uid_and_provider | 1 | uid | A | 0 | NULL | NULL | | BTREE | | | | users | 0 | index_users_on_uid_and_provider | 2 | provider | A | 0 | NULL | NULL | YES | BTREE | | | | users | 0 | index_users_on_reset_password_token | 1 | reset_password_token | A | 0 | NULL | NULL | YES | BTREE | | | +-------+------------+-------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+ 5 rows in set (0.00 sec) |
MySQL にログイン後上記のクエリで uid と provider で、ユニークな複合インデックスが作成されていることを確認できます。
通常フォームでのユーザー登録・サインインと OAuth 認証を併用する場合の問題点
通常フォームからのサインアップ・サインインと、Twitter/Facebook でのOAuth認証を併用する場合、いくつか問題があります。
uid, provider, name は OmniAuth を使った OAuth 認証から取得する情報となります。両者を併用する場合に問題となるのは、email カラム(ユニークキー)と、複合キーインデックスでユニークになっている uid + provider の組み合わせのカラム。
email カラムは通常フォームでの、ユーザー登録・サインイン時の認証用のユニークキーとして使われます。
uid + provider カラムは OAuth 認証での、ユニークな複合インデックスとして使われます。
通常のフォームでのユーザー登録・サインインでは、以下の事情を考慮しないといけない。
そして、Twitter/Facebook の OAuth 認証では以下の制約があります。
・FacebookのOAuth認証では、Email情報を取得できるが、すでに通常のフォームからユーザーが登録されていた場合、Emailが重複する可能性がある。
・逆に、ユーザーがFacebookでOAuth認証済みの場合、新たに通常フォームからのユーザー登録時に、Emailが重複する可能性がある。
以上を考慮して、通常のフォームからのユーザー登録・ログインとOAuth認証によるログインを併用するための仕様を、以下の4通り考えました。
仕様案1: ユニークインデックスは上述の通りで、ダミーのuid, Emailアドレスを利用する方法
・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 の複合インデックスにユニークキーを設定する
マイグレーションを以下のように設定。
1 2 3 4 5 6 7 |
def change remove_index :users, :email remove_index :users, [:uid, :provider] change_column :users, :email, :string change_column :users, :uid, :string add_index :users, [:email, :uid], unique: true end |
いったん、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
例えば、
1 |
config.authentication_keys = :userid |
などとして、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のもの)
※以下は参考非推奨です。
上記項目の実装は、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のもの)
・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 のアカウントを持っていない人も登録できるようにしたかったので、この仕様については考えませんでした。
- Devise の関連記事
- RailsのDevise認証機能での実装チェックリストまとめ
- Deviseで送信されるメールのfrom(送信者メールアドレス)を変更
- Facebook の OAuth 認証で OAuthException(191)エラー
- Rails Devise でパスワードリセットなどのメールテンプレート(Mailer ビュー)をカスタマイズ
- Rails + Devise 環境でのフレンドリーフォワーディング機能を修正
- Deviseでユーザー登録完了時にウェルカムメールを送信する
- Rails Devise でユーザーがプロフィール情報を更新後に元のページにリダイレクトさせる
- Devise でユーザーがパスワードなしでアカウント情報を変更するのを許可
- Rails Deviseの日本語化辞書ファイル(devise.ja.yml)
- Rails + Devise で admin ユーザー(管理者)を削除できないようにする
- 初回公開日: 2013年12月12日
Leave Your Message!