1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rails 7 × Reactでomniauth-google-oauth2を使ったGoogle認証を実装

Posted at

この記事はプログラミング学習者がアプリ開発中に躓いた内容を備忘録として記事におこしたものです。内容に不備などあればご指摘頂けると助かります。

0.前提条件

X(旧Twitter)のクローンサイトの制作過程でGoogle認証を使ったログイン機能を実装している際に実装した内容をご紹介したいと思います。

フロントエンド:React(JavaScript) 19.1.0
バックエンド:Ruby on Rails(Ruby) 7.0.0 APIモード
インフラ:Docker
PC;Mac Book Air M2チップ

1.記事作成の経緯

これまでDeviseやdevise_token_authといったgemを使って認証機能を実装してきたのですが、SNS認証を使ったログインは実装経験が無かったもののアプリを利用する側としては何度も利用してきていたので挑戦することにしました。
せっかく実装できたので、備忘録を兼ねて記事として残しておきたいと思います。

2.実装概要

  • Google Cloud側の設定としてクライアントIDとクライアントシークレットを生成する
  • 必要なgemをインストールする
  • ルーティング、コントローラー、omniauth.rbの設定
  • フロントエンド側でボタンクリックした時のイベントハンドラでバックエンド側へ接続

3.認証情報の生成

1, 先ずはGoogle CloudでクライアントIDとクライアントシークレットを生成していきます。

  • Google Cloud Consoleにアクセスして新しいプロジェクトを作成していきます。
    project一覧画面.png
  • 名前を決めてプロジェクトを作成します。
    project作成画面.png
    次に認証情報を取得していきます。
  • 左上のナビゲーションメニューをクリックして、APIとサービス → 認証情報をクリック
    APIとサービスから認証情報.png
  • 上部バーにある認証情報を作成をクリックして、OAuthクライアントIDを選択
    認証情報を作成〜OAuthクライアントID.png
  • 先に同意画面を作成するように怒られますので、右下の同意画面を構成をクリック
    同意書面を構成.png
  • 遷移先であるブランディングの画面で開始ボタンをクリック
    ブランディング開始.png
  • 必要項目を埋めていきます。アプリ名、ユーザーサポートメールを入力して次へ
    アプリ情報を入力.png
  • 対象を外部で選択して次へ
    アプリ情報 外部を選択.png
  • 連絡先情報にメールアドレスを入力して次へ
    連絡先情報を入力.png
  • Google APIサービス:ユーザーデータに関するポリシーに同意するためのチェックボックスをクリックして続行
    ポリシー同意.png
  • 最後に作成ボタンを押してブランディングが作成されます。
    作成ボタン.png
  • 作成が終わるとブランディングの画面に遷移します。
    ブランディング選択.png
  • サイドメニューのデータアクセスをクリック
    アプリ情報.png
  • スコープを追加または削除をクリックして、出てきたウィンドウで「Google アカウントのメインのメールアドレスの参照」メニューのチェックボックスをクリック
    データアクセスの設定.png
  • 上のクリックで選択した内容が非機密のスコープに反映されたことを確認して、画面したのSaveをクリック
    データアクセス保存.png
  • 次にサイドメニューのクライアントをクリックして上部バーのクライアントを作成をクリック
    クライアントを作成.png
  • アプリケーションの種類、名前、承認済みのリダイレクト URIを入力して画面下の作成ボタンをクリック。これでクライアントIDとクライアントシークレットが生成できました。
    この画面を閉じてもしばらくはクライアントIDとクライアントシークレットを確認できるのですが、一定時間経つとクライアントシークレット側が確認できなくなるのでスクリーンショットなどで保管しましょう。
    認証データ生成.png

4.バックエンド側の実装

1, 必要なgemをインストールしていきます。

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

上記のgemをインストールするためにコマンドを実行します。私はDocker上でのインストールでしたので、docker版も載せておきます。

コマンド
docker compose exec サービス名 bundle install # コンテナ起動時
or
bundle install

手動でomniauth.rbを作成します。
Google Cloudで生成したクライアントIDとクライアントシークレットを参照する設定です。
また、最後の行ではgetとpostのリクエストのみを許可しています。

config/initializers/omniauth.rb
require 'omniauth'

OmniAuth.config.silence_get_warning = true # getリクエストの時の警告を無くす
# credentials.yml.encの内容を参照して承認する
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :google_oauth2,
           Rails.application.credentials.google[:client_id],
           Rails.application.credentials.google[:client_secret]
end
OmniAuth.config.allowed_request_methods = %i[get post]

2, Credentialsの設定
Google Cloudで生成したクライアントIDとクライアントシークレットを参照できるようにcredentialsに記入していきます。

コマンド
docker compose exec -e EDITOR="vim" サービス名 bin/rails credentilas:edit # コンテナ起動時
or
EDITOR=vim bin/rails credentials:edit -e development

