👩‍💻

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

2021/05/23に公開

2022/3/31 Rails7で動作するように修正したものを追記しました。

はじめに

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

「Rails Googleでログイン」でググると、deviseとomniauth-google-oauth2で実装してるのがたくさん出てきましたが、Zennのように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にログイン関連メソッドを追加

あとで使うので先に作っておきます。ついでにログインチェックも入れておきます。
(SessionsHelperはRailsチュートリアル風です)

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
  skip_before_action :check_logged_in, only: :create
  
  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>

ログインのlink_tomethod: :postを入れないといけない理由はすぐ後で触れます。
これでログインのリンクをクリックすると、見慣れたGoogleでログインの画面に遷移し、画像も名前も表示されました。(追記: Rails7で試すと、link_toを使うとうまく動作しませんでした!詳細は後述)

この時点でmethod: :postと、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を入れる必要があるというわけでした。

5. 追記: Rails7でログイン画面に遷移しない問題

Rails7にアップデートした上でここまでの手順を試すと、ログインのlink_toでうまく動かなくなっていましたので、追記しておきます。

  • ruby 3.1.0
  • Rails 7.0.2.3

先に結論を書いておくと

<%= button_to "Googleでログイン", "/auth/google_oauth2", method: :post, data: { turbo: false } %>

でうまくいきました。なぜこれでないといけないのか、以下に書いていきます。

Rails7ではRailsUJSの代わりにTurboが使われるようになった影響で、link_tomethodオプションが使えなくなっているようです。
Turboのドキュメントを見ると、

<a href="/articles/54" data-turbo-method="delete">Delete the article</a>

というふうにdata-tubo-method属性が使えると書いてあります。
また、RailsGuidesにはlink_toでの使い方がしれっと書いてありました。

これに倣うと、先ほどのlink_toは以下のようになります.

app/views/layouts/application.html.erb
- <%= link_to "Googleでログイン", "/auth/google_oauth2", method: :post %>
+ <%= link_to "Googleでログイン", "/auth/google_oauth2", data: { turbo_method: :post } %>

一方、link_toとは違って、button_tomethodオプションはRails7でも健在のようです。こちらの場合は以下のようになります。

app/views/layouts/application.html.erb
- <%= link_to "Googleでログイン", "/auth/google_oauth2", method: :post %>
+ <%= button_to "Googleでログイン", "/auth/google_oauth2", method: :post } %>

どちらのやり方でもGETではなくPOSTリクエストになりました。Routing Error: No route matches [GET] "/auth/google_oauth2"のエラーを直すための選択肢は2つあることになります。
どちらでもいいかと思いきや...?

2/2. 「Googleでログイン」ボタンを押すとCORSのエラーが出る問題

上記2つの実装どちらも、「Googleでログイン」ボタンを押してもGoogleログイン画面に遷移せず、開発者ツールのログに以下のようなエラーが出ます。

Access to fetch at 'https ://accounts.google .com/o/oauth2/auth?access_type=offline&client_id=xxxxxxxx.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fgoogle_oauth2%2Fcallback&response_type=code&scope=email+profile&state=xxxxxxxxxxxx' (redirected from 'htt p://localhost:3000/auth/google_oauth2') from origin 'htt p://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

Access-Control-Allow-Originヘッダーが要求されたリソースに存在しない??いろいろ調べてrack-corsgemを試したりしましたが、全く変化がありません。

結局、こちらの記事に答えを見つけました。ログインボタンではTurboをオフにせよとのことです。
これら↓
https://github.com/hotwired/turbo/issues/45
https://github.com/hotwired/turbo/issues/74
のissueで言われているように、
data-turbo=falseをつけるやり方があるようです。
この書き方で、最終的にうまくいきました。

app/views/layouts/application.html.erb
- <%= link_to "Googleでログイン", "/auth/google_oauth2", method: :post %>
+ <%= button_to "Googleでログイン", "/auth/google_oauth2", method: :post, data: { turbo: false } %>

上述した通り、Rails7においてlink_toでPOSTリクエストを飛ばすにはdata: { turbo_method: :post }を使うしかないのですが、これはdata: { turbo: false }とは共存できないようです(Turboを使わないようにするので当然ですが。試しにdata: { turbo_method: :post, turbo: false }としてみると、POSTではなくGETリクエストになってしまいました)。
そのため、link_toではダメでbutton_toを使うしかないという結論にいたりました。

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

Discussion