Edited at

既存のDevise認証アプリに OmniAuth認証を追加する

More than 3 years have passed since last update.

Screenshot 2015-08-01 11.34.30.png

OmniAuth認証についてのメモ。


概要


  • Deviseで認証を行うRailsアプリに対して、OmniAuthでの認証機能を追加する。

  • Twitterをプロバイダーとする。

  • OmniAuthで認証する場合は、パスワード入力を免除する。

  • 新規登録時、emailは必須項目とする。

  • Deviseのconfirmableモジュールでemailの確認をする。


手順


認証してもらいたいプロバイダーにアプリを登録し、APIキー等を取得

TwitterのApplication Managementサイトに行き、フォームに必要事項を記入して提出

Screenshot 2015-07-31 10.42.18.png



  • Name: アプリ名(ユーザーが認証手続きするときに表示される)


  • Description: アプリについての説明 (ユーザーが認証手続きするときに表示される)


  • Website: 今Websiteがなければ、仮のURLを記入する。後に変更可能。


  • Callback URL: omniauth-twitterにより認証完了後に呼ばれるURLを記入する。Deviseを使用している場合はDeviseが面倒を見てくれるので、深く考えずhttp://127.0.0.1:3000/auth/twitter/callbackを入力する。

左から3番目のタブ(Keys and Access Tokens)をクリックし、API KeyとAPI Secretを確認。

Screenshot 2015-07-31 13.33.45.png


Gemfileにomniauth-twitterを追加


/Gemfile

#...

gem 'omniauth-twitter'

#...


$ bin/bundle


API KeyとAPI Secretを/config/secrets.ymlに加える

直接ペーストせずに環境変数経由で渡すようにするとよい。

これで以降は、Development/Producton環境を問わずRails.application.secrets.twitter_api_keyRails.application.secrets.twitter_api_secretとしてAPI KeyAPI Secretにアクセス可能になる。


/config/secrets.yml

production:

# ==> Rails
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
# ==> OmniAuth
facebook_api_key: <%= ENV["FACEBOOK_API_KEY"] %>
facebook_api_secret: <%= ENV["FACEBOOK_API_SECRET"] %>
twitter_api_key: <%= ENV["TWITTER_API_KEY"] %>
twitter_api_secret: <%= ENV["TWITTER_API_SECRET"] %>

development:
# ==> Rails
secret_key_base: <%= ENV["SECRET_KEY_BASE_DEV"] %>
# ==> OmniAuth
facebook_api_key: <%= ENV["FACEBOOK_API_KEY"] %>
facebook_api_secret: <%= ENV["FACEBOOK_API_SECRET"] %>
twitter_api_key: <%= ENV["TWITTER_API_KEY"] %>
twitter_api_secret: <%= ENV["TWITTER_API_SECRET"] %>

test:
# ==> Rails
secret_key_base: <%= ENV["SECRET_KEY_BASE_TEST"] %>


Development環境の場合、dotenvを利用して、API Key等を/.envから自動で環境変数に割り当てることも可能。


/.env

#...

# ==> OmniAuth
TWITTER_API_KEY = "T0ONiGrNuMNsKb5AyNC05mOpe"
TWITTER_API_SECRET = "FxrQ6ddvlVbIaUlvEm0xg4fgoK9ACmWWnkesdU60ck1vJFoBM8"

#...


この時点でgitに秘密のデータがaddされないことを確認しておくのが賢明。

# Ignore secrets

.env
.secret


Deviseにプロバイダー名、API Key、API Secretを渡す

/config/initializers/devise.rbの230行目あたりにプレースホルダーがある。


/config/initializers/devise.rb

#...

# ==> OmniAuth
config.omniauth :twitter, Rails.application.secrets.twitter_api_key,
Rails.application.secrets.twitter_api_secret

#...


注意: 複数の参考資料を参照していると混乱しやすいが、Devise認証付きのアプリでOmniAuthを実装する場合は、Deviseがmiddlewareを作ってくれるので、/config/initializers/omniauth.rbは不要。ここで手動でmiddlewareを作ってしまうと、Deviseのつくったmiddlewareと衝突し、認証が全て失敗してしまう。


/config/initializers/omniauth.rb

# 注意: Devise付きのアプリでOmniAuthを実装する場合は不要

# Rails.application.config.middleware.use OmniAuth::Builder do
# provider :twitter, Rails.application.secrets.twitter_api_key,
# Rails.application.secrets.twitter_api_secret
# end


omniauthableモジュールをUserモデルに追加


/app/models/user.rb

class User < ActiveRecord::Base