Dockerで作業されている場合、Docker内にvimがインストールされていない可能性があります。
インストールされていない状態で上記のコマンドを実行してもFile encryped and saved.と出力されるだけでcredentialsを編集できませんのでご注意ください。
その場合はdocker compose exec サービス名 apt-get install vimを実行してvimをインストールしてから改めてトライしてください。

credentialsを開くことができたら下記の内容を追記してください。

credentilas
google:
  client_id: 取得したクライアントID
  client_secret: 取得したクライアントシークレット

作成したcredentials.yml.encは暗号化され、config/master.key、若しくは環境変数RAILS_MASTER_KEYを使用して復号化されます。

最終的に上で設定した認証情報をもとにGoogle認証を行うのですが、
私の場合、クライアントIDがcredentialsより正しく取得されずにGoogle認証が行われない不具合が発生しました。
その時の原因は下記のように開発環境用のcredentialsファイルと共用のcredentialsファイルの2つが生成されてしまったことによる認証情報の競合が起きていたことでした。
config/credentials/development.yml.enc
config/credentials.yml.enc
今回はテストや本番環境では使用せず開発環境だけでの動作を目的としていたため、development.yml.encを削除することで正しく動作させることができるようになりました。

credentialsを設定して認証機能の実装が終わったのでGithubにPushしたところ、NoMethodError: undefined method `[]' for nil:NilClassというエラーが発生してGithub Actionsのテストで止まってしまいました。
これは設定したcredentialsの中身をmaster.keyが無いために復号できずにnilと認識されていることが原因でした。

credentials.yml.enc
  provider :google_oauth2,
           Rails.application.credentials.google[:client_id],
           Rails.application.credentials.google[:client_secret]

この問題に対して、master.keyを使って復号できるようにGithubのsecretsに登録。
また、Github Actions内で参照できるようにworkflowsのエラーが起きているテストのymlファイルにRAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}記載することでmaster.keyを参照できるように設定しました。エラーは解消されテストも問題なく通過できるようになりました。

3, ルーティングの設定
RailsはAPIモードなので、namespaceを使った階層構造で実装しています。

routes.rb
  namespace :api do
    namespace :v1 do
      mount_devise_token_auth_for 'User', at: 'users', controllers: {
        omniauth_callbacks: 'api/v1/omniauth_callbacks'
      }

下のrails routesのリストに載っている青枠のhttp://localhost:3000/api/v1/users/google_oauth2にアクセスがあるとGoogle認証が走り、認証が完了したらリダイレクト先としてGoogle Cloudで設定した赤枠のhttp://localhost:3000/omniauth/google_oauth2/callbackに遷移します。
Googleの認証画面への遷移はOmaniauthミドルウェアが自動で行います。
認証が終わってリダイレクトして、http://localhost:3000/omniauth/google_oauth2/callbackにアクセスがあるとapi/v1/omniauth_callbacksコントローラー内のredirect_callbacksメソッドを動かします。
omniauth_callbacksのルーティング.png

4, コントローラーの実装
Google側で認証された情報がrequest.env['omniauth.auth']で取得できますので、そこから必要な情報を取得していきます。
create_new_auth_tokenはdevise_token_authというGemのメソッドで必要なトークン情報を全て生成してくれるので便利です。
ユーザー情報の保存できたら、ルート画面(http://localhost:5173)へ遷移するように設定しており、そのURLに生成したトークン情報を埋め込んでいます。フロントエンド側で埋め込まれた情報をパラメーターとして受け取り使用します。

app/controllers/api/v1/omniauth_controller.rb
module Api
  module V1
    class OmniauthCallbacksController < ApplicationController
      def redirect_callbacks
        auth = request.env['omniauth.auth'] # googleから認証情報を取得
        return if auth.nil?

        # request.env['omniauth.auth']にリクエストパラメータのユーザー情報が入ってくる
        user = User.from_omniauth(request.env['omniauth.auth'])
        if user.persisted?
          # create_new_auth_tokenでトークン情報を全て生成する
          token = user.create_new_auth_token
          if user.save
            # フロントエンド側でトークン情報を取得できるようにURLのパラメーター内にトークン情報を入れる
            redirect_to "http://localhost:5173?status=success&access-token=#{token['access-token']}&uid=#{user.uid}&client=#{token['client']}&expiry=#{token['expiry']}"
          end
        else
          render json: { status: 'ERROR', message: '401 Unauthorized', data: user.errors }, status: :unprocessable_entity
        end
      end
    end
  end
end

下のモデルではコントローラー内で使っているメソッドを記述しています。
Googole側から取得した認証情報でサーバー内に該当のユーザーが居るかどうか確認して、居なければ認証情報をもとに新しくユーザーデータを作成します。
データベースのバリデーションでパスワード、電話番号、誕生日が必須なので、便宜上ダミーデータを使用しています。パスワードに関してはよりセキュアな設定も可能なのですが、今回は開発環境だけなので直ぐに分かる内容で設定しています(てへぺろ)。

user.rb
def self.from_omniauth(auth)
    find_or_create_by(provider: auth.provider, uid: auth.uid) do |user|
      user.email = auth.info.email
      user.password = 'password'
      user.name = auth.info.name
      user.phone_number = '090-1234-5678'
      user.birthday = '20250101'
    end
end

5.フロントエンド側の実装

続いてバックエンド側から送られてきた認証情報を取得していく処理をフロントエンド側で実装します。

GoogleIcon.jsx
import React, { useEffect } from "react";
import styled from "styled-components";
import GoogleIconImage from "../../assets/google_icon.png";
import { useNavigate, useSearchParams } from "react-router-dom";

const Button = styled.button`
  background-color: white;
  border-radius: 20px;
  width: 300px;
  height: 40px;
  text-align: center;
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;
  &: hover {
    background-color: #e1e3e1;
  }
