Rails で Firebase Authentication を使う場合のユーザーモデルとテスト

Firebase Authentication を使う場合、ユーザーモデルとテストはどうする?

Rails で個人用の英単語帳アプリを開発しています。開発環境は Rails 7.0.4 + Ruby 3.1.2 です。

ユーザ認証に Firebase Authentication を導入すると、フォームに入力した email と password を Firebase Authentication API に渡して返り値を受け取るだけで認証が実装できます。認証のためにユーザーモデルを作成する必要はありません。

ただし、ユーザごとのデータを保存するテーブルの関連付けとしてユーザーモデルは必要になります。私の場合、英単語帳を flaschcards というテーブルに保存しているため、「誰が登録した英単語か」を識別するためにユーザーモデルを作成することにしました。ユーザーを作成するために外部の Firebase Authentication API を実行する必要があるため、(Devise 利用時など内部で完結する場合と違い)テストの方法も一工夫する必要が出てきました。

以下は Firebase Authentication を使った場合のユーザーモデルとテストの実装についてまとめたものです。同様の構成を考えている方の参考になれば幸いです。最期に Devise との個人的な比較と所感を載せています。

実装の流れ

大きな流れとしては以下になります。Firebase Authentication API を叩いた時に返ってくる localId (Firebase の管理画面上では User UID と表記される) をユーザーモデルに格納して利用します。

  1. FIrebase authentication を利用して認証機能を実装する(省略)
  2. ユーザーモデル(User)を作成する
  3. Firebase authentication の API で返される localId で User を作成する
  4. ユーザごとのデータを持つフォームを修正する(省略)
  5. テストを作成する(ユーザ作成のために外部の Firebase Authentication API の利用が必要ため、モデルやコントローラーのテストではなくシステムテストで行う)

ユーザーモデル(User)を作成する

$ bundle exec rails g model User firebase_local_id:string
$ bundle exec rails g migration AddUserIdToFlashcards user:references

app/models/user.rb

class User < ApplicationRecord
  has_many :flashcards
(... snip...)

app/models/flashcard.rb

class Flashcard < ApplicationRecord
  belongs_to :user
(... snip ...)

Firebase authentication の API で返される localId で User を作成する

app/controllers/home_controller.rb

class HomeController < ApplicationController
  before_action :set_user_data, only: %i[signup login]

  def signup
    uri = URI("https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=#{Rails.application.credentials.firebase_api_key}")
    response = Net::HTTP.post_form(uri, "email": @email, "password": @password)
    data = JSON.parse(response.body)
    session[:data] = data
    session[:firebase_local_id] = data["localId"]

    if response.is_a?(Net::HTTPSuccess)
      User.create(firebase_local_id: data["localId"])
      redirect_to flashcards_path, notice: "Signed up successfully"
    end
  end

  def login
(... snip ...)

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  helper_method %i[current_user authenticate_user]

  def authenticate_user
    redirect_to home_login_path, notice: 'You must be logged in to view your data.' unless current_user
  end

  def current_user
    @current_user ||= session[:firebase_local_id]
    @current_user_id ||= User.find_by(firebase_local_id: session[:firebase_local_id]).id
  end
end

テストを作成する(ユーザ作成のために外部の Firebase Authentication API の利用が必要ため、モデルやコントローラーのテストではなくシステムテストで行う)

test/system/flashcards_test.rb

require "application_system_test_case"

class FlashcardsTest < ApplicationSystemTestCase
  setup do
    @user = FactoryBot.create(:user)
    @flashcard = FactoryBot.create(:flashcard)

    visit home_signup_url

    fill_in "email", with: "foo@example.com"
    fill_in "password", with: "abc123"

    click_on "Submit"

  end

(... snip ...)

Devise と Firebase Authentication の比較

個人的な結論

Firebase Authentication のほうが扱いやすいように感じました。理由としては API を叩くだけで完結するのと、自分で情報を持たないからです。Devise を利用したユーザーモデル(User)は内部にメールアドレス、暗号化されたパスワード、その他接続時の情報を抱えるようです。接続時の情報など(例: current_sign_in_ip)は安易に流出しないようにデフォルトでは Trackable が無効になっています。こうしたガードレールがあるので、基本的には安全だと思いますが Devise の実装をきちんと理解していない限り、ブラックボックスの部分が残ってしまうように思います。内部に情報を持たず API だけで完結する Firebase のほうが扱いやすい、と感じたのはこうした理由からです。

個人的な比較

Devise Firebase Authentication
透明性 きちんと理解していないとブラックボックスな点が残る 基本的に API を使うだけなので単純明快に感じる
情報の見つけやすさ Deviseのドキュメントやネットの記事はあふれている 記事は多くないのでモデルやテストは自作する必要がある(簡単)
心理的安全性 内部に情報を抱えるので不安な点が残る(理解していれば大丈夫) 内部に情報を抱えないので不安にならない
ユーザーモデル(User)の大きさ 肥大化しがち 使い方にもよるが他テーブルへユーザIDを関連付けるだけなら Firebase authentication が返す locaIId だけでいい

比較時のスクリーンショット

Devise

Firebase Authentication

参考情報

Firebase authentication

Rails7 で Firebase authentication を使う方法

www.youtube.com

Devise と Firebase Authentication のどちらを使うべきか?