【Rails7】Googleログインを公式ドキュメントに沿って実装する
こんにちは。Googleカレンダーと連携して家族や自分のLINEにスケジュールを通知できるアプリ「トリコミ」を個人開発でリリースしました。
このアプリの中で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制約をかけてデータベースを作成。
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
モデル側にも同じ制約をかけます。
class User < ApplicationRecord
validates :email, presence: true, uniqueness: true
end
最後にログイン前とログイン後を識別するためのページを適当に作成しておきます。
$ bundle exec rails g controller StaticPages before_login after_login
Rails.application.routes.draw do
# 下2行を追加
root 'static_pages#before_login'
get '/after_login', to: 'static_pages#after_login'
end
<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>
<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ログインの公式ドキュメントです。
サムネは英語ですが、本体ページへ飛べば若干あやしめの日本語で読めます。Google翻訳なのかな。
ガイドを読んでいくと、どうやら“古いGoogleログイン”と“新しいGoogleログイン”というものがあり、“新しいGoogleログイン”を使うとパーソナライズされたボタンが表示されるそう。
https://developers.google.com/identity/gsi/web/guides/overview から引用
これ以外にも“新しいGoogleログイン”の利点やその仕組みが事細かに説明されており「ほげーなるほどぉー」となること請け合いなので、まずは公式ドキュメントを一読するのをおすすめします。
今回はこちらの“新しいgoogleログイン”のやり方で進めていきます。
Google APIの設定を行う
こちらに書いてある通り、Google API Consoleへアクセスし、アプリがGoogleログインを使うのに必要なクライアントIDを発行したり、反対にGoogle側がOAuthの使用を許可するドメインに自分のアプリのドメインを登録したりしていきます。
まず、適当なプロジェクトを作成して……。
「OAuth同意画面」から設定を進めていきます。「User Type」は「外部」を選択。
アプリ名やユーザーサポートメールの宛先など必須項目を埋めていきます。
ここでアプリ名にgoogleが含まれるとエラーになるようです。注意!
続いてアプリのユーザーにどこまで許可を求めるかというスコープの設定をします。公式ドキュメントには、「(Googleログインの)認証にはデフォルトのスコープ(メール、プロファイル、openid)で十分」とあるので、その3つを選択します。
最後にテストユーザーに自分のメールアドレスを登録。
続いて「認証情報」を設定してクライアントIDを発行します。上部の「+ CREATE CREDENTIALS」をクリックして「OAuth クライアントID」を選択します。
アプリケーションのURLは「ウェブアプリケーション」を選択。
公式ドキュメントに書いてある通り、「承認済みのJavaScript生成元」にはhttp://localhost
とhttp://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ログインボタンを実装する
次はビューにGoogleログインボタンを追加します。上記のガイドに載っているサンプルコードをそのまま使います。
YOUR_GOOGLE_CLIENT_ID
には先ほど発行したご自身のクライアントIDを入れてください。もしアプリを外部に公開する場合は、クライアントIDは環境変数などに設定してそこから呼び出すようにしましょう。
data-login_uri
に、callback先のURLを設定します。
<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ログインボタンが表示されました!
クリックするとおなじみのウィンドウが開きます。これより先に進もうとするとその後の処理を書いていないのでエラーになります。
なお、ボタンのデザインをカスタマイズしたい場合は、こちらのリファレンスを参考にするといいと思います。
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
アクションで受けるようにルーティングを追加します。
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
周辺情報も探しながら読み解くと、
- CSRF対策のため、cookieとパラメータに含まれている、
g_csrf_token
を検証する - パラメータの
credentials
をデコードすればpayload
(ユーザー情報)が得られる
ということがわかりました。
下記のブログが、英語ですがRailsのコード付きでわかりやすいです。
1.について説明を加えると、Railsではデフォルトでprotect_from_forgery
というCSRF対策のメソッドが走っており、外部ドメインからのPOSTリクエストをそのまま受けることができません。
今回は、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のライブラリが使えます。
このライブラリを使うためにはgemのインストールが必要なので、Gemfile
にgem 'googleauth'
を追加してbundle install
しましょう。
gem 'googleauth'
$ bundle install
これで準備が整いました。callbackアクションのソースコードをまとめると下記となります。
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_action
でverify_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"
}
こんな感じ(公式より引用しました)。メールアドレス以外にも名前やプロフィール写真などいろいろ取得できます。
今回はemail
をキーとしてUserモデルをfind_or_create
して、セッションにユーザーIDを記憶させて簡易ログイン機構とし、ログイン後のページに遷移させます。
はい、これでGoogleログインが完成しました! あとはよくあるセッション管理のパターンをあてはめていけばよいと思います。
本番環境では?
本番環境にデプロイするときにやることは、
- Google API Consoleで、承認済みの JavaScript 生成元、承認済みのリダイレクト URIを本番環境のものに差し替える
- Google API Consoleで、アプリの公開ステータスを本番環境に切り替える
-
app/views/static_pages/before_login.html.erb
のGoogleログインボタンのHTMLのdata-login_uri
を本番環境のものに差し替える
の3点です。
感想
RailsアプリにGoogleログインを実装する方法は、Auth0というサービスを使ったり、omniauth
というgemを使うケースをよく見かけるのですが、なるべく公式ドキュメントに沿った形で実装するとこうなるよーという例でした。
個人開発したアプリではsorcery
というgemを使って基本のユーザーモデルとログイン機構を作り、そこにあとからGoogleログイン機能を足したのですが、そのような拡張をするケースでも公式ドキュメントに沿った形だとやりやすかったです。
それにしても、実際のアプリにコードを書くより、こうやってブログにまとめるほうが何倍も時間がかかりました(おかげでRailsのCSRF対策に詳しくなりましたが…)。
もしこの記事が参考になりましたら、いいねボタンを押していただけると非常に喜びます。
Discussion
GCPでプロジェクト作成や、OAuth2.0 クライアントIDは料金はかかるのでしょうか?
ポートフォリオサイトで使用したいのですそれほどリクエスト回数は多くないとは思うのですが、「1ヶ月○回リクエストを読んだら料金が発生する」とかあればご教示いただきたいです。