OmniAuth を使った認証について、 devise と組み合わせてログインする記事は割と見かけるのだけれども Authlogic との組み合わせの記事はあまり見かけなくて、実際にやろうとした時にちょっとばたばたしたので自分用にメモ。
1ユーザに複数の認証アカウント
なんで devise でなくて authlogic 使いたかったかっていうと、1ユーザが複数のアカウントでログインできる状態にしたかったから。
そのためには
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 インストール
gem 'authlogic'
$ bundle
User
名前はもちろん、Userでなくてなんでも良い。
$ rails g model User persistence_token:string
persistence_token
は authlogic が使うカラムなので、必ず作っておく必要がある。
class User < ActiveRecord::Base
acts_as_authentic
end
で、生成されたモデルの中で acts_as_authentic
を呼ぶ。
UserSession
これも、名前はなんでも構わない。
class UserSession < Authlogic::Session::Base
end
Authlogic::Session::Base
を継承したクラスがログインセッションを扱うモデルになる。
ログインする時は UserSession.create
だし、ログイン中のセッションを探す時は UserSession.find
だし。まるで ActiveRecord::Base
を継承したモデルのような操作感。
UserSession.new
するとインスタンスが返ってきて、それを form_for
に渡せるのも便利。
で、これがとても大事なことなんだけれども。
UserSession.new
や UserSession.create
は、 UserSession.create(params[:user_session])
みたいな感じでユーザIDとパスワードを受け取ってログインするんだけれど。
それ以外にも、ログイン対象のモデル(今回なら User
)のインスタンスを直接受け取ることもできる。
モデルのインスタンスを受け取ってログインできるの、超絶便利!
omniauth-facebook で認証する
omniauth-facebook
gem 'omniauth-facebook'
で bundle
する。
Facebook アプリの設定
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 で user
と uid
それぞれ unique index 指定しておくこと。
で、認証後にユーザを特定するためのメソッドを用意する。
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 は自動で作られてしまうのだけれども、コールバックは自分で設定する必要がある。
%w(get post).each do |method|
send(method, 'auth/:provider/callback' => 'user_sessions#create')
end
ついでにログアウトも追加しておく
get 'logout' => 'user_sessions#destroy'
UserSessionsController
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
ログイン中の情報
ログイン中のユーザ
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 を使っても良いんだけど。
def login?
!!current_user
end
あると使うし、便利かなって。
ログイン必須
def require_login
unless login?
raise # 何かちょうど良いエラーでも
end
end
後はログイン必須にしたいコントローラで。
class XxxController < ApplicationController.rb
before_action :require_login
# ...
ほかサービスのアカウントでも認証できるようにする
例えば、ここから Twitter アカウントでも認証させたくなったら。
FacebookAccount と同じ要領で追加すれば良いだけなので以下略。
あ、これじゃあタイトル詐欺だ、すみません。
Facebook.authenticate
で、ユーザがログイン中の時のことを考慮しないといけないとか。
後、アカウント単位で連携を解除できるようにしたいとか。
そういうところは、ちょこっと気をつけないといけないと思うので、頑張ってください。