Tech Racho エンジニアの「?」を「!」に。
  • 開発

[Devise How-To] OmniAuth: 概要(翻訳)

こんにちは、hachi8833です。Devise How-TO翻訳シリーズは、需要の多いOmniAuth関連を当面優先してお送りいたします。どうぞよろしくお願いします。

DeviseでOmniAuthを使うことで、Facebook認証、Twitter認証、Googleアカウント認証、GitHubアカウント認証などを導入できます。本翻訳記事の第一弾は、その基礎にあたる部分の解説です。

Devise Wikiもくじリンク

  1. 「ワークフローのカスタマイズ」
  2. 「認証方法のカスタマイズ」「OmniAuth」
  3. 「ビュー/コンテンツのカスタマイズ」「特権/認証」
  4. 「テスト」「特殊な設定」
  5. 「アプリでのその他の設定」「JavaScript」
  6. 「他の認証プラグインからの移行」「アップグレード」

概要

読みやすさのため、config.omniauthのコードを途中で改行しています。

原文の更新や誤りにお気づきの場合は、ぜひ@techrachoまでお知らせください。更新いたします。

OmniAuth: 概要(翻訳)

Deviseはバージョン1.2からOmniAuthとの統合をサポートしています。このWikiページでは「とあるOAuthプロバイダ」を例に使って連携の基本を解説します。

Deviseバージョン1.5はOmniAuth 1.0以降をサポートするため、本チュートリアルではこれを用います。

始める前に

アプリケーションにはconfig.omniauthによってomniauth providerミドルウェアが追加されることを理解しておいてください。つまり、このproviderミドルウェアをconfig/initializers/omniauth.rbに再度追加してはいけないということです。もし再追加すると両方ともクラッシュし、認証が常に失敗してしまいます。

Facebookの例

最初にアプリにOmniAuth gemを追加します。この場合はGemfileに以下のように記述します。

gem 'omniauth-facebook'

ここではFacebookを例として使いますが、他のどんなOmniAuth gemを使ってもかまいません。gem名は一般に「omniauth-プロバイダ名」の形式になっています。プロバイダ名はfacebookだったりtwitterだったりします。完全なプロバイダリストについてはList of Strategiesをご覧ください。

次に、Userモデルにproviderカラム(文字列)とuidカラム(文字列)を追加します。

rails g migration AddOmniauthToUsers provider:string uid:string
rake db:migrate

次に、使うプロバイダをconfig/initializers/devise.rbで宣言する必要があります。

config.omniauth :facebook, 
                "APP_ID", 
                "APP_SECRET"

注意: v2.0.1にはコールバックURLエラーの問題があります。回避するには、configにcallback_urlを追加する必要があります。

config.omniauth :facebook, 
                "APP_ID", 
                "APP_SECRET",
                callback_url: "CALLBACK_URL"

上のAPP_IDAPP_SECRETは、アプリのidとsecret(秘密情報)に置き換えます。CALLBACK_URLhttps://myapp.com/users/auth/facebook/callbackのような形式にする必要があります。

パーミッションやリクエストされたスコープを変更するには、the omniauth-facebook gemのREADMEをご覧ください。

strategyの設定が終わったら、モデル(app/models/user.rbなど)に以下を記述してOmniAuthを有効にします。

devise :omniauthable, :omniauth_providers => [:facebook]

注意: Railsサーバーを実行している場合は、Deviseイニシャライザに:omniauthableを追記するなどして変更した後にRailsを再起動しないとエラーになります。

現時点では、Deviseでomniauthableにできるモデルは1つだけです。OmniAuthで複数のモデルを使いたい場合は、OmniAuth with multiple modelsをご覧ください。

Userモデルをomniauthableにすると、config/routes.rbにdevise_for :usersを記述することで以下のURLメソッドがDeviseによって作成されます。

  • user_omniauth_authorize_path(provider)
  • user_omniauth_callback_path(provider)

Deviseは*_urlというメソッドは作成しません(そのディレクトリより上でコールバックヘルパーを使うことはありえないため)。

Facebookによる認証を提供するには、レイアウトに上の1つ目のメソッドを以下のように追加します。

<%= link_to "Sign in with Facebook", user_facebook_omniauth_authorize_path %>

注意: user_omniauth_authorize_pathメソッドに渡されるシンボルは、Deviseのconfigブロックに渡されるプロバイダのシンボルと一致します。どのシンボルを渡せばよいかについては、omniauth-*のREADMEをご覧ください。

