Rails+omniauth-google-oauth2でGoogleログイン(devise無し)
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を使います。
+ 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のドキュメントに書いてあるまんまの
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]
を消した理由は最後に出てきます)
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
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チュートリアル風です)
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
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します。
class HomeController < ApplicationController
+ skip_before_action :check_logged_in, only: :index
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メソッドで定義しておきます。
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
で見つからなかった場合のみブロック内の処理を行います。
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を作成、ログインを試す
<!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_to
にmethod: :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 } %>
でうまくいきました。なぜこれでないといけないのか、以下に書いていきます。
1/2. link_toのオプションのmethod: :postが効かず、Routing Error: No route matches [GET] "/auth/google_oauth2"が出る問題
Rails7ではRailsUJSの代わりにTurboが使われるようになった影響で、link_to
のmethod
オプションが使えなくなっているようです。
Turboのドキュメントを見ると、
<a href="/articles/54" data-turbo-method="delete">Delete the article</a>
というふうにdata-tubo-method
属性が使えると書いてあります。
また、RailsGuidesにはlink_to
での使い方がしれっと書いてありました。
これに倣うと、先ほどのlink_to
は以下のようになります.
- <%= link_to "Googleでログイン", "/auth/google_oauth2", method: :post %>
+ <%= link_to "Googleでログイン", "/auth/google_oauth2", data: { turbo_method: :post } %>
一方、link_to
とは違って、button_to
のmethod
オプションはRails7でも健在のようです。こちらの場合は以下のようになります。
- <%= 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-cors
gemを試したりしましたが、全く変化がありません。
結局、こちらの記事に答えを見つけました。ログインボタンではTurboをオフにせよとのことです。
これら↓
のissueで言われているように、
data-turbo=false
をつけるやり方があるようです。
この書き方で、最終的にうまくいきました。
- <%= 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