Firebase JWT

認証JWTFirebaseJavaScriptNode.jsWebAPIトークン検証

ライブラリ

Firebase JWT

概要

Firebase JWTは、Firebase Authenticationサービスが提供するJSON Web Token(JWT)ベースの認証システムです。

詳細

Firebase JWTは、Firebase Authenticationサービスが生成・管理するJWT(JSON Web Token)形式の認証トークンを使用して、ユーザー認証を行うシステムです。Firebase JavaScriptクライアントSDKとFirebase Admin SDKを通じて、クライアントサイドでのトークン取得からサーバーサイドでのトークン検証まで、包括的な認証フローを提供します。

Firebase IDトークンは、ユーザーがFirebaseにサインインした際に生成される暗号化されたJWTです。これらのトークンには、ユーザーのUID、メールアドレス、サインインプロバイダーなどの基本的なプロフィール情報が含まれています。トークンはFirebaseの秘密鍵で署名されており、Firebase Admin SDKやサードパーティのJWTライブラリを使用して検証できます。

Virtual DOMという仮想的なDOM表現の代わりに、Firebase JWTはVirtual DOMという仮想的なトークン表現を使用することで、実際のデータベースアクセスを最小限に抑え、高いセキュリティを実現しています。トークンベースの認証により、セッション管理が不要になり、ステートレスで拡張可能なアプリケーション開発が可能です。

現在では世界最大のクラウドプラットフォームGoogle Cloudのエコシステムを持ち、Firebase Authentication、Firebase Firestore、Firebase Functions、Firebase Hostingなど多くの関連サービスと組み合わせて使用されています。

メリット・デメリット

メリット

  • ステートレス認証: セッション状態の管理が不要で、スケーラブルなアーキテクチャ
  • 自動トークン更新: SDKが自動的にトークンの有効期限を管理・更新
  • マルチプラットフォーム対応: Web、iOS、Android、Node.jsで統一的な認証体験
  • 豊富な認証プロバイダー: Google、Facebook、Twitter、GitHub等の外部認証サービス連携
  • カスタムクレーム: アプリケーション固有の権限情報をトークンに追加可能
  • Firebase エコシステム: Firestore、Functions、Hosting等との完全統合
  • 開発効率: 認証基盤の構築・運用コストを大幅削減

デメリット

  • Firebase依存: Firebase以外の認証システムとの統合が困難
  • カスタマイズ制限: 認証フローのカスタマイズに制限がある
  • コスト: 大規模ユーザー数での従量課金
  • ベンダーロックイン: 他のクラウドプロバイダーへの移行が困難
  • デバッグの複雑さ: トークン関連のエラーの原因特定が困難な場合がある

主要リンク

書き方の例

基本的なサインイン・トークン取得

import { initializeApp } from 'firebase/app';
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth';

// Firebaseの初期化
const firebaseConfig = {
  // 設定情報
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);

// ユーザーサインイン
const signInUser = async (email, password) => {
  try {
    const userCredential = await signInWithEmailAndPassword(auth, email, password);
    const user = userCredential.user;
    
    // IDトークンの取得
    const idToken = await user.getIdToken();
    console.log('IDトークン:', idToken);
    
    return user;
  } catch (error) {
    console.error('サインインエラー:', error);
    throw error;
  }
};

トークンの強制更新と詳細情報取得

import { getAuth } from 'firebase/auth';

const auth = getAuth();

// 現在のユーザーのトークン情報を取得
const getUserTokenInfo = async () => {
  const user = auth.currentUser;
  if (!user) {
    throw new Error('ユーザーがサインインしていません');
  }

  try {
    // トークンの強制更新
    const idToken = await user.getIdToken(true);
    
    // トークンの詳細情報を取得
    const idTokenResult = await user.getIdTokenResult();
    
    console.log('IDトークン:', idToken);
    console.log('発行者:', idTokenResult.claims.iss);
    console.log('ユーザーID:', idTokenResult.claims.sub);
    console.log('有効期限:', new Date(idTokenResult.claims.exp * 1000));
    console.log('カスタムクレーム:', idTokenResult.claims);

    return {
      token: idToken,
      claims: idTokenResult.claims,
      expirationTime: idTokenResult.expirationTime
    };
  } catch (error) {
    console.error('トークン取得エラー:', error);
    throw error;
  }
};

サーバーサイドでのトークン検証(Node.js Admin SDK)

const admin = require('firebase-admin');

