読者です 読者をやめる 読者になる 読者になる

みかづきメモ

学習したことのメモとか、日記とか、備忘録。

OmniAuth + devise で、いろいろな外部認証を追加する

Ruby on Rails Ruby

Ruby on Rails にて、 devise + OmniAuth で、 Twitter などの外部認証を追加します。
有名な組み合わせなので、日本語資料も多いですが、一応。


想定している環境は、

  • すでに devise でのログインは実装済み(モデルは User )
  • 実装済みのものと併用して、 Twitter などでもログインしたい
  • TwitterFacebook など、複数の外部サービスを使えるようにしたい
  • 外部認証でのアカウント作成は行わない

といった、ありふれたものを想定しています。

とりあえず、まずはインストールから。

# @ Gemfile
gem 'omniauth'
gem 'omniauth-facebook'
gem 'omniauth-twitter'


次に、 Consumer Key などの設定を行います。
OmniAuth 単体の場合は、 config/initializers/omniauth.rb に行いますが、
devise と併用する場合、 config/initializers/devise.rb に記述します。
ここでは、 devise.rb にすでに OmniAuth 用の設定があるので、そこに追加します。

# @ config/initializers/devise.rb
# ==> OmniAuth
# Add a new OmniAuth provider. Check the wiki for more information on setting
# up on your models and hooks.
# config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'
config.omniauth :facebook, 'YOUR_APP_ID', 'YOUR_APP_SECRET'
config.omniauth :twitter, 'YOUR_CONSUMER_KEY', 'YOUR_CONSUMER_SECRET'


次に、データベースに項目を追加します。
今回はすでに devise を使用しているので、マイグレーションファイルを追加します。
なお、ここでは複数の外部サービスでログイン可能にするため、
provideruid の組み合わせでユニークなものとしておきます。

$ rails g migration AddOmniauthToUsers provider:index uid:index

ユニーク設定のために、書き換えます。

# @ db/migrate/20160111132341_add_omniauth_to_users.rb
class AddOmniauthToUsers < ActiveRecord::Migration
  def change
    add_column :users, :provider, :string
    add_column :users, :uid, :string
    
    add_index :users, [:provider, :uid], unique: true
  end
end

そして、適用。

$ bundle exec rake db:migrate


次に、 devise で扱うモデルに対して、 omniauthable を追加します。

# @ app/models/user.rb
devise :omniauthable, :omniauth_providers => [:facebook, :twitter]


次は、ルーティング設定を行います。
/users/omniauth_callbacks/:provider にて、コールバックを行うようにします。

# @ config/routes.rb
devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }

この時、 URL にてロケール設定を行っているなど、動的に変更されるものがある場合、
以下の様なエラーが出力されることがあります。

$ bundle exec rake routes
rake aborted!
Devise does not support scoping omniauth callbacks under a dynamic segment
and you have set "/:locale/users". You can work around by passing
`skip: :omniauth_callbacks` and manually defining the routes. Here is an example:

    match "/users/auth/:provider",
      constraints: { provider: /google|facebook/ },
      to: "devise/omniauth_callbacks#passthru",
      as: :omniauth_authorize,
      via: [:get, :post]

    match "/users/auth/:action/callback",
      constraints: { action: /google|facebook/ },
      to: "devise/omniauth_callbacks",
      as: :omniauth_callback,
      via: [:get, :post]

こうなった場合は、:locale などの外で、一度ルーティング設定を行う必要があります。

# @ config/routes.rb
devise_for :users, skip: [:sign_up, :sign_in, :sign_out, :registrations, :sessions, :passwords, :confirmations, :unlock], 
  controllers: { omniauth_callbacks: 'omniauth_callbacks' }

scope "/:locale" do
  device_for :users, skip: [:omniauth_callbacks], ...
end


最後は認証部分です。
まずは、コントローラーから。
メソッド名は、 OmniAuth のプロバイダー名にしておきます。

# @ app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController

  # Facebook の認証処理
  def facebook
    authorization
  end
  
  # Twitter の認証処理
  def twitter
    authorization
  end
  
  private
  def authorization
    if current_user
      user = user.omniauth request.env['omniauth.auth']
    else
      user = User.from_omniauth request.env['omniauth.auth']
    end

    if user.persisted?
      # 成功時処理
    else
      # 失敗時処理
    end
  end
end


次に、モデル部分。
ここでは、外部認証での新規ユーザー作成は行わないので、更新および認証処理のみしています。

# @ app/models/user.rb
class User < ActiveRecord::Base
  ...
  
  def omniauth auth
    self.provider = auth.provider
    self.uid = auth.uid
    self.save
    self
  end

  class << self
    def from_omniauth auth
      User.where(provider: auth.provider, uid: auth.uid).first
    end
  end
end

こんな感じで、認証を追加することができます。
では。