AuthlogicとOmniAuthを使って複数のアカウントと連携する

More than 3 years have passed since last update.

OmniAuth を使った認証について、 devise と組み合わせてログインする記事は割と見かけるのだけれども Authlogic との組み合わせの記事はあまり見かけなくて、実際にやろうとした時にちょっとばたばたしたので自分用にメモ。


1ユーザに複数の認証アカウント

なんで devise でなくて authlogic 使いたかったかっていうと、1ユーザが複数のアカウントでログインできる状態にしたかったから。

そのためには


app/models/user.rb

class User < ActiveRecord::Model

has_one :facebook_account
has_one :twitter_account
end

というように、なっていてほしい。

でもって、ログイン処理の対象になるのは FacebookAccount や TwitterAccount でなくて User モデルであってほしい。

最初 devise でそういうことをやろうとしたんだけど、なんだかうまくいかなくて authlogic に乗り換えたわけです。

authlogic にも Facebook とか OpenID とかの Add on があるみたいなんですが、そちらはどんな実装になっているのかコードを読み切れなかったので、使っていません。

認証に関しては、対応サービスも多い OmniAuth 使うことにしました。


authlogic の準備


authlogic インストール


Gemfile

gem 'authlogic'


$ bundle


User

名前はもちろん、Userでなくてなんでも良い。

$ rails g model User persistence_token:string

persistence_token は authlogic が使うカラムなので、必ず作っておく必要がある。


app/models/user.rb

class User < ActiveRecord::Base

acts_as_authentic
end

で、生成されたモデルの中で acts_as_authentic を呼ぶ。


UserSession

これも、名前はなんでも構わない。


app/models/user_session.rb

class UserSession < Authlogic::Session::Base

end

Authlogic::Session::Base を継承したクラスがログインセッションを扱うモデルになる。

ログインする時は UserSession.create だし、ログイン中のセッションを探す時は UserSession.find だし。まるで ActiveRecord::Base を継承したモデルのような操作感。

UserSession.new するとインスタンスが返ってきて、それを form_for に渡せるのも便利。

で、これがとても大事なことなんだけれども。

UserSession.newUserSession.create は、 UserSession.create(params[:user_session]) みたいな感じでユーザIDとパスワードを受け取ってログインするんだけれど。

それ以外にも、ログイン対象のモデル(今回なら User)のインスタンスを直接受け取ることもできる。

モデルのインスタンスを受け取ってログインできるの、超絶便利!


omniauth-facebook で認証する


omniauth-facebook


Gemfile

gem 'omniauth-facebook'


bundle する。


Facebook アプリの設定


config/initializers/omniauth_builder.rb

Rails.application.config.middleware.use OmniAuth::Builder do

provider :facebook, ENV['FACEBOOK_APP_ID'], ENV['FACEBOOK_APP_SECRET']
end


FacebookAccount

$ rails g model FacebookAccount user:references uid:string access_token:string name:string image:text

migrate で useruid それぞれ unique index 指定しておくこと。

で、認証後にユーザを特定するためのメソッドを用意する。


app/models/facebook_account.rb

class FacebookAccount < ActiveRecord::Base

belongs_to :user

def self.authenticate(auth)
account = self.where(uid: auth.uid).take
account ||= self.new(uid: auth.uid)
account.access_token = auth.credentials.token
account.name = auth.info.name
account.image = auth.info.image

unless account.user
account.user = User.create
end

account.save!
account
end
end


uid が見つかればそのアカウントを使うし、見つからなければ新しく作る。

ログイン中のユーザに対してアカウント追加したい場合はこれだとうまくいかないんだけど、今はまだ考えない。

受け取るのは OmniAuth::AuthHash インスタンス。

ついでなので、ユーザの情報を更新もしておく。


いよいよログイン


コールバックURL

Omniauth は、認証開始の URL は自動で作られてしまうのだけれども、コールバックは自分で設定する必要がある。


config/routes.rb

  %w(get post).each do |method|

send(method, 'auth/:provider/callback' => 'user_sessions#create')
end


ついでにログアウトも追加しておく


config/routes.rb

  get 'logout' => 'user_sessions#destroy'



UserSessionsController


app/controllers/user_sessions_controller.rb

class UserSessionsController < ApplicationController

def create
account = account_model.authenticate(env['omniauth.auth'])
@user_session = UserSession.new(account.user)
if @user_session.save
flash[:notice] = 'Authenticate success'
else
flash[:error] = 'Authenticate failed.'
end
redirect_to root_path
end

def destroy
current_user_session.destroy
redirect_to root_path
end

private
def account_model
# params[:provider] に意図していない文字列が入ってないかチェックしたりとか
Object.const_get("#{params[:provider].capitalize}Account")
end
end



ログイン中の情報


ログイン中のユーザ


app/controllers/application_controller.rb

class ApplicationController < ActionController::Base

# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception

helper_method :current_user_session, :current_user

private

def current_user_session
return @current_user_session if defined?(@current_user_session)
@current_user_session = UserSession.find
end

def current_user
return @current_user if defined?(@current_user)
@current_user = current_user_session && current_user_session.user
end
end


これで current_user で、ログイン中ユーザが参照できる。


ログイン中かどうか

current_user を使っても良いんだけど。


app/controllers/application_controller.rb

  def login?

!!current_user
end

あると使うし、便利かなって。


ログイン必須


app/controllers/application_controller.rb

  def require_login

unless login?
raise # 何かちょうど良いエラーでも
end
end

後はログイン必須にしたいコントローラで。

class XxxController < ApplicationController.rb

before_action :require_login
# ...


ほかサービスのアカウントでも認証できるようにする

例えば、ここから Twitter アカウントでも認証させたくなったら。

FacebookAccount と同じ要領で追加すれば良いだけなので以下略。

あ、これじゃあタイトル詐欺だ、すみません。

Facebook.authenticate で、ユーザがログイン中の時のことを考慮しないといけないとか。

後、アカウント単位で連携を解除できるようにしたいとか。

そういうところは、ちょこっと気をつけないといけないと思うので、頑張ってください。