#...

devise :database_authenticatable, :registerable, :confirmable,
:recoverable, :rememberable, :trackable, :validatable,
:omniauthable, omniauth_providers: [:twitter]

#...


これでDeviseが自動でSign in with Twitterリンクを生成してくれる。試しに/users/sign_upに行ってリンクがあるかどうか確認する。

Screenshot 2015-08-01 11.37.19.png


user_omniauth_authorize_path(provider)

Deviseが自動で 生成したuser_omniauth_authorize_pathメソッドを利用して好きなところにOmniAuth認証へのリンクを貼る。

= link_to "Log in with twitter", user_omniauth_authorize_path(:twitter)


OmniAuth認証データをデータベースに保存するためのマイグレーション

$ rails g migration add_omniauth_to_users provider uid



  • provider: 認証したプロバイダー名


  • uid: 一意的ID

$ rake db:migrate


OmniAuth callbackを取り扱うコントローラを作成

$ rails g controller omniauth_callbacks

本コントローラをdevise_forに登録する


/config/routes.rb

Rails.application.routes.draw do

#...

devise_for :users, controllers: { omniauth_callbacks: 'omniauth_callbacks' }

#...

end


認証失敗時の取り扱い等の機能を取り込むため、super classはDevise::OmniauthCallbacksControllerとする。

複数のプロバイダーを利用する場合も内容は同じなので、alias_methodを用い、同じコードを複数のプロバイダーに対して使用できるようにする。


/app/controllers/omniauth_callbacks_controller.rb

class OmniauthCallbacksController < Devise::OmniauthCallbacksController

def all
# 認証データを元にデータベースでユーザーを探し、なければ作る。
user = User.from_omniauth(request.env["omniauth.auth"])
if user.persisted? # ユーザーがデータベース上に存在している。
sign_in_and_redirect user # ユーザーをsign_inする
set_flash_message(:notice, :success, kind: __callee__.to_s.capitalize) if is_navigational_format?
else # 何らかの理由でデータベースに保存されていない。
session["devise.user_attributes"] = user.attributes # 認証データを覚えておく。
redirect_to new_user_registration_url(from_omniauth_callback: "1") # ユーザーを新規登録ページに転送。
end
end

alias_method :twitter, :all
end



UserモデルにOmniAuth認証処理に使用するメソッドを追加


/app/models/user.rb

class User < ActiveRecord::Base

#...

devise :database_authenticatable, :registerable, :confirmable,
:recoverable, :rememberable, :trackable, :validatable, :omniauthable

# OmniAuth認証データを元にデータベースでユーザーを探す。なければ新しく作る。
def self.from_omniauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
user.provider = auth.provider
user.uid = auth.uid
user.username = auth.info.nickname
# パスワード不要なので、パスワードには触らない。
end
end

# session["devise.user_attributes"]が存在する場合、それとparamsを組み合わせてUser.newできるよう、Deviseの実装をOverrideする。
def self.new_with_session(params, session)
if session["devise.user_attributes"]
new(session["devise.user_attributes"]) do |user|
user.attributes = params
user.valid?
end
else
super
end
end

# ログイン時、OmniAuthで認証したユーザーのパスワード入力免除するため、Deviseの実装をOverrideする。
def password_required?
super && provider.blank? # provider属性に値があればパスワード入力免除
end

# Edit時、OmniAuthで認証したユーザーのパスワード入力免除するため、Deviseの実装をOverrideする。
def update_with_password(params, *options)
if encrypted_password.blank? # encrypted_password属性が空の場合
update_attributes(params, *options) # パスワード入力なしにデータ更新
else
super
end
end
end



新規登録ページでパスワード欄が不要な場合は隠す

例、OmniAuth認証後にemailのみ入力を求める場合。


/app/views/devise/registrations/new.html.haml

= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|

- if params[:from_omniauth_callback]
.alert.alert-danger Email address is required.
- else
= devise_error_messages!

.field.form-group
= f.text_field :username, autofocus: false, class: 'form-control', placeholder: "Username"

.field.form-group
= f.email_field :email, autofocus: false, class: 'form-control', placeholder: "Email"

- if f.object.password_required?

.field.form-group
= f.password_field :password, autocomplete: "off", class: 'form-control',
placeholder: "Password (#{@minimum_password_length} characters min)"

.field.form-group
= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control',
placeholder: "Password confirmation"

.actions
= f.button "Create my account", class: "submit btn btn-success btn-lg btn-block",
data: { disable_with: "<i class='fa fa-spinner fa-spin'></i> Processing..." }



参考文献