LoginSignup
4
4

More than 3 years have passed since last update.

Next.js + Rails + Firebase Authでjwt認証を実装しよう(Email編)

Posted at

はじめに

JWT認証に関して、Next.js Rails Firebase Authenticationという組み合わせでの実装をした記事があまりないとのことなので、今回は実際にやっていこうと思う。今回はEmailでの実装になるため、Google認証で実装をしたいという方は同シリーズの
https://qiita.com/satopin/items/fa0c35a0ba69a379683e
をご覧いただければ幸いである。
ソースコードはこちらから
https://github.com/yumaasato/my-mahjong
(まだアプリ自体は作成中なので悪しからず・・・)

環境

Ruby: 2.6.5
Rails 6.0.3.6
TypeScript
firebase-auth-rails
Redis-server

*ちなみに今回、user認証でよく用いられるdeviseは必須ではありません。
今回は使わずに実装を進めていきます。
また、今回redis-serverのインストールからconfig/initializers/firebase_id_token.rbで設定を行うまでは
https://qiita.com/satopin/items/fa0c35a0ba69a379683e 
と同じのため、3-1までは飛ばしてもらって構わない。

1. Redis-serverのインストール

mac版の場合

$ brew install redis-server

Linuxでは

$ sudo apt install redis-server

内部でfirebase_id_tokenを使っているためこのRedisをインストールする必要があるとのことです。

2. firebase-auth-railsの追加

今回、firebase-auth-railsというgemを利用することでjwt認証を比較的簡単に行うことができます。

Gemfile
gem 'firebase-auth-rails'

を追加します。

そして、

$ bundle install

を実行します。

3-1. 実装(プロジェクトの設定)

まずはじめに、Reidsとfirebaseプロジェクトの設定をおこないます。

config/initializers/firebase_id_token.rb
FirebaseIdToken.configure do |config|
  config.redis = Redis.new
  config.project_ids = [ENV['FIREBASE_PROJECT_ID']]
end

3-2.Userテーブルの設計

今回は、Userのnameをクライアント側で登録せずにEmailとPasswordのみの認証を行う。
そのため、Userテーブルの編集を行う必要がある。
以下は一例である。

