😸

【Rails7】Googleログインを公式ドキュメントに沿って実装する

2022/08/05に公開
1

こんにちは。Googleカレンダーと連携して家族や自分のLINEにスケジュールを通知できるアプリ「トリコミ」を個人開発でリリースしました。

https://torikomi.fly.dev/

このアプリの中でGoogleログインを使っています。公式ドキュメントに沿って実装してみたら意外と簡単だったので、ここにそのやり方をまとめておきます。

環境

以下の環境で作成しています。

  • macOS 11.6.2
  • Ruby 3.1.2
  • Rails 7.0.3
  • PostgreSQL 14.4

下準備

説明のためサンプルアプリを作成します。後半の--css tailwindは、TailwindCSSというCSSをあてるためのコマンドなので、省略しても大丈夫です。見映えをよくしたいので今回は入れました。

またデータベースにPostgreSQLを指定していますが、これも各自の環境にあわせてお好きなものを。

$ rails new google_login_app --css tailwind --database=postgresql

Userモデルを作成します。ユーザーの認証はGoogle側にお任せするので、パスワード関連のカラムは作成しません。

bundle exec rails g model User email:string

emailカラムにnot null制約とunique制約をかけてデータベースを作成。

db/migrate/XXXXXXX_create_users.rb
class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :email, null: false, index: { unique: true }

      t.timestamps
    end
  end
end
$ bundle exec rails db:create
$ bundle exec rails db:migrate

モデル側にも同じ制約をかけます。

app/models/user.rb
class User < ApplicationRecord
  validates :email, presence: true, uniqueness: true
end

最後にログイン前とログイン後を識別するためのページを適当に作成しておきます。

$ bundle exec rails g controller StaticPages before_login after_login
config/routes.rb
Rails.application.routes.draw do
  # 下2行を追加
  root 'static_pages#before_login'
  get '/after_login', to: 'static_pages#after_login'
end
app/views/static_pages/before_login.html.erb
<div>
  <script src="https://accounts.google.com/gsi/client" async defer></script>
  <% if notice.present? %>
    <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
  <% end %>
  <h1 class="font-bold text-4xl">ログイン前のページ</h1>
</div>
app/views/static_pages/after_login.html.erb
<div>
  <script src="https://accounts.google.com/gsi/client" async defer></script>
  <% if notice.present? %>
    <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
  <% end %>
  <h1 class="font-bold text-4xl">ログイン後のページ</h1>
</div>

Googleログインの公式ドキュメントを読む

はい! こちらが、Googleログインの公式ドキュメントです。

https://developers.google.com/identity/gsi/web

サムネは英語ですが、本体ページへ飛べば若干あやしめの日本語で読めます。Google翻訳なのかな。

ガイドを読んでいくと、どうやら“古いGoogleログイン”と“新しいGoogleログイン”というものがあり、“新しいGoogleログイン”を使うとパーソナライズされたボタンが表示されるそう。


https://developers.google.com/identity/gsi/web/guides/overview から引用

これ以外にも“新しいGoogleログイン”の利点やその仕組みが事細かに説明されており「ほげーなるほどぉー」となること請け合いなので、まずは公式ドキュメントを一読するのをおすすめします。

今回はこちらの“新しいgoogleログイン”のやり方で進めていきます。

Google APIの設定を行う

https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid

こちらに書いてある通り、Google API Consoleへアクセスし、アプリがGoogleログインを使うのに必要なクライアントIDを発行したり、反対にGoogle側がOAuthの使用を許可するドメインに自分のアプリのドメインを登録したりしていきます。

まず、適当なプロジェクトを作成して……。

「OAuth同意画面」から設定を進めていきます。「User Type」は「外部」を選択。

アプリ名やユーザーサポートメールの宛先など必須項目を埋めていきます。

ここでアプリ名にgoogleが含まれるとエラーになるようです。注意!

続いてアプリのユーザーにどこまで許可を求めるかというスコープの設定をします。公式ドキュメントには、「(Googleログインの)認証にはデフォルトのスコープ(メール、プロファイル、openid)で十分」とあるので、その3つを選択します。

最後にテストユーザーに自分のメールアドレスを登録。

続いて「認証情報」を設定してクライアントIDを発行します。上部の「+ CREATE CREDENTIALS」をクリックして「OAuth クライアントID」を選択します。

アプリケーションのURLは「ウェブアプリケーション」を選択。

公式ドキュメントに書いてある通り、「承認済みのJavaScript生成元」にはhttp://localhosthttp://localhost:3000を入力。

「承認済みのリダイレクトURI」には、Googleログイン後のユーザーのリダイレクト先を設定します。Rails側のルーティングをまだ設定していませんが、リダイレクト後の処理はGoogleLoginApiコントローラを作成のcallbackアクションが受け持つ想定で、http://localhost:3000/google_login_api/callbackと入力。

