No day younger than today

RubyとかRailsとか蒙古タンメンとか

【Rails】DeviseとOmniauthでGoogleログインを実装する

こんにちは、ふーがです。
フィヨルドブートキャンプでプログラミングを学習しています。

現在は自作サービスを作成しているところで、そのサービスでGoogleログインを導入したので、その手順を残しておきます。

環境

前提

devise gemを使用してユーザー認証機能を実装している前提です。
deviseでの ユーザー認証はたくさん記事があるので検索してみてください。

github.com

GoogleのクライアントIDとクライアントシークレットの取得

以下の記事がわかりやすく、手順も変わっていなさそうなので参照してください。

zenn.dev

1点だけ注意点があり、手順9/10で入力するリダイレクトURIだけ変える必要があります。
具体的には、deviseを使う場合、リダイレクトURLの欄は

http://localhost:3000/users/auth/google_oauth2/callback

とする必要があります。

取得したクライアントIDとクライアントシークレットは.envファイルに記述しておくと良いでしょう。

Gemのインストール

# Gemfile
gem 'omniauth-google-oauth2'
gem 'omniauth-rails_csrf_protection'

omniauth-rails_csrf_protectionは、RailsでOmniauth gemを使用する際の、クロスサイトリクエストフォージェリによる攻撃を緩和するためのgemです。
現在(2021年9月)はこれが必須のようですが、omniauth-google-oauth2をbundleしても自動では入らないので手動でbundleする必要があります。

github.com

また、認証リンクへのリクエストメソッドはPOSTである必要があります。
button_tolink_to ..., method: :postを使わないと動かないので注意してください。

omniauth用のカラムを作成する

migrationを用意して、Userテーブルにカラムを追加します。

$ bin/rails g controller AddOmniauthToUsers proviser:string uid:string
$ bin/rails g db:migrate

Rails側の設定をする

まずはconfig/initializers/devise.rbに以下の設定を追記します。

# config/initializers/devise.rb
config.omniauth :google_oauth2, ENV['GOOGLE_ID'], ENV['GOOGLE_SECRET'], {}

ルーティングも追記します。

#config/routes.rb
devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }

deviseを導入した際にdevise_for :usersは追加されているはずなので、今回はcontrollers: ...の記述を追加していることになります。
これはcallback時に使用するコントローラーを明示的に指定しています。

ここで指定したコントローラーを実装します。
今後、他にもサービスプロバイダが増える可能性があるので、少し抽象度を上げた実装にしています。

# app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  skip_before_action :verify_authenticity_token, only: :google_oauth2

  def google_oauth2
    callback_for(:google)
  end

  def callback_for(provider)
    @user = User.from_omniauth(request.env['omniauth.auth'])

    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication
      set_flash_message(:notice, :success, kind: provider.to_s.capitalize) if is_navigational_format?
    else
      session["devise.#{provider}_data"] = request.env['omniauth.auth'].except(:extra)
      redirect_to new_user_registration_url
    end
  end

  def failure
    redirect_to root_path
  end
end

コントローラーに記述があるとおり、User#from_omniauthを実装する必要があるので、Modelの方に実装していきます。

# app/models/user.rb
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]
  end
end

以上で設定完了です。

deviseはデフォルトでGoogle認証がマッピングされた時にはログインページにGoogleログイン用のリンクを出してくれるようになっています。
ここまで設定が終わればそのリンクが表示されるようになっているはずです。

providerとuidにindexとunique制約を付ける

同じサービスプロバイダから何度も登録できないように、providerとuidの組み合わせが一位になるようにunique制約を付けます。

$ bin/rails g migration AddIndexUidAndProviderToUsers

生成されたマイグレーションファイルを以下のように修正して、マイグレートを実行します。

class AddIndexUidAndProviderToUsers < ActiveRecord::Migration[6.1]
  def change
    add_index :users, %i[uid provider], unique: true
  end
end

あわせて、Model側にもバリデーションを設定します。

# app/models/user.rb
validates :uid, presence: true, uniqueness: { scope: :provider }, if: -> { uid.present? }

通常登録時にuidを埋める処理

前項で実行したマイグレートにより、omniauthを使用しない通常の登録時に問題が発生します。
通常の登録だと、uidもproviderも空文字で登録されるため、2人目以降のユーザー登録時にunique制約に引っかかってしまうわけです。

この問題に対処するため、通常のユーザー登録時にuidを埋める処理を差し込みます。

そのために、Devise::RegistrationsControllerを継承したUsers::RegistrationsContorllerを作成します。

# app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
  def build_resource(hash = {})
    hash[:uid] = User.create_unique_string
    super
  end
end

Devise::RegistrationsControllerで定義されているbuild_resourceメソッドをオーバーライドして、uidにランダムな文字列を詰め込んだ後、superを呼んで元の処理に返しています。
create_unique_stringメソッドは下記のようにしています。

# app/models/user.rb
def self.create_unique_string
  SecureRandom.uuid
end

メソッドをオーバーライドしたので、コントローラーの向き先も変更します。

# config/routes.rb
devise_for :users, controllers: {
  omniauth_callbacks: 'users/omniauth_callbacks',
  registrations: 'users/registrations'
}

Google経由で登録したユーザーの更新

deviseで実装したユーザー情報の更新は、デフォルトだと変更するときにパスワードの入力が必要です。
しかし、omniauthで登録したユーザーパスワードを自分で設定しないため、パスワードを入力できません。
このままだとメールアドレスの変更もできない状態になってしまうので、この問題に対処します。

パスワードの更新時以外はパスワードの入力を必須でなくします。

先ほど作成したapp/controllers/users/registrations_controller.rbに以下を追記します。

# app/controllers/users/registrations_controller.rb
def update_resource(resource, params)
  return super if params['password'].present?

  resource.update_without_password(params.except('current_password'))
end

これもDevise::RegistrationsControllerで定義されているメソッドをオーバーライドしている形です。
passwordフィールドが入力されている場合はsuperを呼んでそれ以外(パスワード更新時以外)は、パスワードなしで更新できるようにしています。

まとめ

omniauth-rails_csrf_protectionを入れなければいけないことと、認証リンクのHTTPメソッドはPOSTにしなくてはいけないという部分で若干ハマりました。

もしGoogleログインを実装する機会があれば、お気をつけください。