RailsでDeviseとOmniAuthによるTwitter/FacebookのOAuth認証、および通常フォーム認証を併用して実装

スポンサーリンク

Rails アプリケーションにて、Facebook/Twitter の OAuth認証を実装します。Devise と OmniAuth を使います。まずは事前に、Devise を導入し、通常の認証・認可の機能を追加する。認証用のユーザーのモデルを User としました。以下参照。

Rails4 にて Devise でユーザー登録・ログイン認証・認可の機能を追加 | EasyRamble

また、通常フォームでのユーザー登録・サインインと Twitter/Facebook の OAuth 認証を併用する仕様にしたいと思います。仕様は以下のページで考えましたので、そちらを先に読んでおくと分かりやすいかもです。

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

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

— 環境 —
Rails 5.0.0.1
Devise 4.2.0
omniauth 1.3.1
omniauth-facebook 4.0.0
omniauth-twitter 1.2.1

【追記 2016/11/07】
Rails 5.0.0.1 + Devise 4.2.0 + OmniAuth 1.3.1 という現時点(2016/11/07)の最新バージョン環境で、Rails アプリケーションに Devise + OmniAuth によるユーザー認証の実装を試しましたところ、記事公開当初と同様の手順で実装できました。
【追記ここまで】

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

OAuth 認証用の gem をインストール

Gemfile に以下を追加。

bundle install。

OAuth プロバイダーの一覧はこちら。→ List of Strategies

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