db/migrate/****_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :password_digest 
      t.string :name, default: ""   # null: falseとnameが必須になるので今回は外す
      t.string :email, default: ""
      t.string :uid, null: false, default: ""

      t.timestamps

      t.index :uid, unique: true  # 同じuidによる登録を防ぐ
    end
  end
end

ここで、uidを null:falseにしておくことで、Firebase Authで認証したUserをRails側で受け取れるようにしておく。

3-3.ユーザー登録用のコントローラを実装する

以下はあくまで一例である

app/controllers/api/v1/auth/users_controller.rb
require_dependency 'api/v1/application_controller'
module Api
  module V1
    module Auth
      class UsersController < V1::ApplicationController
        skip_before_action :authenticate_user

        def index
          # ユーザー一覧を取り出す
          users = User.order(created_at: :desc)
          render json: { status: 'SUCCESS', message: 'Loaded users', data: users }
        end

        def create
          # ユーザーを作成する
          FirebaseIdToken::Certificates.request
          raise ArgumentError, 'BadRequest Parameter' if payload.blank?
          @user = User.find_or_initialize_by(uid: payload['sub']) do |user|
            user.email = payload['email']
          end
          if @user.save
            render json: @user, status: :ok
          else
            render json: @user.errors, status: :unprocessable_entity
          end
        end

        private

        def token
          params[:token] || token_from_request_headers
        end

        def payload
          @payload ||= FirebaseIdToken::Signature.verify token
        end
      end
    end
  end
end

Google認証では、

@user = User.find_or_initialize_by(uid: payload['sub']) do |user|
            user.name = payload['name']
          end

としたが、これでは、payloadでUserのnameが要求される。今回Userのnameは使わずに、Emailのみの認証を行うため、

@user = User.find_or_initialize_by(uid: payload['sub']) do |user|
            user.email = payload['email']
          end

とする。ちなみに user.password= payload['password']とする必要はない。Passwordを暗号化した文字列ならわかるが、データベースにpasswordを保管するのはセキュリティ的に問題があるからだ。

3-4 Routeの追加

Rails.application.routes.draw do
  namespace 'api' do
    namespace 'v1' do
      resources :players
      resources :games, only: %i(index)
        namespace 'auth' do
          post 'users' => 'users#create' #追記箇所
          get 'users' => 'users#index' #追記箇所
        end
    end
  end
end

今回の場合、Firebase Authを介してUserの認証を行なっているため、上記のようにする。
ちなみに、get 'users' => 'users#index'は、User一覧をみれるようにするために設定した。

4-1. クライアント側の実装

次にNext.js側の実装を行う。フロント側の実装方法が省かれている記事が多いが、ここではNext側についても実装方法を記述する。あくまで参考(一部省略)だが、

pages/sign_in/Auth.tsx
// Eメール認証
  const signUpEmail = async () => {
    await auth.createUserWithEmailAndPassword(email, password).catch((err) => alert(err.message));
    router.push('/')
  };

  // 認証後Rails側にリクエストを送る
  const handleEmailsignUp = () => {
    const request = async () => {
      await signUpEmail();
      const auth = getAuth();
      const currentUser = auth.currentUser;
      // Firebase Authの認証
      if (auth && auth.currentUser) {
        const token = await currentUser.getIdToken(true);
        const config = { token };
        // Rails側にリクエストを送る
        try {
          await axios.post('/api/v1/auth/users', config);
        } catch (error) {
          console.log(error);
        }
      }
    };
    request();
  };

return ( 
        <>
           <TextField
              variant="outlined"
              margin="normal"
              required
              fullWidth
              id="email"
              label="Email Address"
              name="email"
              autoComplete="email"
              value={email}
              onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                setEmail(e.target.value);
              }}
            />
            <TextField
              variant="outlined"
              margin="normal"
              required
              fullWidth
              name="password"
              label="Password"
              type="password"
              id="password"
              autoComplete="current-password"
              value={password}
              onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                setPassword(e.target.value);
              }}
            />

            <Button
              disabled={
                isLogin
                  ? !email || password.length < 6
                  : !email || password.length < 6
              }
              fullWidth
              variant="contained"
              color="primary"
              className={classes.submit}
              startIcon={<EmailIcon />}
              onClick={
                isLogin
                  ? async () => {
                    try {
                      await handleEmailLogin();
                    } catch (err) {
                      alert(err.message);
                    }
                  }
                  : async () => {
                    try {
                      await handleEmailsignUp();
                    } catch (err) {
                      alert(err.message);
                    }
                  }
              }
            >
              {isLogin ? "Login" : "Register"}
            </Button>
          </>
)

これでButtononClick

5-1. 実装後のオペレーション(重要)

実装後、$ rails cとして、firebase_id_tokenのDownloading Certificates以下に記載してある内容を実行する必要がある。

irb(main):001:0>FirebaseIdToken::Certificates.request
・
・
・
irb(main):002:0>FirebaseIdToken::Certificates.present?
=> true

となってはじめて、jwt認証を使うことができる。

注意点

この実装ではnameを使わずに実装をおこなったため、クライアント側でnameを入れて認証を行なった際には、エラーが起こる可能性がある。nameを入れて実装を進めたいという場合には、別の方法を参照してもらいたい。

また、Railsにおいて、.envファイルで環境変数を管理する際には、

gem 'dotenv-rails'

をインストールする必要があるため、忘れずに設定しておこう。
(これで認証に時間がかかったので・・・)
さらに、認証を行う際には、Railsサーバーとクライアント側のサーバーに加えて、$ redis-serverでRedis-serverを起動する必要があるため、忘れないようにしよう。

終わりに

実装だけでもかなりの時間をかけた上に、Next.js(TypeScript) + Rails + Firebase Authの組み合わせでのjwt認証の記事が少ないということで今回記事を書くことになった。この記事が一人でも多くの人の参考になれば幸いである。

4
4
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
4
4