OmniAuthを利用したソーシャルログイン機能の実装方法

【結論】
・ソーシャルログイン機能を実装するには、
 コールバック機能やsessionといったrailsの幅広い
 知識が無いとエラーに対応出来ない。

・コピペでは動かない


【目次】

【本題】

ソーシャルログインを実装したが・・・

先日から取り組んでいたソーシャルログインが、
とりあえずローカル環境で動作出来るところまで実装できました。

但し、かなり無理矢理に実装したので、
実運用には耐えられないかもしれないです・・・

今回は、その実装例をまとめました。
基本的に、下記のサイトを参考にしています。

github.com


0:deviseでアカウント機能を実装

まず、deviseでアカウント認証機能を実装します。

1:Gemfileの導入

今回は、omniauthを利用してソーシャルログインを実装するので、
omniauthのgemを導入します。

omniauthは、メインのgemと各ソーシャルメディア毎のgemがあるので、
それぞれを導入します。今回は、FacebookGoogleの二つにしています。

また、ソーシャルメディアのアクセスキーを環境変数で扱うのですが、
今回は、以前導入したdotenv-railsをそのまま利用します。

gem 'dotenv-rails'
gem 'omniauth'
gem 'omniauth-facebook'
gem 'omniauth-google-oauth2'

2:Userテーブルにカラムを追加

OmniAuthによるSNS連携では、
サービス名を「provider」、各サービスのユーザーIDを「uid」
というカラム名で管理します。

なので、それらを保存する為のカラムを新たに追加する必要があります。

下記のコマンドでカラムを追加するマイグレーションファイルを生成し、
マイグレーションを実行します。

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

3:アプリIDとapp secretを取得

ソーシャルメディアにアクセスするには、
開発者用のIDとシークレットキーが必要になります。
下記の開発者用ページから、取得が可能です。

developers.facebook.com

console.developers.google.com


取得方法は、下記のQiitaが参考になります。

qiita.com

4:アプリIDとapp secretを設定

deviseの設定ファイルにOmniAuth連携用の設定を記述します。

・config/initializers/devise.rb

  config.omniauth :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET']
  config.omniauth :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET']

環境変数を用いますが、dotenv-railsを導入済みなので、
ルートディレクトリに「.env」ファイルを作成し、そちらに環境変数を記述します。

・.env

FACEBOOK_KEY = '223**************************9'
FACEBOOK_SECRET = 'c761***********************************bd9'
GOOGLE_CLIENT_ID = '771*************************************************************com'
GOOGLE_CLIENT_SECRET = 'M*********************L'|

これでRails側でENV['環境変数名']として指定した値を利用することができます。

4:deviseのコントローラーを生成

SNSへアクセスして、正常に認証された後の処理は
deviseのコントローラーが担いますが、このままだと上手く連携させられない為、
deviseのコントローラーの挙動を変える必要があります。
その為、ターミナルより、下記コマンドを実行して、deviseのコントローラーを作成します。

rails g devise:controllers users

更に生成したコントローラーを有効にする為に、
ルーティングを再定義します。

・config/routes.rb

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

この辺りの詳しい事は、過去記事で取り上げています。

https://ryoutaku-jo.hatenablog.com/entry/2019/02/05/192803ryoutaku-jo.hatenablog.com

5:コールバック時の処理を記述する

※この辺りから我流で無理矢理動かすコードになっています。

SNS認証されたコールバック時の処理は、
先ほど生成された「omniauth_callbacks_controller.rb」で記述できます。

・app/controllers/users/omniauth_callbacks_controller.rb

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    callback_from :facebook
  end

  def google_oauth2
    callback_from :google
  end

  private

  def callback_from(provider)
    provider = provider.to_s
    @user = User.find_for_oauth(request.env['omniauth.auth'])

    if @user.persisted?
      flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: provider.capitalize)
      sign_in_and_redirect @user, event: :authentication
    else
      session[:nickname] = @user.nickname
      session[:email] = @user.email
      session[:password] = @user.password
      session[:provider] = @user.provider
      session[:uid] = @user.uid
      redirect_to new_user_registration_sns_path
    end
  end
end

「@user = User.find_for_oauth(request.env['omniauth.auth'])」という記述で、
SNSから取得したユーザー情報を@user に渡します。
find_for_oauthメソッドは、モデルで後ほど定義します。

なお、ここでの記述内容は、
公式のwikiや、Qiitaなどで紹介されている方法とかなり異なる部分があります。

具体的に違うのは、下記2点です。

・sessionの記述
公式では、「session["devise.facebook_data"]」としていましたが、
これだと、ニックネームなど個々の値を取得するのが面倒だったので、
sessionに格納する時点で、キー毎に分けています。

・リダイレクト先のパス
公式は「new_user_registration_path」ですが、
「new_user_registration_sns_path」にしています。

本家メルカリだと、SNSで登録する場合、
メールアドレスとパスワードの入力が省略されますが、
通常の登録フォームと兼用させるのが難しかったので、
別にビューを用意して、そちらに飛ぶようにしています。

6:コールバック用コントローラーで呼び出すメソッドを定義する

まずUserモデルにOmniAuthを有効化する記述を以下の通り追加します。

・app/models/user.rb

  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :omniauthable, omniauth_providers: %i[facebook google_oauth2]

次にomniauth_callbacks_controllerで利用する
SNS認証で得られたユーザー情報を取得するメソッドを定義します。

・app/models/user.rb

  def self.find_for_oauth(auth)
    user = User.where(uid: auth.uid, provider: auth.provider).first

    unless user
      user = User.create(
        uid:      auth.uid,
        provider: auth.provider,
        nickname:     auth.info.name,
        email:    User.dummy_email(auth),
        password: Devise.friendly_token[0, 20]
      )
    end

    user
  end

  private

  def self.dummy_email(auth)
    "#{auth.uid}-#{auth.provider}@example.com"
  end

ここも公式と大きく違う点があります。
それは、メールアドレスの記述です。

公式通り「auth.info.email」で取得を試みたのですが、
一向に取得できず、scopeや自身のFacebookアカウントなど、
色々調査したのですが、八方塞がりでした・・・

そこで、メールアドレスの取得は諦めて、ダミーのメールアドレスを生成する事にしました。
uidはprovider毎に一意の値なので、それを組み合わせたメールアドレス("#{auth.uid}-#{auth.provider}@example.com")を
userのメールアドレスとする記述になっています。

7:SNS認証で登録する際のコントローラーの挙動を編集する

sessionに保存した値を、フォームに出力して、
フォームをsubmitした際に、sns認証用の情報もDBに保存される様に記述します。

なお、こちらの記述は公式にはありません。

class Users::RegistrationsController < Devise::RegistrationsController

  def sns
    @user = User.new(
      nickname: session[:nickname],
      email: session[:email],
      password: session[:password],
      password_confirmation: session[:password],
      )
  end

  def create
    super
    @user.uid = session[:uid]
    @user.provider = session[:provider]
    @user.save
  end
end

8:SNS認証用のリンクを貼る

最後に、SNS認証用のリンクを貼ります。

= link_to "Facebookでログイン", user_facebook_omniauth_authorize_path
= link_to "Googleでログイン", user_google_oauth2_omniauth_authorize_path


以上で、作業完了です。

総括

初めはコピペで簡単に出来ると高をくくっていましたが、
コールバック機能やsessionなど、railsの幅広い知識が無いと
エラーに対処できないので、かなり苦戦しました。

正直、これで良いのか分かりませんが、
とりあえず登録・ログインは出来たので、一旦は良しとしたいと思います。