クライアントIDとクライアントシークレットが発行されました。Googleログインで使うのはこのうちクライアントIDだけですが、どこかにメモするかJSONをダウンロードしましょう。もしダウンロードし忘れても、クライアントIDとクライアントシークレットはGoogle API Consoleからいつでも確認することができます。

これでGoogle API Consoleでの作業は終わりです。

Googleログインボタンを実装する

https://developers.google.com/identity/gsi/web/guides/display-button

次はビューにGoogleログインボタンを追加します。上記のガイドに載っているサンプルコードをそのまま使います。

YOUR_GOOGLE_CLIENT_IDには先ほど発行したご自身のクライアントIDを入れてください。もしアプリを外部に公開する場合は、クライアントIDは環境変数などに設定してそこから呼び出すようにしましょう。

data-login_uriに、callback先のURLを設定します。

app/views/static_pages/before_login.html.erb
<div>
  <!-- クライアントライブラリの読み込み 下記1行を追加 -->
  <script src="https://accounts.google.com/gsi/client" async defer></script>

  <% if notice.present? %>
    <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
  <% end %>
  <h1 class="font-bold text-4xl">ログイン前のページ</h1>

  <!-- Googleログインボタンの表示 ここから追加 -->
  <div id="g_id_onload"
    data-client_id="YOUR_GOOGLE_CLIENT_ID"
    data-login_uri="http://localhost:3000/google_login_api/callback"
    data-auto_prompt="false">
  </div>
  <div class="g_id_signin"
    data-type="standard"
    data-size="large"
    data-theme="outline"
    data-text="sign_in_with"
    data-shape="rectangular"
    data-logo_alignment="left">
  </div>
  <!-- ここまで追加 -->
</div>

Railsサーバーを立ち上げてhttp://localhost:3000にアクセスすると、Googleログインボタンが表示されました!

クリックするとおなじみのウィンドウが開きます。これより先に進もうとするとその後の処理を書いていないのでエラーになります。

なお、ボタンのデザインをカスタマイズしたい場合は、こちらのリファレンスを参考にするといいと思います。

https://developers.google.com/identity/gsi/web/reference/html-reference

GoogleLoginApiコントローラを実装する

ユーザーがGoogle側で認証されると、ユーザー情報とともにアプリにPOSTリクエストが返ってきます。このPOSTリクエストの宛先は、Google API consoleおよびGoogleログインボタンのHTMLで、/google_login_api/callbackと指定しました。

ここでは新しくGoogleLoginApiコントローラを作成し、/google_login_api/callbackに返ってきたPOSTリクエストからユーザー情報を取得します。

まずはコントローラの作成から。

$ bundle exec rails g controller GoogleLoginApi callback

そして、/google_login_api/callbackに来たPOSTリクエストをGoogleLoginApiコントローラのcallbackアクションで受けるようにルーティングを追加します。

config/routes.rb
Rails.application.routes.draw do
  root 'static_pages#before_login'
  get '/after_login', to: 'static_pages#after_login'
  # 下記を追加
  post '/google_login_api/callback', to: 'google_login_api#callback'
end

そしてその後の処理なのですが……頼りにしてきた公式ドキュメントも、このあたりの説明から日本語が崩壊し、ついに英語がそのまま掲載される事態になっています。なんでこうなるのw

https://developers.google.com/identity/gsi/web/guides/verify-google-id-token

周辺情報も探しながら読み解くと、

  1. CSRF対策のため、cookieとパラメータに含まれている、g_csrf_tokenを検証する
  2. パラメータのcredentialsをデコードすればpayload(ユーザー情報)が得られる

ということがわかりました。

下記のブログが、英語ですがRailsのコード付きでわかりやすいです。

https://t27duck.com/posts/10-integrating-google-one-tap-in-a-rails-application

1.について説明を加えると、Railsではデフォルトでprotect_from_forgeryというCSRF対策のメソッドが走っており、外部ドメインからのPOSTリクエストをそのまま受けることができません。

https://qiita.com/KumatoraTiger/items/cc6a1107374cce500e6d

https://railsguides.jp/security.html#クロスサイトリクエストフォージェリ(csrf)

今回は、Google側が用意したg_csrf_tokenの検証によりCSRF対策を行うこととし、代わりにGoogleLoginApiコントローラではprotect_from_forgeryをスキップします。

そして、g_csrf_tokenの検証の仕方ですが、cookieにg_csrf_tokenが存在し、かつパラメータ(リクエストボディ)にg_csrf_tokenが存在し、かつ両者が同一であればOKです。

