👩‍💻

Rails+omniauth-google-oauth2でGoogleログイン(devise無し)

10 min read

はじめに

ユーザーの個人情報はできるだけ持ちたくないですよね。
ユーザー登録・ログインの部分をOAuthを利用してGoogleに丸投げして、DBにパスワードを保存しないようにすれば、パスワード漏洩のリスクは無くすことができます。
この記事では、備忘録のために導入手順をまとめていきます。

「Rails Googleでログイン」でググると、deviseとomniauth-google-oauth2で実装してるのがたくさん出てきましたが、Googleでログインしかしないならdeviseを入れるのも大仰な気がするので、今回はdevise無しでやっていきます。

この記事の目標は、Railsで簡単なサンプルアプリを作って最低限Googleでログインできるようにするところまでです。

Ruby/Railsやgemのバージョンは以下の通りです。

  • ruby 3.0.1
  • Rails 6.1.3.2
  • omniauth 2.0.4
  • omniauth-google-oauth2 1.0.0
  • omniauth-rails_csrf_protection 1.0.0

アプリケーションの準備

1/2. rails new

まずgoogle_login_sampleという名前でRailsアプリを作成します。

$ rails new google_login_sample --skip-test --skip-action-mailer --skip-action-text --skip-action-cable

(今回関係ない機能はskipしました。rails newのオプションについてはドキュメントを参照)

2/2. omniauth-google-oauth2 gemを追加

omniauth-google-oauth2というgemを使います。

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

omniauth-rails_csrf_protectionは最後のほうで入れる必要が出てくるので、今のうちに入れた方が良いです。

$ bundle install

omniauth-google-oauth2をインストールすると、omniauthというgemも追加されます。あとでこのREADMEやwikiも参照します。

omniauth-google-oauth2のREADMEによると、次にGoogle API Setupをしろとのことです。

3. Google APIの設定

https://console.cloud.google.com/apis/dashboard へ行きます。

1/10: プロジェクトを作成をクリック。

(または「プロジェクトの選択」>「新しいプロジェクト」を選択)

2/10: 適当なプロジェクト名をつけて作成。

3/10: メニューの「APIとサービス」>「OAuth同意画面」へ。

4/10: User Typeは「外部」を選択して「作成」。

5/10: アプリ名とメールアドレスを埋めたら「保存して次へ」。

(アプリ名に"Google"が含まれるとエラーになったのでちょっと変えました)

6/10: 「スコープを追加または削除」で「Googleアカウントのメインのメールアドレスの参照」をチェックして「更新」したのち、「保存して次へ」。

7/10: 「+ADD USERS」をクリックして自分のメールアドレスを追加したら、「保存して次へ」。

8/10: 「APIとサービス」のメニューの「認証情報」>「認証情報を作成」>「OAuthクライアントID」を選択。

9/10: アプリケーションの種類を「ウェブアプリケーション」にして、「名前」は適当に

「承認済みのリダイレクトURI」は、http://localhost:3000/auth/google_oauth2/callbackとします。
これは、omniauthのドキュメントに書いてあるまんまの

routes.rb
post '/auth/:provider/callback', to: 'sessions#create'

のようなルーティングに合わせるためです。
「作成」を押すとクライアントIDとクライアントシークレットが出てくるので、コピーしてどこかにメモっておきます。

10/10: 「APIとサービス」のメニューの「ライブラリ」からGoogle+ APIを選択して、「有効にする」をクリック。

以上でGoogle APIの設定は完了です。

4. アプリケーションの中身を記述

1/7. 設定ファイルを作成

omniauth-google-oauth2のドキュメントに戻って、Usageのところを見ます。
config/initializers/omniauth.rbというファイルを作って、クライアントIDとクライアントシークレットを入れろとのことです。ドキュメントではdotenvを使ってるようですが、せっかくなのでcredentialsを使って書くことにします。
(OmniAuth.config.allowed_request_methods = %i[get]を消した理由は最後に出てきます)

config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :google_oauth2,
           Rails.application.credentials.google[:client_id],
           Rails.application.credentials.google[:client_secret]
end

$ EDITOR=vim bin/rails credentials:edit -e developmentで中身を以下のように記述します。

google:
  client_id: さっきコピーしたクライアントID
  client_secret: さっきコピーしたクライアントシークレット

ドキュメントによると、これで/auth/google_oauth2というパスが有効になるとのことです。このパスにPOSTリクエストを送ると、Googleでログインの画面に遷移します。

2/7. ユーザーモデル作成

今回はサンプルアプリで試すだけなのでカラムは最小限にします。

$ bin/rails g model user name email image
$ bin/rails db:migrate

3/7. Controllerを作成してルーティングを設定する

$ bin/rails g controller home index
$ bin/rails g controller sessions
config/routes.rb
Rails.application.routes.draw do
  root 'home#index'

  get 'auth/:provider/callback', to: 'sessions#create'
  get 'auth/failure', to: redirect('/')
  get 'log_out', to: 'sessions#destroy', as: 'log_out'

  resources :sessions, only: %i[create destroy]
end

