2016-11-01

初心者がRails+DeviseでTodoアプリ作る【Omniauth編】

Omuniauthを使ってRailsアプリにソーシャルサインオンの機能を追加してみました。

facebook編

まずはgemのインストール
gem 'omniauth-facebook'

モデルの変更

class User < ApplicationRecord
  has_many :tasks, dependent: :destroy

  # Include default devise modules. Others available are:
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :confirmable, :lockable, :timeoutable, 
         :omniauthable # 追加

  def self.from_omniauth(auth)
    where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
      user.email = auth.info.email
      user.password = Devise.friendly_token[0,20]
      # user.name = auth.info.name   # assuming the user model has a name
      # user.image = auth.info.image # assuming the user model has an image
    end
  end
end

プロバイダのキーとプロバイダ内のユーザIDをユーザテーブルに追加

$ rails g migration AddOmniauthToUsers provider:string uid:string
$ bundle exec rake db:migrate

config/initializers/devise.rbに以下の設定を追加

config.omniauth :facebook, "APP_ID", "APP_SECRET"

ビューにfacebookログインのリンクを追加

<%= link_to 'facebookアカウントでログイン', user_facebook_omniauth_authorize_path %>

controllers/users/omniauth_callbacks_controller.rbを作成

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    # You need to implement the method below in your model (e.g. app/models/user.rb)
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, :event => :authentication #this will throw if @user is not activated
      set_flash_message(:notice, :success, :kind => "Facebook") if is_navigational_format?
    else
      session["devise.facebook_data"] = request.env["omniauth.auth"]
      redirect_to new_user_registration_url
    end
  end

  def failure
    redirect_to root_path
  end
end

config/routes.rbを修正

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

仕組み

user_facebook_omniauth_authorize_pathではinitializersで設定したfacebookのAPP IDとSECRETからAuthorize URLを生成し、コールバックが返ってきたら作成したcallbackのコントローラで認可後の処理をします。

request.env["omniauth.auth"]にはプロバイダやユーザ情報が入っているので、それらの情報を使ってActiveRecordのwhere(provider: auth.provider, uid: auth.uid)で検索し、既にプロビジョニングされているユーザであればログイン、そうでなければユーザの新規作成をしています。

コールバック直後にユーザを自動作成するのではなく、ユーザ入力や確認画面等をはさみたい場合は、モデルをcreateせずにrequest.env["omniauth.auth"]をsessionで持ち回って登録画面に遷移させればOK。sessionで"devise."のプレフィックスが付いたものはdeviseでログインしたときに自動的に削除されるためセキュアかつ効率的です。

これで、"facebookアカウントでログイン"リンクをクリックすると、facebookの認証/認可画面を介してアプリケーションのユーザが自動的に作成 or ログインできます。

Twitter、Githubの対応サンプル

以下のGemをインストール
gem 'omniauth-twitter'
gem 'omniauth-github'

initializerに追加

config.omniauth :twitter, "CLIENT_KEY", "CLIENT_SECRET"
config.omniauth :github, "APP KEY", "APP SECRET"

以下のリンクを追加

<%= link_to 'Twitterアカウントでログイン', user_twitter_omniauth_authorize_path %>
<%= link_to 'githubアカウントでログイン', user_github_omniauth_authorize_path %>

callbackはtwitter, githubというメソッドを追加して上述のfacebookメソッドのように実装すればOKです。

ただし、request.env['omniauth.auth']の値はプロバイダによって変わってくるので、プロビジョニングの部分等は各プロバイダを考慮した上で実装する必要があります。

補足

リファレンスではUserモデルのdeviseのモジュール定義でomniauth_providersを定義しています。
class User < ApplicationRecord
...
  # Include default devise modules. Others available are:
  devise ..., :omniauthable, omniauth_providers: [:facebook, :twitter, :github]
...
end

実はinitializersでomniauthメソッドで定義したソーシャルプロバイダを全て利用する場合は、omniauth_providersの定義が無くても動きます。

devise+omniauthでは利用するソーシャルプロバイダのキー("twitter"とか"facebook"とか)を配列で管理しており、プロバイダのキーを使って認証、コールバック用のURLやコントローラへのマッピングを生成しています。その配列に対するアクセサがomniauth_providersメソッドになります。

mapping.to.omniauth_providers.each do |provider|
  match "#{path_prefix}/#{provider}",
    to: "#{controllers[:omniauth_callbacks]}#passthru",
    as: "#{provider}_omniauth_authorize",
    via: [:get, :post]

  match "#{path_prefix}/#{provider}/callback",
    to: "#{controllers[:omniauth_callbacks]}##{provider}",
    as: "#{provider}_omniauth_callback",
    via: [:get, :post]
end

deviseメソッドによって可変長の引数に対応したモジュール(lib/devise/modelsディレクトリ直下のモジュール)のモジュール+モジュール内のmodule ClassMethodsがinclude、extendされます

if mod.const_defined?("ClassMethods")
  class_mod = mod.const_get("ClassMethods")
  extend class_mod

  if class_mod.respond_to?(:available_configs)
    available_configs = class_mod.available_configs
    available_configs.each do |config|
      next unless options.key?(config)
      send(:"#{config}=", options.delete(config))
    end
  end
end

omniauthableモジュールのClassMethods内ではDevise::Models.configメソッドが呼び出され、available_configsに[:omniauth_providers]がセットされるとともに、omniauth_providersのアクセサが定義されます。Getterはインスタンス変数が定義されていればインスタンス変数を返し、そうでなければSuperClassのGetter、それもなければDeviseのクラスメソッドを返します。

def self.config(mod, *accessors) #:nodoc:
  class << mod; attr_accessor :available_configs; end
  mod.available_configs = accessors

  accessors.each do |accessor|
    mod.class_eval <<-METHOD, __FILE__, __LINE__ + 1
      def #{accessor}
        if defined?(@#{accessor})
          @#{accessor}
        elsif superclass.respond_to?(:#{accessor})
          superclass.#{accessor}
        else
          Devise.#{accessor}
        end
      end
      def #{accessor}=(value)
        @#{accessor} = value
      end
    METHOD
  end
end

Deviseのクラスメソッドはinitializers内のomniauthメソッドで定義したプロバイダのキーの配列を返します。

def self.omniauth_providers
  omniauth_configs.keys
end

omniauth_providersのプロパティを定義すると、Setterが呼び出され、インスタンス変数が定義された状態になります。インスタンス変数が定義されている場合、Getterはそのインスタンス変数を返すようになるので、この時点でDeviseのクラスメソッドomniauth_providersは呼び出されなくなります。

たとえばinitializersでfacebook、twitterに対して定義していてもomniauth_providersのプロパティでfacebookしか設定していない場合はプロパティの設定が優先されるため、twitterをソーシャルサインオンに利用できなくなり、twitter用の認証、コールバックURLは記述するとエラーになります。

参考URL

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