plataformatec/devise と intridea/omniauth を使って、Twitterでも、Facebookでも、GitHubでもログインできるような機能を作る方法を書いておく。
最初に
devise には Omniauthable という「これ使えば簡単に実装できるよ!」って雰囲気のmoduleがいます。
しかし、これは罠です。使うと死にます。
Currently, Devise's Omniauthable module does not work with multiple models. No need to worry though, as the Omniauthable module is but a simple wrapper around OmniAuth.
引用: https://github.com/plataformatec/devise/wiki/OmniAuth-with-multiple-models
ソースコード
# db/migrate/20160102181416_create_authentications.rb class CreateAuthentications < ActiveRecord::Migration def change create_table :authentications do |t| t.references :user, index: true, foreign_key: true, null: false t.string :provider, null: false t.string :uid, null: false t.string :token, null: false t.timestamps null: false end add_index :authentications, [:provider, :uid], unique: true end end # app/models/authentication.rb class Authentication < ActiveRecord::Base belongs_to :user validates :user_id, presence: true validates :provider, presence: true validates :uid, presence: true, uniqueness: { scope: :provider } validates :token, presence: true def self.from_omniauth(auth, user) obj = find_or_initialize_by provider: auth.provider, uid: auth.uid obj.token = auth.credentials.token transaction do obj.user ||= user || User.create!(email: auth.info.email, password: Devise.friendly_token[0, 20]) obj.save! if obj.changed? end obj end end # app/controllers/authentications_controller.rb class AuthenticationsController < ApplicationController def create @authentication = Authentication.from_omniauth(auth_hash, current_user) if user_signed_in? flash[:notice] = 'Successfully linked that account' else sign_in @authentication.user, event: :authentication flash[:notice] = 'Signed in' end redirect_to request.env['omniauth.origin'] || root_path end private def auth_hash request.env['omniauth.auth'] end end
説明
いくつかメモ残しておかないと忘れそうなので、メモ書いておく。
ユーザーの情報
deviseのwikiには下記のようなコードが書かれています。
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
これ、最初の罠です。
first_or_create
のブロックは初回しか実行されません。このコードだと、ユーザーが各サービスでプロフィールなどを変更しても、アプリ側のログイン時に更新されなくなってしまいます。
↓みたいに書いておけば、ログイン時に毎回情報が更新されるようになります。
def self.from_omniauth(auth) user = find_or_initialize_by(provider: auth.provider, uid: auth.uid) user.attributes = { email: auth.info.email, password: Devise.friendly_token[0, 20], name: auth.info.name, # assuming the user model has a name image: auth.info.image # assuming the user model has an image } user.save if user.changed? user end
find_or_initialize_by
や if user.changed?
で無駄なSQLが発行されないようにしてる。
ログイン状態による分岐
@authentication = Authentication.from_omniauth(auth_hash, current_user) if user_signed_in? flash[:notice] = 'Successfully linked that account' else sign_in @authentication.user, event: :authentication flash[:notice] = 'Signed in' end
flash[:notice]
に書いてある通りだけど、ログイン時はログインユーザーの連携サービスとして関連付けるようにしている。
実際に関連付けている処理はmodelで行っている。
transaction do obj.user ||= user || User.create!(email: auth.info.email, password: Devise.friendly_token[0, 20]) obj.save! if obj.changed? end
リダイレクト
redirect_to request.env['omniauth.origin'] || root_path
OmniAuthでは認証後に表示するページをorigin
というパラメータで渡すことができる。
この動作に対応するため、上記のようなコードにしている。