get 'auth/:provider/callback', to: 'sessions#create'の部分は、omniauthのREADMEに書いてあるまんまにしました。
get 'auth/failure', to: redirect('/')の部分は、omniauthのwikiを参考にしています。wikiの下の方に「プロバイダ側でユーザ認証に失敗した場合、OmniAuthはレスポンスをキャッチしてから、/auth/failureにリクエストをリダイレクトする」とあるので、'auth/failure'を受け取ったらトップにリダイレクトするようにしました。
また、ログアウト用に'log_out'のルーティングも追加しました。

4/7. ApplicationControllerにログイン関連メソッドを追加

あとで使うので先に作っておきます。ついでにログインチェックも入れておきます。

app/helpers/sessions_helper.rb
module SessionsHelper
  def current_user
    return unless (user_id = session[:user_id])

    @current_user ||= User.find_by(id: user_id)
  end
  
  def log_in(user)
    session[:user_id] = user.id
  end

  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include SessionsHelper
  before_action :check_logged_in

  def check_logged_in
    return if current_user

    redirect_to root_path
  end
end

トップページと、ログイン時のSessions#createだけはログインチェックしないでほしいのでskipします。

app/controllers/home_controller.rb
class HomeController < ApplicationController
+  skip_before_action :check_logged_in, only: :index
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
+  skip_before_action :check_logged_in, only: :create

5/7. SessionsControllerにログイン・ログアウト処理を追加

omniauthのREADMEに書いてある感じを参考にSessionsControllerを書いていきます。(本当ならログイン後はトップページ以外にリダイレクトしたいですね)
google-omniauth-oauth2のREADMEを見ると、request.env['omniauth.auth']でauthentication hashにアクセスできると書いてありますので、privateメソッドで定義しておきます。

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def create
    if (user = User.find_or_create_from_auth_hash(auth_hash))
      log_in user
    end
    redirect_to root_path
  end
   
  def destroy
    log_out
    redirect_to root_path
  end

  private

  def auth_hash
    request.env['omniauth.auth']
  end
end

User.find_or_create_from_auth_hash(auth_hash)はこのあと実装します。

6/7. Userモデルにfind_or_create_from_auth_hashメソッド追加

auth_hash、つまりrequest.env['omniauth.auth']にはいろんな情報が入っています。何を取ってくるかはアプリによると思います。
ちなみにfind_or_create_byメソッドは、findで見つからなかった場合のみブロック内の処理を行います。

app/models/user.rb
class User < ApplicationRecord
  class << self
    def find_or_create_from_auth_hash(auth_hash)
      user_params = user_params_from_auth_hash(auth_hash)
      find_or_create_by(email: user_params[:email]) do |user|
        user.update(user_params)
      end
    end
    
    private

    def user_params_from_auth_hash(auth_hash)
      {
        name: auth_hash.info.name,
        email: auth_hash.info.email,
        image: auth_hash.info.image,
      }
    end
  end
end

7/7. Viewを作成、ログインを試す

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
...省略...
  </head>
  <body>
    <header>
+      <% if current_user %>
+        <%= image_tag current_user.image %>
+        <%= current_user.name %>さん
+        <%= link_to "ログアウト", log_out_path %>
+      <% else  %>
+        ゲストさん
+        <%= link_to "Googleでログイン", "/auth/google_oauth2", method: :post %>
+      <% end %>
+    </header>
    <%= yield %>
  </body>
</html>

ログインのリンクにmethod: :postを入れないといけない理由はすぐ後で触れます。
これでログインのリンクをクリックすると、見慣れたGoogleでログインの画面に遷移し、画像も名前も表示されました。

この時点でomniauth-rails_csrf_protectionのgemを入れていないと、ログにwarningが出ます。

WARN -- omniauth: (google_oauth2) You are using GET as an allowed request method for OmniAuth. This may leave you open to CSRF attacks. As of v2.0.0, OmniAuth by default allows only POST to its own routes. You should review the following resources to guide your mitigation:
(訳: あなたはOmniAuthの許可されたリクエストメソッドとしてGETを使用しています。これでは、CSRF攻撃を受けてしまう可能性があります。v2.0.0の時点で、OmniAuthはデフォルトで独自のルートへのPOSTのみを許可しています。ミティゲーションの指針として、以下のリソースを確認する必要があります。)
https://github.com/omniauth/omniauth/wiki/Resolving-CVE-2015-9284
https://github.com/omniauth/omniauth/issues/960
https://nvd.nist.gov/vuln/detail/CVE-2015-9284
https://github.com/omniauth/omniauth/pull/809

要するにOmniAuthでGETを使うとセキュリティの問題があると怒られているわけです。一番上のリンクを見るとomniauth-rails_csrf_protectionのgemを入れろとあるので、素直に入れます。

このgemはHTTP GETメソッドを使用したOAuthリクエストフェーズへのアクセスを無効にして、デフォルトをPOST-onlyに変更するそうです。
link_toを使うとデフォルトでGETメソッドになるため、もしログインのリンクにmethod: :postを入れていなければ、omniauth-rails_csrf_protectionのgemがGETが無効にしているのでMissing templateのエラーになってしまいます。

そのため、ログインのリンクにはmethod: :postを入れる必要があるというわけでした。

以上でGoogleでログインのサンプルアプリは実装完了ということにします。
間違い等のご指摘お待ちしております。

Discussion

ログインするとコメントできます