上のリンクをクリックすると、ユーザーはFacebookにリダイレクトされます(リンクが表示されていない場合は、サーバーを再起動してください)。

Facebook側で認証情報が挿入されると、再度リダイレクトしてアプリケーションのコールバックメソッドに戻ります。コールバックを実装する場合に最初に必要なのは、config/routes.rbを開いて、Omniauthコールバックを実装するコールバックをDeviseに伝えることです。

devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }

次は、以下のコードのみを含むapp/controllers/users/omniauth_callbacks_controller.rbを追加します。

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
end

実装するコールバックのアクションは、プロバイダと同じ名前にする必要があります。facebookプロバイダのアクションの場合、たとえば以下のようなコードをコントローラに追加できます。

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    # 以下のメソッドをモデル(app/models/user.rbなど)で実装する必要がある
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, :event => :authentication #@userが無効だと例外がスローされる
      set_flash_message(:notice, :success, :kind => "Facebook") if is_navigational_format?
    else
      session["devise.facebook_data"] = request.env["omniauth.auth"]
      redirect_to new_user_registration_url
    end
  end

  def failure
    redirect_to root_path
  end
end

このアクションについていくつか補足します。

  1. FacebookからOmniAuthで取得したすべての情報はrequest.env["omniauth.auth"]のハッシュでアクセスできます。取得できる情報の種類については、OmniAuthのドキュメントとと各gem(Facebookの場合はomniauth-facebook)のREADMEをご覧ください。

  2. 有効なユーザーが見つかると、Deviseのsign_inメソッドまたはsign_in_and_redirectメソッドでサインインできます。:event => :authenticationはオプションであり、Wardenのコールバックを使う場合にのみ渡す必要があります。

  3. Deviseのデフォルトのメッセージの中からflashメッセージに流用することも、使わないこともできます。

  4. ユーザーがサイトから離れたら、OmniAuthのデータをセッションに保存します。データの保存では、devise.が名前空間のキーとして使われます。これにより、ユーザーがサインインに使ったセッションからdevise.で始まるすべてのデータがDeviseによって削除され、セッションが自動的にクリーンアップされます。最後に、ユーザーを元の登録フォームページにリダイレクトします。

コントローラを定義したら、モデル(app/models/user.rbなど)で以下のようにfrom_omniauthメソッドを実装します。

def self.from_omniauth(auth)
  where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
    user.email = auth.info.email
    user.password = Devise.friendly_token[0,20]
    user.name = auth.info.name   # Userモデルにnameカラムがあるとする
    user.image = auth.info.image # Userモデルにimageカラムがあるとする
    # Deviseのconfirmableを使い、かつプロバイダがメールのバリデーションを行っている場合は、
    # 以下の行のコメントを解除して確認メールをスキップする
    # user.skip_confirmation!
  end
end

このメソッドは、providerフィールドとuidフィールドを指定して既存のユーザーを検索します。ユーザーが見つからない場合は、ランダムなパスワードなどの情報を追加して新しいユーザーを作成します。

なお、first_or_createメソッドを使うと、ユーザー作成時にproviderフィールドとuidフィールドが自動的に作成されることにご注意ください。first_or_create!メソッドも基本的に同じですが、ユーザーレコードのバリデーションに失敗するとExceptionがraiseされる点が異なります。

訳注: first_or_createメソッドやfirst_or_create!メソッドは以下のようにActiveRecord::Queryingdelegateで実装されていますが、APIドキュメントはありません。

# https://github.com/rails/rails/blob/5-1-stable/activerecord/lib/active_record/querying.rb#L5 より
module ActiveRecord
  module Querying
    ...
    delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all

また、DeviseのRegistrationsControllerはリソースをビルドする前にデフォルトでUser.new_with_sessionを呼び出します。つまり、ユーザー登録(サインアップ)前にユーザーが初期化された場合にセッションのデータをコピーする必要がある場合は、モデルでnew_with_sessionのみを実装すればよいということです。

以下のコード例では、Facebookのメールが取得できる場合にメールをコピーします。

class User < ApplicationRecord
  def self.new_with_session(params, session)
    super.tap do |user|
      if data = session["devise.facebook_data"] && session["devise.facebook_data"]["extra"]["raw_info"]
        user.email = data["email"] if user.email.blank?
      end
    end
  end
end

最後に、ユーザーがFacebookでのユーザー登録(サインアップ)をキャンセルできるようにするには、ユーザーをcancel_user_registration_pathにリダイレクトするようにします。これにより、Deviseが開始したセッションデータはすべて削除され、上のnew_with_sessionフックがその後呼ばれることはなくなります。