// Admin SDKの初期化
const serviceAccount = require('./path/to/serviceAccountKey.json');
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount)
});

// IDトークンの検証
const verifyIdToken = async (idToken) => {
  try {
    // トークンの検証とデコード
    const decodedToken = await admin.auth().verifyIdToken(idToken);
    
    console.log('ユーザーID:', decodedToken.uid);
    console.log('メールアドレス:', decodedToken.email);
    console.log('認証プロバイダー:', decodedToken.firebase.sign_in_provider);
    console.log('認証時刻:', new Date(decodedToken.auth_time * 1000));
    
    return decodedToken;
  } catch (error) {
    console.error('トークン検証エラー:', error);
    throw new Error('無効なトークンです');
  }
};

// Express.jsミドルウェアでの使用例
const authenticateToken = async (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"

  if (!token) {
    return res.status(401).json({ error: 'アクセストークンが必要です' });
  }

  try {
    const decodedToken = await verifyIdToken(token);
    req.user = decodedToken;
    next();
  } catch (error) {
    return res.status(403).json({ error: 'トークンが無効です' });
  }
};

カスタムクレームの設定と活用

const admin = require('firebase-admin');

// カスタムクレームの設定(管理者権限)
const setAdminClaim = async (uid) => {
  try {
    await admin.auth().setCustomUserClaims(uid, {
      admin: true,
      role: 'administrator',
      permissions: ['read', 'write', 'delete']
    });
    
    console.log(`ユーザー ${uid} に管理者権限を付与しました`);
  } catch (error) {
    console.error('カスタムクレーム設定エラー:', error);
    throw error;
  }
};

// クライアントサイドでのカスタムクレーム確認
const checkUserRole = async () => {
  const user = auth.currentUser;
  if (!user) return null;

  try {
    // トークンを強制更新してカスタムクレームを取得
    const idTokenResult = await user.getIdTokenResult(true);
    
    const isAdmin = idTokenResult.claims.admin || false;
    const userRole = idTokenResult.claims.role || 'user';
    const permissions = idTokenResult.claims.permissions || [];

    return {
      isAdmin,
      role: userRole,
      permissions
    };
  } catch (error) {
    console.error('ロール確認エラー:', error);
    return null;
  }
};

リアルタイム認証状態監視

import { getAuth, onAuthStateChanged } from 'firebase/auth';

const auth = getAuth();

// 認証状態の監視
const setupAuthListener = () => {
  return onAuthStateChanged(auth, async (user) => {
    if (user) {
      // ユーザーがサインイン中
      console.log('ユーザーサインイン:', user.uid);
      
      try {
        // 最新のトークンを取得
        const idToken = await user.getIdToken();
        
        // APIリクエストのヘッダーに設定
        setAuthHeader(idToken);
        
        // ユーザー情報の更新
        updateUserUI(user);
      } catch (error) {
        console.error('トークン取得エラー:', error);
      }
    } else {
      // ユーザーがサインアウト
      console.log('ユーザーサインアウト');
      clearAuthHeader();
      redirectToLogin();
    }
  });
};

// APIリクエスト用のヘッダー設定
const setAuthHeader = (token) => {
  // Axiosの場合
  axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
  
  // Fetchの場合は、リクエスト毎に設定
  window.firebaseToken = token;
};

// サインアウト処理
const signOutUser = async () => {
  try {
    await auth.signOut();
    console.log('サインアウト完了');
  } catch (error) {
    console.error('サインアウトエラー:', error);
  }
};

Firebase Functions での認証確認

const functions = require('firebase-functions');
const admin = require('firebase-admin');

admin.initializeApp();

// HTTP関数での認証確認
exports.secureFunction = functions.https.onCall(async (data, context) => {
  // 認証状態の確認
  if (!context.auth) {
    throw new functions.https.HttpsError(
      'unauthenticated', 
      'この関数には認証が必要です'
    );
  }

  const uid = context.auth.uid;
  const email = context.auth.token.email;
  
  // カスタムクレームの確認
  const isAdmin = context.auth.token.admin === true;
  
  if (!isAdmin) {
    throw new functions.https.HttpsError(
      'permission-denied', 
      '管理者権限が必要です'
    );
  }

  // 認証済みユーザーのみの処理
  try {
    const result = await performAdminOperation(data, uid);
    return { success: true, data: result };
  } catch (error) {
    throw new functions.https.HttpsError(
      'internal', 
      '処理中にエラーが発生しました'
    );
  }
});