`;

export function GoogleIcon() {
  const [searchParams] = useSearchParams();
  const navigate = useNavigate();

  const handleGoogleLogin = () => {
    // googleの認証画面へ遷移して認証を行う
    window.location.href = `${
      import.meta.env.VITE_API_URL
    }/api/v1/users/google_oauth2`;
  };

  // useEffectでsearchParamsが変わった時に処理を始動させる
  useEffect(() => {
    // 取得するparamsの中にaccess-tokenが含まれている場合に始動
    if (searchParams.has("access-token")) {
      // URLのパラメーターからトークン情報を取得
      const access_token = searchParams.get("access-token");
      const client = searchParams.get("client");
      const uid = searchParams.get("uid");

      // ローカルストレージにトークン情報を保管
      localStorage.setItem("access-token", access_token);
      localStorage.setItem("client", client);
      localStorage.setItem("uid", uid);

      // ログイン後のメインページへ遷移
      navigate("/main");
    }
  }, [searchParams]);

  return (
    <Button onClick={handleGoogleLogin}>
      <img src={GoogleIconImage} alt="google_ico" width="24px" height="24px" />
      Google で登録
    </Button>
  );
}
.env
VITE_API_URL="http://localhost:3000"

onClickイベントで設定したhandleGoogleLoginを皮切りにGoogle認証が動き出します。
window.location.hrefで指定のURLへ遷移させます。import.meta.env.VITE_API_URLは事前に.envで設定したURLを指しています(VITEを使用した場合の書き方)。
その後は先述のバックエンド側での処理が行われ、パラメーターにトークン情報が入った状態で返ってきます。
useSearchParamsフックを使ってパラメーター(searchParams)に変化があった場合に察知してuseEffect内のコードを実行します。パラメーターにトークン情報が入っていることを確認して取得した情報をローカルストレージに保管します。

最後にログアウトの実装です。

SignOutIcon.jsx
import React from "react";
import styled from "styled-components";
import axios from "axios";
import { useNavigate } from "react-router-dom";
import toast from "react-hot-toast";
import { HandleError } from "../../utils/HandleError";

const Button = styled.button`
  background-color: black;
  border-radius: 20px;
  text-align: center;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 200px;
  height: 40px;
  color: white;
  border: none;
  font-size: 20px;
  margin-bottom: 20px;
  &:hover {
    background-color: #1b1b1b;
    cursor: pointer;
  }
`;

const Span = styled.span``;

export const SignOutIcon = () => {
  const access_token = localStorage.getItem("access-token");
  const client = localStorage.getItem("client");
  const uid = localStorage.getItem("uid");

  const navigate = useNavigate();

  const handleSignOut = async () => {
    try {
      const response = await axios.delete("/users/sign_out", {
        headers: {
          "access-token": access_token,
          client: client,
          uid: uid,
        },
      });
      console.log(response.data);
      if (response.status === 200) {
        console.log("Signed out successfully!");
        localStorage.clear();
        navigate("/");
      }
    } catch (error) {
      HandleError(error);
    }
  };

  return (
    <Button onClick={handleSignOut}>
      <Span>サインアウト</Span>
    </Button>
  );
};

スクリーンショット 2025-06-30 20.36.12.png
api/v1/users/sign_outへアクセスすることでdevise_token_authのログアウトするためのメソッドが動きます。ログアウト完了後はローカルストレージに保存されているトークン情報を削除することで完全にログアウトしたことになります。

以上で実装内容の説明を終わります。
最後までご覧頂きありがとうございました。

6.参考資料

Rails 7 × Googleログイン – omniauth-google-oauth2で簡単OAuth認証
useSearchParams
Model Concerns

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?