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は記述するとエラーになります。