OmniAuth認証についてのメモ。
概要
- Deviseで認証を行うRailsアプリに対して、OmniAuthでの認証機能を追加する。
- Twitterをプロバイダーとする。
- OmniAuthで認証する場合は、パスワード入力を免除する。
- 新規登録時、emailは必須項目とする。
- Deviseのconfirmableモジュールでemailの確認をする。
手順
##認証してもらいたいプロバイダーにアプリを登録し、APIキー等を取得
TwitterのApplication Managementサイトに行き、フォームに必要事項を記入して提出
- 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を確認。
Gemfileにomniauth-twitterを追加
#...
gem 'omniauth-twitter'
#...
$ bin/bundle
##API KeyとAPI Secretを/config/secrets.yml
に加える
直接ペーストせずに環境変数経由で渡すようにするとよい。
これで以降は、Development/Producton環境を問わずRails.application.secrets.twitter_api_key
、Rails.application.secrets.twitter_api_secret
としてAPI Key
とAPI Secret
にアクセス可能になる。
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
から自動で環境変数に割り当てることも可能。
#...
# ==> 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行目あたりにプレースホルダーがある。
#...
# ==> 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と衝突し、認証が全て失敗してしまう。
# 注意: 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モデルに追加
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
に行ってリンクがあるかどうか確認する。
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に登録する
Rails.application.routes.draw do
#...
devise_for :users, controllers: { omniauth_callbacks: 'omniauth_callbacks' }
#...
end
認証失敗時の取り扱い等の機能を取り込むため、super classはDevise::OmniauthCallbacksControllerとする。
複数のプロバイダーを利用する場合も内容は同じなので、alias_methodを用い、同じコードを複数のプロバイダーに対して使用できるようにする。
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認証処理に使用するメソッドを追加
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のみ入力を求める場合。
= 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..." }