db/migrate/***_add_omniauth_columns_to_users.rb

マイグレート。

データベース確認。

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

config/initializers/devise.rb に OAuth 認証に必要なキー・シークレットトークンを記述

Facebook, Twitter の developer サイトに登録して、OAuth 認証の API 利用に必要な key, secret を取得します。

Facebook の API Key

Home – Facebook開発者 で取得。
ログイン → 右上の「Register Now」から developer 登録。
Facebook Developers の右上の「+新しいアプリを作成」。
・アプリ名:任意
・名前空間:任意
・カテゴリ:その他(任意)

と入力して、App ID, App Secret を取得。
アプリをFacebookに結合する方法を選択で、Facebookでログインが可能なウェブサイトに「http://localhost:3000」を入力。

Twitter の API Key

Twitter Developers で取得。
ログイン → 右上 My Applications → Create a new application
・Name:任意
・Description:任意
・Website:適当なURL(http://localhost:3000 だとNG)
・Callback URL:http://127.0.0.1:3000/auth/twitter/callback
(http://localhost:3000 だとNG)

その後、Consumer key, Consumer secret を取得し、 key, secret を config/initializers/devise.rb に設定。
Settings で 「Allow this application to be used to Sign in with Twitter」にチェック。一回TwitterでOAuth認証を行うと、次回からは確認画面が省略される。

OmniauthでTwitter認証(OAuth認証) – yamotonalds’s blog

production 用と development 用でAPI Keyを使い分ける場合は、それぞれFacebook用/Twitter用に2つずつ取得します。取得した API Key を config/initializers/devise.rb に記述。Rails.env.production? で判定して使い分けしています。

config/initializers/devise.rb

User モデルに omniauthable を追加

使う Devise のモジュールを User モデルで設定します。

app/models/user.rb

私は、以上のように設定しました。今回は、:confirmable, :lockable, :timeoutable は使いません。

Devise は以下の URL ヘルパーメソッドを生成します。

user_omniauth_authorize_path(provider)
user_omniauth_callback_path(provider)

callback のヘルパーメソッドは直接使うことはありません。1番目の OAuth 認証用の URL ヘルパーメソッドをビューで使用します。

OAuth の callback 用ルーティングを追加

config/routes.rb に callback 用のルーティングを追加します。

config/routes.rb

Users::OmniauthCallbacksController を作成

callback は provider と同名のメソッドを実装する必要があります。

app/controllers/users/
omniauth_callbacks_controller.rb

このアクションには3つの特徴がある。

1. OAuth 認証による provider からのレスポンスの情報は、request.env[“omniauth.auth”] にハッシュとして格納されている。

2. モデルから正当なユーザーが見つかった場合は、サインインさせなければならない。Devise のデフォルトのフラッシュメッセージは変更できます。次に、ユーザーをサインインさせリダイレクトする。sign_in_and_redirect メソッドに :event => :authentication 引数を渡し、全ての認証コールバックが呼ばれるようにしている。

3. ユーザーがログインを持続していない場合、セッションに OmniAuth 情報を格納する。session[“devise.facebook_data”] と devise. の名前空間でセッション情報を保存する。こうしておくと、ユーザーがサインインした時はいつでも、Devise が devise. で始まるセッション情報を削除し、セッションのクリーンアップを自動で行ってくれる。最後に、登録フォームにリダイレクトする。

なお、Twitter のコールバックのほうが、request.env[“omniauth.auth”].except(“extra”) となっているのは、ActionDispatch::Cookies::CookieOverflow エラー | EasyRamble の理由によるものです。また、User.find_for_facebook_oauth, User.find_for_twitter_oauth は後で実装する。

以降、OAuth と通常サインアップを併用させる仕様に基づく実装を行います。仕様の詳細はこちら。→ Rails4 で Devise と OmniAuth を使い、通常フォームでのユーザー登録・サインインと OAuth 認証を併用する仕様を考えた | EasyRamble

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

・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 グループ

まずは、Users::RegistrationsController で build_resource(hash=nil) をオーバーライドします。resource を組み立てる前に、hash に hash[:uid] を追加しています。

app/controllers/users/registrations_controller.rb

続いて、User.create_unique_string を User モデルに定義。

app/models/user.rb

以上で、Devise::RegistrationsController の create アクション時に、独自に User モデルの uid 属性を、DB の users テーブルに保存することができます。

OAuth 認証時の email フィールド設定の実装

Twitter の OAuth 認証では Email アドレスが取得できないことに注意。また、通常フォームによるサインアップと OAuth 認証で Email が重複するのも NG です。

・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 クラスメソッドを定義する。これらのメソッドは、上の方の Users::OmniauthCallbacksController で定義した twitter, facebook のコールバックメソッドで使われるものです。

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

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

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

User.find_for_facebook_oauth, User.find_for_twitter_oauth の定義を含みつつ、User モデルの全体は以下のようになりました。

app/models/user.rb

User.find_for_facebook_oauth, User.find_for_twitter_oauth は、ユーザーを DB から見つけるか、または見つからない場合にユーザーオブジェクトを生成して DB に保存するために実装します。

unless 節で新たなユーザーのOAuth認証の場合に、OAuth認証によって取得した name, provider, uid を設定しています。Facebook の場合は email も取得。Twitter の OAuth 認証時の email は、User.create_unique_email で一意になるように設定します。また password 属性が空ですと、encrypted_password が NULL になり弾かれるので、これも適当に設定しています。

Users::OmniauthCallbacksController のコールバックメソッドも、User モデルの self.find_for_provider_oauth メソッドも重複箇所が多いので、OAuth 認証で使う provider が多い場合などはリファクタリングが必要。provider の名前からメソッドを動的に生成すればコードが随分短くなりそうなので、いずれ挑戦してみたいと思います。

User.new_with_session について

OmniAuth: Overview を参照すると、サインアップ前に User モデルに情報を保存する場合、User.new_with_session を実装する方法もあるようです。

Devise の RegistrationsController はデフォルトで、リソース(Userモデルのインスタンスなど)を作成する前に、User.new_with_session クラスメソッドを呼び出します。これは次のことを意味する。もし、ユーザーがサインアップ前に初期化される時にセッションなどから情報をコピーする必要がある場合、モデルで new_with_session を実装する必要がある。

公式Wikiの参考コードは以下。

なお、最初はこの User.new_with_session(params, session) を利用してして、通常サインアップ時の uid 設定を実現しようとしたのですが上手くできませんでした。

new_with_session という名前なので、セッションスタートと同時に user を作るのかな?そうだとすると、当然その前の uid に対する strong parameters ?による制約で、サインアップがはじかれるという状態になってしまうのが理由でしょうか。どこかの provider で OAuth 認証する場合であれば上手く動くはず。ということで、今のところこの new_with_session は使っていません。

ビューに OAuth 認証用のログインへのリンクを表示

最後に、ビューで Twitter/Facebook への OAuth 認証用のリンク先URLを表示させます。

私は以下のように、app/views/layouts/application.html.erb のヘッダーのナビゲーション部分に追加しました。

app/views/layouts/application.html.erb

以上で終了、さっとブラウザで試した感じでは上手く動作している模様。色々実装を先に試しちゃったのでテスト書かなきゃです…

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

Leave Your Message!