Devise を導入して Facebook 認証との統合をやってみる

認証エンジン Devise を導入して、Facebook 認証との統合までをやってみます。

ほとんどは README に沿って、所々意訳を含めてお送りいたします。

Devise セットアップ

トップページ用のコントローラーを作っておきます

rails generate controller home index

Devise をインストールします

gem install devise

手動でもセットアップできますが、必要なファイルを生成してくれるコマンドが用意されています。

rails generate devise:install
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================
Some setup you must do manually if you haven't yet:
  1. Setup default url options for your specific environment. Here is an
     example of development environment:
       config.action_mailer.default_url_options = { :host => 'localhost:3000' }
     This is a required Rails configuration. In production it must be the
     actual host of your application
  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:
       root :to => "home#index"
  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:
       <p><%= notice %></p>
       <p><%= alert %></p>
  4. If you are deploying Rails 3.1 on Heroku, you may want to set:
       config.assets.initialize_on_precompile = false
     On config/application.rb forcing your application to not access the DB
     or load models when precompiling your assets.
===============================================================================

ファイルが二つ生成されて、メッセージが表示されます。

各メッセージの指示はざっくり訳すと以下のような感じです。

1. default_url_options を設定する

config/environments/*.rb の各種環境にホスト名を設定する。

これはリマインダーメール内のリンクなどに必要な設定。

config.action_mailer.default_url_options = { :host => ‘localhost:3000’ }

2. root_url を設定する

root :to => “home#index” root_url のようにどこかへ定義します。

root_url がログイン成功後のリダイレクト先になるので、最初に作った home#index に向けておきます。

rm public/index.html 削除して、/ で home#index が表示されるのを確認しておきましょう。

3. flash メッセージの設定

ログインの通知領域を配置します。

<p><%= notice %></p>

<p><%= alert %></p>

app/views/layout/application.html.erb の <%= yield %> の手前あたりに書き込んでおきます。

ログイン成功とかログイン失敗とかのメッセージが出ます。

4. Rails 3.1 を Heroku で動かす場合

heroku で動かす場合には config/environments/production.rb で config.assets.initialize_on_precompile = false にする

assets のコンパイルが自動でなくなるので、デプロイ時には rake assets:precompile を実行する。

モデル生成

Devise の初期化コードが用意できたら、Devise をつかったモデルを生成します。

rails generate devise user name:string
      invoke  active_record
      create  db/migrate/20120119171319_devise_create_users.rb
      create  app/models/user.rb
      invoke  test_unit
      create  test/unit/user_test.rb
      create  test/fixtures/users.yml
      insert  app/models/user.rb
       route  devise_for :users

name のフィールドを持った user モデルを生成します。

既に user モデルがある場合、そのモデルに Devise のオプションが追記されます。

config/routes.rb に追記された route devise_for :users によって Devise 用のルートが作成されます。

rake routes で確認してみましょう。

rake db:migrate
rails server

rake routes で確認すれば見た通りですがユーザー登録に必要そうな route が用意されました。

動作確認

いいかげんな感じですが、app/views/layouts/application.html.erb に登録やログインなどのリンクを追記して、動作を確認してみます。

<%= link_to 'sign_up', new_user_registration_path %>
<%= link_to 'sign_in',  new_user_session_path %>
<%= link_to 'edit', edit_user_registration_path %>
<%= link_to 'sign_out', destroy_user_session_path, :method => :delete %>
<%= link_to 'password_new',  new_user_password_path %>

ビューは Devise 内蔵のものが読み込まれるので、それっぽい画面がでて、ユーザー登録やログインができるようになっています。

Devise の提供するヘルパ

以下のメソッドが利用可能になる。

今回は user モデルでつくりましたが他の名前の場合は user の部分を書き換えたメソッドになります。

before_filter :authenticate_user! 
user_signed_in? # サインインしているかどうか
current_user # 現在のユーザーオブジェクト
user_session # ユーザーセッション

Devise オプション

モデル内に追記された devise メソッドに渡せるオプションです。

:database_authenticatable # データベースでアカウントのパスワードを管理できるように
:registerable # ユーザーがアカウント作成、編集、削除できるように
:recoverable # パスワードの再問い合わせができるように
:rememberable # 自動的にログインする機能ができるように
:trackable # ログイン履歴を取れるように
:validatable 入力されたパスワードやメールアドレスをチェックできるように
:token_authenticatable # API等の開発用にトークン認証ができるように
:encryptable # パスワード暗号化のカスタムを有効にする
:confirmable # ユーザー登録完了時に認証確認メールを送信されるように
:lockable # パスワードをミスするとアカウントにロックがかかるように
:timeoutable # 一定時間操作がなければログアウトするように
:omniauthable # omniauth と連携できるように

ビューのカスタマイズ

Devise 内蔵のビューによってビューを用意しなくても動作していましたが、それをカスタマイズしたい場合は、以下のコマンドを呼びます。

rails generate devise:views

app/views/devise にビューが生成されるので、これをカスタマイズに使います。

user と admin に分けて Devise を使っているような場合で、Devise のビューを分けて作りたい場合は、config/initializers/devise.rb で config.scoped_views = true を追記します。

rails generate devise:views users

ジェネレートにモデル名を渡すことで、app/views/users に Devise のビューが生成されます。

Omniauth との統合

Facebook と統合してみる。

gem 'omniauth'
gem 'omniauth-facebook'

config/initializers/devise.rb で config.omniauth :facebook, “APP_ID”, “APP_SECRET” を追記します。

app/model/user.rb の Devise メソッドに :omniauthable を引数に渡します。

これで、二つのヘルパが利用可能になります。

user_omniauth_authorize_path(provider)
user_omniauth_callback_path(provider)

ただし callback のヘルパは直接つかいません。

Facebook の認証リンク

Facebook でログインするリンクを以下のように作ります。

<%= link_to "Sign in with Facebook", user_omniauth_authorize_path(:facebook) %>

Facebook の認証ページへ飛ぶリンクができました。リンク先でログインすると認証が完了しますが、認証後の処理をするコントローラーが無いのでそれを作ります。

まず、routes.rb の Devise にコールバックで使うコントローラへの経路を定義します。

devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }

app/controllers/users/omniauth_callbacks_controller.rb を作成します。

ここには、サービスプロバイダと同名のアクション名を定義します。今回は Facebook なので facebook アクションを定義します。

find_for_facebook_oauth は後ほどモデルに定義します。

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    # You need to implement the method below in your model
    @user = User.find_for_facebook_oauth(request.env["omniauth.auth"], current_user)
    if @user.persisted?
      flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Facebook"
      sign_in_and_redirect @user, :event => :authentication
    else
      session["devise.facebook_data"] = request.env["omniauth.auth"]
      redirect_to new_user_registration_url
    end
  end
end

ここでポイントなのは、Omniauth 経由の Facebook 認証情報は request.evn[“omniauth.auth”] に格納されます。

取得できる情報については Omniauth のドキュメントを参考にしてください。

認証に成功し、ユーザーが存在する場合メッセージを渡してリダイレクトします。sign_in_and_redirect に :event => :authentication を渡しているのは全ての認証コールバックが呼ばれるようにするためです。

ユーザーが見つからない場合は、Omniauth のデータ(この場合は Facebook の登録データ)を devise. という名前空間を用いてセッションに格納しています。

こうしておくと、ユーザーがサインインしたときに、Devise が自動的に devise. で始まるデータをセッションから削除するので便利です。

その後、登録フォームへリダイレクトしています。

コントローラーを定義したら、モデルに find_for_facebook_oauth メソッドを定義します。

def self.find_for_facebook_oauth(access_token, signed_in_resource=nil)
  data = access_token.extra.raw_info
  if user = User.where(:email => data.email).first
    user
  else # Create a user with a stub password.
    User.create!(:email => data.email, :password => Devise.friendly_token[0,20])
  end
end

このメソッドは単純に、メールアドレスでユーザーを検索するか、存在しなければ、認証情報に含まれるメールアドレス(この場合は Facebook に登録されているもの)で、ランダムなパスワードを設定してユーザーを作成します。

リソースがビルドされる前に、Devise の RegistrationsController はデフォルトで User.new_with_session を呼びます。

これはサインアップへ遷移する際、ユーザーが初期化されるタイミングでセッションからデータをコピーするのに利用します。new_with_session メソッドをモデルに定義します。

class User < ActiveRecord::Base
  def self.new_with_session(params, session)
    super.tap do |user|
      if data = session["devise.facebook_data"] && session["devise.facebook_data"]["extra"]["raw_info"]
        user.email = data["email"]
      end
    end
  end
end

Facebook によるサインアップをキャンセルしたい場合、cancel_user_registration_path にリダイレクトします。

devise. で始まるセッションデータを削除し、new_with_session のフックは実行されません。

Twitter との連携

twitter と連携したい場合は、omniauth-twitter をインストールして、メソッド名などを “facebook” から “twitter” にしたものを作れば良いです。

良いですが、twitter の返す request.env[“omniauth.auth”] の中身は当然異なり、そのままでは動きません。

その辺はアプリケーションごとの仕様や実装によるでしょうが、Omniauth のストラテジとメソッド名の書き換えだけで対応できる手軽さは本当に助かりますね。

テストモード

以下、翻訳してみただけ。

Omniauth には統合テストを行うためのモック機能が備わっています。

以下のようにテストモードを有効に出来ます。

OmniAuth.config.test_mode = true

テストモードが有効になると、Omniauth へのリクエストは以下のようなモックを返し、

/auth/provider にアクセスとは即座に /auth/provider/callback へリダイレクトするようになります。

認証モック

テストモード中に返すプロバイダハッシュは以下のように設定します。

OmniAuth.config.mock_auth[:twitter] = {
  :provider => 'twitter',
  :uid => '123545'
  # etc.
}

どのサービスでもないプロバイダを示す :deault というキーをセットすることが出来ます。

:deault はすべてのプロバイダで返される認証モックとして働きます。

モックの追加

新しいモックプロバイダーを追加するのに add_mock メソッドを使うこともできます。

This information will automatically be merged with the default info so it will be a valid response.

OmniAuth.config.add_mock(:twitter, {:uid => '12345'})

失敗のモック

ハッシュの変わりにシンボルをセットすると、メッセージとともに認証失敗を返します。

OmniAuth.config.mock_auth[:twitter] = :invalid_credentials

/auth/failure?message=invalid_credentials へリダイレクトされます。

コントローラーのセットアップ

Omniauth のテストを行う際、env 変数をセットする必要があるでしょう。

以下のサンプルのようにRSpec や Twitter 用としてセットすることができます。

before do
  request.env["devise.mapping"] = Devise.mappings[:user]
  request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:twitter]
end

ルーティングエラーを防ぐために、以下のサンプルを参考にしてください。

Failure/Error: get :twitter
AbstractController::ActionNotFound:
  Could not find devise mapping for path "/users/auth/twitter/callback".
    Maybe you forgot to wrap your route inside the scope block? For example:
      devise_scope :user do
        match "/some/route" => "some_devise_controller"
      end

まだまとめ中…。

シェアする

  • このエントリーをはてなブックマークに追加

フォローする