ログアウト用リンク

# config/routes.rb
devise_scope :user do
  delete 'sign_out', :to => 'devise/sessions#destroy', :as => :destroy_user_session
end

上のコードだけでできます。動作がうまく統合されるようになったら、以下を参考に統合テストを書きましょう。

OmniAuthだけで認証する

他の認証方法を使わずOmniAuthだけで認証する場合、new_user_sessionという名前のルーティングを定義する必要があります(未定義の場合はrootが使われます)。

この場合のルーティング例を以下に示します。OmniAuthでデータベースや他の認証方法を併用している場合、以下は不要です。

devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }

devise_scope :user do
  get 'sign_in', :to => 'devise/sessions#new', :as => :new_user_session
  get 'sign_out', :to => 'devise/sessions#destroy', :as => :destroy_user_session
end

上のルーティングを使う場合、sessionsコントローラには何も処理を追加しません。たとえばプロバイダの認証リンクを表示できればよいのであれば、これで十分です。

注意: Deviseでsign_out_via = :deleteを設定している場合は、sign_outルーティングがDELETEリクエストにマッチするようコードの修正が必要になることがあります。

また、:database_authenticatableを使っていない場合は、以下のようにnew_session_path(scope)ヘルパーメソッドを定義して、失敗した場合に正しくリダイレクトする必要があります。

class ApplicationController < ActionController::Base
# ...
  def new_session_path(scope)
    new_user_session_path
  end
end

トラブルシューティング

ユーザーがメールアドレスの利用を許可していなかった場合

Facebookの新しいログインダイアログでは、ユーザーはメールアドレスの入力を拒否できるようになっています。通常、Deviseの登録ではメールが必要になります。こういう場合の手っ取り早い解決方法は、メールアドレスがない場合は再度リクエストするというものです。

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    if request.env["omniauth.auth"].info.email.blank?
      redirect_to "/users/auth/facebook?auth_type=rerequest&scope=email"
    end
  end
end

Facebookから返されたメールアドレスが空になっている場合

2015年7月8日以降、FacebookはAPIをv2.4に更新しました。このため、emailフィールドにアクセスするにはinfo_fieldsの追加が必要です。

config.omniauth :facebook, 
                "APP_ID", 
                "APP_SECRET", 
                scope: 'email', 
                info_fields: 'email,name'

参考: @techmonsterによる解決方法

OpenSSLエラー

以下のようなOpenSSLエラーが発生することがあります。

OpenSSL::SSL::SSLError (SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed):

この場合、CA証明書ファイルの保存場所をOmniAuthに対して明示的に指定する必要があります。これには、certified gemを使う方法と、以下の方法があります。後者は実行しているOS環境によって異なります。

config.omniauth :facebook, 
                "APP_ID", 
                "APP_SECRET",
                :client_options => {:ssl => {:ca_path => '/etc/ssl/certs'}}

Herokuの場合、CAファイルは/usr/lib/ssl/certs/ca-certificates.crtに保存されます。

Engine Yard Cloud serversの場合、CAファイルは/etc/ssl/certs/ca-certificates.crtに保存されます。

証明書の設定には、:ca_fileキーを使います。

config.omniauth :facebook, 
                "APP_ID", 
                "APP_SECRET",
                :client_options => {:ssl => {:ca_file => '/usr/lib/ssl/certs/ca-certificates.crt'}}

OAuth gemを使うstrategy(omniauth-oauthなど)を利用する場合、次の方法で証明書ファイルを指定します。

config.omniauth :facebook, 
                "APP_ID",
                "APP_SECRET",
                :client_options => {:ca_file => '/usr/lib/ssl/certs/ca-certificates.crt'}

Mac OS Xでは証明書はファイルシステムではなくMacのキーチェーンアプリに保存されるので、Mac OS上での開発中に限れば、以下のように単に証明書の検証を無効にするのが最も簡単です。

require "omniauth-facebook"
config.omniauth :facebook, 
                "APP_ID",
                "APP_SECRET",
                :client_options => { :ssl => { :verify => !Rails.env.development? } }

このエラーの詳しい議論については、#260をご覧ください。

strategyクラスを読み込めない場合

Deviseが何らかの理由でアプリのstrategyクラスを読み込めない場合、以下のように:strategy_classオプションで明示的に指定できます。

config.omniauth :facebook, 
                "APP_ID", 
                "APP_SECRET", 
                :strategy_class => OmniAuth::Strategies::Facebook

関連記事(Devise)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。