2.のcredentialからpayloadへのデコードについて、こちらはGoogleが用意しているGoogle Authのライブラリが使えます。

https://github.com/googleapis/google-auth-library-ruby

このライブラリを使うためにはgemのインストールが必要なので、Gemfilegem 'googleauth'を追加してbundle installしましょう。

Gemfile
gem 'googleauth'
$ bundle install

これで準備が整いました。callbackアクションのソースコードをまとめると下記となります。

app/controllers/google_login_api_controller.rb
class GoogleLoginApiController < ApplicationController
  require 'googleauth/id_tokens/verifier'

  protect_from_forgery except: :callback
  before_action :verify_g_csrf_token

  def callback
    payload = Google::Auth::IDTokens.verify_oidc(params[:credential], aud: 'YOUR GOOGLE CLIENT ID')
    user = User.find_or_create_by(email: payload['email'])
    session[:user_id] = user.id
    redirect_to after_login_path, notice: 'ログインしました'
  end

  private

  def verify_g_csrf_token
    if cookies["g_csrf_token"].blank? || params[:g_csrf_token].blank? || cookies["g_csrf_token"] != params[:g_csrf_token]
      redirect_to root_path, notice: '不正なアクセスです'
    end
  end
end

まず、before_actionverify_g_csrf_tokenをメソッドを実行します。不正なアクセスである場合はルートページにリダイレクトされます。

g_csrf_tokenの検証がOKだった場合、callbackアクションが実行されます。

googleauthライブラリのメソッドを使って、戻ってきたcredentialsをデコードします(ここでYOUR GOOGLE CLIENT IDの部分には、ご自身のクライアントIDを入れることをお忘れなく!)。

デコードされたpayloadにはユーザー情報が入っています。中身を見てみますと……。

{
  "iss": "https://accounts.google.com",
  "nbf":  161803398874,
  "aud": "314159265-pi.apps.googleusercontent.com", // あなたのクライアントID
  "sub": "3141592653589793238", // ユーザーのGoogleアカウントのユニークなID
  "hd": "gmail.com",
  "email": "elisa.g.beckett@gmail.com", // ユーザーのemailアドレス
  "email_verified": true,
  "azp": "314159265-pi.apps.googleusercontent.com",
  "name": "Elisa Beckett",
  "picture": "https://lh3.googleusercontent.com/a-/e2718281828459045235360uler",
  "given_name": "Elisa",
  "family_name": "Beckett",
  "iat": 1596474000,
  "exp": 1596477600,
  "jti": "abc161803398874def"
}

こんな感じ(公式より引用しました)。メールアドレス以外にも名前やプロフィール写真などいろいろ取得できます。

https://developers.google.com/identity/gsi/web/reference/js-reference#credential

今回はemailをキーとしてUserモデルをfind_or_createして、セッションにユーザーIDを記憶させて簡易ログイン機構とし、ログイン後のページに遷移させます。

はい、これでGoogleログインが完成しました! あとはよくあるセッション管理のパターンをあてはめていけばよいと思います。

本番環境では?

本番環境にデプロイするときにやることは、

  1. Google API Consoleで、承認済みの JavaScript 生成元、承認済みのリダイレクト URIを本番環境のものに差し替える
  2. Google API Consoleで、アプリの公開ステータスを本番環境に切り替える
  3. app/views/static_pages/before_login.html.erbのGoogleログインボタンのHTMLのdata-login_uriを本番環境のものに差し替える

の3点です。

感想

RailsアプリにGoogleログインを実装する方法は、Auth0というサービスを使ったり、omniauthというgemを使うケースをよく見かけるのですが、なるべく公式ドキュメントに沿った形で実装するとこうなるよーという例でした。

個人開発したアプリではsorceryというgemを使って基本のユーザーモデルとログイン機構を作り、そこにあとからGoogleログイン機能を足したのですが、そのような拡張をするケースでも公式ドキュメントに沿った形だとやりやすかったです。

https://auth0.com/

https://github.com/zquestz/omniauth-google-oauth2

https://github.com/Sorcery/sorcery

それにしても、実際のアプリにコードを書くより、こうやってブログにまとめるほうが何倍も時間がかかりました(おかげでRailsのCSRF対策に詳しくなりましたが…)。

もしこの記事が参考になりましたら、いいねボタンを押していただけると非常に喜びます。

Discussion

yyyymmddyyyymmdd

GCPでプロジェクト作成や、OAuth2.0 クライアントIDは料金はかかるのでしょうか?
ポートフォリオサイトで使用したいのですそれほどリクエスト回数は多くないとは思うのですが、「1ヶ月○回リクエストを読んだら料金が発生する」とかあればご教示いただきたいです。