Rails セッション管理と Firebase 認証の二重管理で発生する不整合問題を徹底解説
Rails アプリケーションで Firebase Authentication を使用している際に、「ログインしているのに認証エラーになる」「SMS認証が不安定」といった問題に遭遇したことはありませんか?
これらの問題の多くは、Rails側のセッション管理とFirebase側の認証管理が独立して動作することで発生する「二重管理問題」が原因です。
本記事では、この複雑な問題を初心者エンジニアにも分かりやすく解説し、なぜこの問題が発生するのか、そしてどのように対処すべきかを詳しく説明します。
目次
Rails セッション管理の基本概念
セッションとは何か
Web アプリケーションにおける「セッション」とは、サーバー側がユーザー固有の情報を一時的に保存するための仕組みです。
# Rails でのセッション使用例
class ApplicationController < ActionController::Base
def login_user(user_id)
session[:user_id] = user_id # ユーザーIDをセッションに保存
end
def current_user
@current_user ||= User.find_by(id: session[:user_id])
# セッションからユーザーIDを取得してユーザーを特定
end
def logged_in?
current_user.present?
end
end
ブラウザとサーバー間のデータフロー
ブラウザ側 Rails サーバー側
┌─────────────────┐ ┌─────────────────────┐
│ Cookie: │ │ セッションストア: │
│ session_id=abc123│ <────────────│ abc123 => { │
└─────────────────┘ │ user_id: 12345 │
│ } │
└─────────────────────┘
重要なポイント:
- ブラウザ側:セッションIDのみをCookieに保存
- サーバー側:実際のデータ(ユーザー情報等)を保存
- 通信:毎リクエスト時にCookieでセッションIDを送信
Rails セッション管理の特徴
| 項目 | 内容 |
|---|---|
| データ保存場所 | サーバー側(メモリ/DB/Redis等) |
| ブラウザ保存内容 | セッションIDのみ(Cookie) |
| データ更新 | サーバー側のコードで明示的に実行 |
| 有効期限 | サーバー側で設定・管理 |
| セキュリティ | サーバー側制御のため比較的安全 |
Firebase 認証の仕組み
Firebase Authentication の特徴
Firebase Authentication は、クライアント側(ブラウザ)で直接動作する認証システムです。
// Firebase 認証の基本的な使用例
import { getAuth, onAuthStateChanged } from 'firebase/auth'
const auth = getAuth()
// 認証状態の監視
onAuthStateChanged(auth, (user) => {
if (user) {
console.log('ユーザーがログイン中:', user.uid)
// JWTトークンの取得
user.getIdToken().then(token => {
console.log('IDトークン:', token)
})
} else {
console.log('ユーザーはログアウト状態')
}
})
Firebase 認証のデータ保存場所
| 項目 | 内容 |
|---|---|
| データ保存場所 | ブラウザ側(localStorage等) |
| 認証情報 | JWTトークン形式 |
| データ更新 | Firebase SDK が自動実行 |
| 有効期限 | 自動リフレッシュ(通常1時間) |
| セキュリティ | クライアント側のため追加対策が必要 |
Firebase 認証が失効するタイミング
Firebase 認証は以下のような場合に失効します:
自動的な失効
- トークン期限切れ(1時間後)
- ネットワーク接続問題
- ブラウザタブを長時間放置
- モバイルアプリのバックグラウンド処理
ユーザーアクション
- ブラウザの「サイトデータを削除」
- プライベートモードの使用
- 複数デバイスでの同時ログイン時の競合
システム側要因
- Firebase API側のメンテナンス
- パスワード変更などのセキュリティ操作
- アカウント無効化
二重管理による問題の発生
Rails + Firebase 構成での認証フロー
多くの Rails + Firebase 構成では、以下のような二重管理が行われています:
class ApplicationController < ActionController::Base
before_action :refresh_firebase_token!
def current_user
# Rails セッションからユーザーを特定
@current_user ||= User.find_by(firebase_uid: session[:firebase_uid])
end
private
def refresh_firebase_token!
# 定期的に Firebase トークンを更新
refresh_token = current_user&.firebase_refresh_token
return unless refresh_token
# Firebase API を呼び出し
response = firebase_api_call(refresh_token)
# Rails セッションを更新
session[:firebase_uid] = response['user_id'] # ← 問題の原因
end
end
// フロントエンド側(Firebase 認証)
class AuthManager {
async getCurrentUser() {
// Firebase 側の認証状態を取得
const user = firebase.auth().currentUser
return user
}
async checkAuthState() {
const user = await this.getCurrentUser()
if (!user) {
// Firebase 側で認証失効 → Rails 側も強制同期
this.forceLogout()
}
}
forceLogout() {
// Rails セッションを削除
fetch('/users/sign_out', { method: 'DELETE' })
.then(() => window.location.href = '/login')
}
}
二重管理の問題構造
問題の核心:Rails側とFirebase側が独立して動作している
Firebase側:リアルタイムで認証状態が変化
Rails側:定期的にトークンを更新してセッション管理
↓
同期タイミングがずれて不整合発生
具体的な不整合発生ケース
ケース1: SMS認証中のセッション不整合
このケースは特に複雑で、SMS認証プロセス中に発生します:
// SMS認証の流れ
async function sendSMSCode(phoneNumber) {
// 1. Firebase で SMS認証開始
const confirmationResult = await signInWithPhoneNumber(auth, phoneNumber, recaptchaVerifier)
// 新しい Firebase UID "new_user_12345" が生成される
}
async function verifySMSCode(code) {
// 3. SMS認証完了
const result = await confirmationResult.confirm(code)
// Firebase 側は "new_user_12345" で認証完了
// 4. Rails 側にデータを送信
const response = await fetch('/users/verify_sms', {
method: 'POST',
body: JSON.stringify({
firebase_uid: result.user.uid, // "new_user_12345"
id_token: result._tokenResponse.idToken
})
})
}
class ApplicationController < ActionController::Base
before_action :refresh_firebase_token! # 毎リクエストで実行
def refresh_firebase_token!
# 2. SMS認証中でも実行される
refresh_token = current_user&.firebase_refresh_token # 既存ユーザーの古いトークン
response = firebase_api_call(refresh_token)
# 古い情報でセッションを上書き
session[:firebase_uid] = response['user_id'] # "old_user_67890"
end
end
問題発生の時系列:
時刻 10:00:01 - SMS認証開始
Firebase: 新しいUID "new_user_12345" を生成中
Rails: session[:firebase_uid] は "old_user_67890"
時刻 10:00:02 - refresh_firebase_token! が実行される
Rails: 古いトークンで Firebase API を呼び出し
Rails: session[:firebase_uid] = "old_user_67890" で上書き
時刻 10:00:03 - SMS認証完了
Firebase: "new_user_12345" で完了
Rails: session[:firebase_uid] は "old_user_67890" のまま
→ 不整合発生!
ケース2: トークン期限切れによる不整合
// Firebase 側でトークンが期限切れ
firebase.auth().onAuthStateChanged((user) => {
if (!user) {
// Firebase 認証が失効
console.log("Firebase認証が失効しました")
}
})
// この時点での状態
// Firebase: null (認証失効)
// Rails: session[:firebase_uid] = "user_67890" (まだ生きている)
# Rails 側ではまだユーザーが取得できる
def current_user
@current_user ||= User.find_by(firebase_uid: session[:firebase_uid])
# まだユーザーが見つかる(Rails 側は生きている)
end
def signed_in?
current_user.present? # true を返す
end
結果:Rails側では「ログイン中」、Firebase側では「ログアウト状態」
ケース3: 並行処理による競合状態
// SMS認証完了時の処理
async function completeSMSAuth(user, credential) {
// Firebase 側で電話番号リンク
const result = await linkWithCredential(user, credential)
// 新しいトークン情報を Rails に送信
submitToRails({
firebase_uid: result.user.uid,
refresh_token: result._tokenResponse.refreshToken,
id_token: result._tokenResponse.idToken
})
}
# 同時に before_action で refresh_firebase_token! が実行
def refresh_firebase_token!
# 古いトークンを使用して Firebase API を呼び出し
old_refresh_token = current_user&.firebase_refresh_token
response = firebase_api_call(old_refresh_token)
# 古い情報でセッションを上書き
session[:firebase_uid] = response['user_id']
end
結果:SMS認証の新しい情報が古い情報で上書きされる
対処法と改善案
短期的な対処法
1. 認証プロセス中のトークン更新を制御
class ApplicationController < ActionController::Base
before_action :refresh_firebase_token!
private
def refresh_firebase_token!
# SMS認証関連のパス中は実行しない
auth_paths = ['/users/phone_number', '/users/verify_sms', '/users/sign_up_with_provider']
return if auth_paths.any? { |path| request.path.include?(path) }
# 既存のトークン更新処理
refresh_token = current_user&.firebase_refresh_token
return unless refresh_token
response = firebase_api_call(refresh_token)
session[:firebase_uid] = response['user_id']
end
end
2. Firebase 認証状態の監視追加
class AuthStateMonitor {
constructor(auth) {
this.auth = auth
this.setupAuthStateMonitoring()
}
setupAuthStateMonitoring() {
this.auth.onAuthStateChanged((user) => {
if (!user && this.isInAuthFlow()) {
console.warn('認証フロー中に Firebase 認証が失効')
this.handleAuthLoss()
}
})
}
isInAuthFlow() {
const authPaths = ['/users/phone_number', '/users/verify_sms']
return authPaths.some(path => window.location.pathname.includes(path))
}
async handleAuthLoss() {
// 自動復旧を試行
try {
await this.attemptAutoRecover()
} catch (error) {
// 復旧失敗時はユーザーに通知
this.notifyUser('認証エラーが発生しました。再度ログインしてください。')
}
}
}
3. 認証中フラグによる排他制御
class SMSAuthManager {
constructor() {
this.authInProgress = false
}
async sendSMSCode(phoneNumber) {
if (this.authInProgress) {
console.log('SMS認証が進行中です')
return
}
this.authInProgress = true
try {
const confirmationResult = await signInWithPhoneNumber(auth, phoneNumber, recaptchaVerifier)
window.confirmationResult = confirmationResult
} catch (error) {
console.error('SMS送信エラー:', error)
} finally {
// 送信完了後はフラグをリセット(認証完了時ではない)
// this.authInProgress = false
}
}
async verifySMSCode(code) {
if (!this.authInProgress) {
console.error('SMS認証が開始されていません')
return
}
try {
const result = await window.confirmationResult.confirm(code)
await this.submitToRails(result)
} catch (error) {
console.error('SMS認証エラー:', error)
} finally {
this.authInProgress = false // 認証完了時にフラグをリセット
}
}
}
中長期的な改善案
1. 統一認証ミドルウェアの導入
class UnifiedAuthMiddleware
def initialize(app)
@app = app
end
def call(env)
request = ActionDispatch::Request.new(env)
# Firebase トークンの検証
if (token = extract_firebase_token(request))
user = authenticate_with_firebase_token(token)
if user
# Rails セッションと Firebase 認証を同期
sync_auth_state(request.session, user)
end
end
@app.call(env)
end
private
def sync_auth_state(session, user)
session[:firebase_uid] = user.firebase_uid
session[:last_auth_sync] = Time.current
end
end
2. 認証状態の一元管理
class UnifiedAuthManager {
constructor() {
this.authState = 'loading'
this.listeners = []
this.initializeAuth()
}
async initializeAuth() {
// Firebase 認証の初期化
this.firebaseAuth = getAuth()
// Rails セッションの確認
const railsAuth = await this.checkRailsAuth()
// 認証状態の統一
await this.synchronizeAuthState(this.firebaseAuth.currentUser, railsAuth)
// 認証状態の監視開始
this.firebaseAuth.onAuthStateChanged(this.handleAuthStateChange.bind(this))
}
async handleAuthStateChange(firebaseUser) {
// Firebase 認証状態が変更された際の処理
if (firebaseUser) {
this.authState = 'authenticated'
await this.syncWithRails(firebaseUser)
} else {
this.authState = 'unauthenticated'
await this.clearRailsAuth()
}
// 状態変更をリスナーに通知
this.notifyListeners()
}
async syncWithRails(firebaseUser) {
const idToken = await firebaseUser.getIdToken(true)
await fetch('/api/auth/sync', {
method: 'POST',
headers: {
'Authorization': `Bearer ${idToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
firebase_uid: firebaseUser.uid
})
})
}
}
根本的な解決案:認証アーキテクチャの統一
最も理想的な解決策は、認証アーキテクチャを統一することです:
アプローチ1: Firebase 認証を主軸とした設計
class ApplicationController < ActionController::Base
before_action :authenticate_with_firebase_token!
private
def authenticate_with_firebase_token!
token = request.headers['Authorization']&.sub(/^Bearer /, '')
return redirect_to login_path unless token
begin
# Firebase トークンを直接検証
decoded_token = FirebaseTokenVerifier.verify(token)
@current_user = User.find_by(firebase_uid: decoded_token['user_id'])
rescue => e
# トークンが無効な場合
redirect_to login_path
end
end
# セッションを使用しない
def current_user
@current_user
end
end
アプローチ2: Rails セッションを主軸とした設計
// Firebase を認証のみに使用し、セッション管理は Rails に委譲
class RailsCentricAuth {
async authenticateWithFirebase(email, password) {
// Firebase で認証のみ実行
const userCredential = await signInWithEmailAndPassword(auth, email, password)
const idToken = await userCredential.user.getIdToken()
// Rails にトークンを送信してセッション開始
const response = await fetch('/auth/firebase_login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id_token: idToken })
})
if (response.ok) {
// Firebase 側はログアウト(Rails セッションのみ使用)
await signOut(auth)
window.location.href = '/dashboard'
}
}
}
まとめ
Rails アプリケーションで Firebase Authentication を使用する際の二重管理問題について解説しました。
重要なポイント
- Rails セッションとFirebase 認証は異なる仕組みで動作する
- 同期タイミングのずれが不整合の主な原因
- 短期的対処法で症状は軽減できるが、根本解決には設計変更が必要
- 認証アーキテクチャの統一が最も確実な解決策
初心者エンジニアへのアドバイス
- 認証システムは複雑になりがちなので、シンプルな設計を心がける
- ログ出力を充実させて問題の早期発見に努める
- 段階的な改善で既存システムに影響を与えないよう注意する
- 公式ドキュメントを活用して最新のベストプラクティスを学ぶ
実務でよくある対処パターン
多くの現場では、まず短期的対処法を適用して症状を軽減し、その後時間をかけて根本的な改善を行うアプローチが取られます。
- 緊急対応:認証プロセス中のトークン更新制御
- 安定化:Firebase 認証状態の監視追加
- 改善:統一認証ミドルウェアの導入
- 最適化:認証アーキテクチャの統一
この記事で説明した内容を参考に、あなたのプロジェクトに最適な認証システムを構築してください。認証は Web アプリケーションの根幹部分なので、安全性と使いやすさの両立を目指して設計することが重要です。