🦓

Express、TypeScriptとFirebaseで認証機能を実装する

に公開

はじめに

Firebase Authentication を使って Web アプリに簡単にユーザー認証機能を実装してみました。
Expressと、Firebase Admin SDKを使用した認証の実装になります。

https://firebase.google.com/products/auth

前提

  • Node.js をインストール済み
  • npm をインストール済み
  • Express.js をインストール済み
  • TypeScript をインストール済み

tl:dr;

  1. Firebase Authentication プロジェクトの初期化
  2. Express側を設定する
  • firebase-admin SDKをインストールする
  • Firebase SDK をプロジェクトに追加する
  1. Next.js側を設定する
  • firebaseパッケージをインストールする
  • Firebase Authentication インスタンスを生成する
  1. 認証フロー

Firebase Authentication プロジェクトの初期化

Firebase Console からプロジェクトを新規に作成し、Firebase Authentication を有効化します。
AuthenticationのSign-in設定画面からプロバイダーを選択します。

Firebase Authentication プロジェクトの設定画面 から、API キーなどを取得します。

https://console.firebase.google.com/u/0/project/_/authentication/users?pli=1

ExpressアプリにFirebase SDK を導入する

Firebase Authentication を使うには、Firebase SDK をプロジェクトに追加します。

  1. コンソールの設定画面→サービスアカウントからプロジェクトの環境変数をダウンロードし、 envファイルに追加します。
  2. npm install firebase-adminでSDKをインストールします。
  3. プロジェクトに firebase-admin.ts を作成し、情報を貼り付けます。
src/config/firebase-admin.ts
import admin, { ServiceAccount } from 'firebase-admin';

const serviceAccount = {
  type: 'service_account',
  project_id: process.env.FIREBASE_PROJECT_ID,
  private_key:
    process.env.FIREBASE_PRIVATE_KEY &&
    process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'),
  client_email: process.env.FIREBASE_CLIENT_EMAIL,
};

try {
  if (!admin.apps.length) {
    admin.initializeApp({
      credential: admin.credential.cert(serviceAccount as ServiceAccount),
    });
  }
} catch (error) {
  console.error('Firebase admin initialization error:', error);
  throw error;
}

export const auth = admin.auth();
export const db = admin.firestore();

https://firebase.google.com/docs/admin/setup#add-sdk

Firebase Authentication API を使う準備

Firebase Authentication API を使うには、Firebaseをインポートして認証インスタンスを生成します。
クライアントではNext.jsとTypeScriptを使ってます。
コンソールの設定画面から取得した環境変数をenvファイルに追加します。

firebase.ts を作成します。

src/lib/firebase.ts
import { initializeApp } from 'firebase/app'
import { getAuth, GoogleAuthProvider } from 'firebase/auth'
import { getFirestore } from 'firebase/firestore'

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
}

const app = initializeApp(firebaseConfig)

export const db = getFirestore(app)
export const auth = getAuth(app)
export const provider = new GoogleAuthProvider()

https://www.npmjs.com/package/firebase

Firebase Authentication API を使う準備ができました。
Next.js側の実装については別記事でまとめる予定です。

認証フロー

今回の認証機能において、このようなフローで実装してみました。

Express側において、

  • すべてのAPIリクエストにIDトークンが必要なため、トークンの検証
    • APIリクエスト時にIDトークンを付与 (フロントエンド)
  • 保護されたリソースへのアクセス許可

「セキュリティの確保」を担当に集中するように実装してみました。

フロントエンド(Next.js + Firebase)とバックエンド(Express + Firebase Admin)の認証における役割分担:

フロントエンド(Next.js + Firebase) バックエンド(Express + Firebase Admin)
認証基本機能 - ログインUI/UXの提供
- 認証処理の実行
- セッションの管理
- ユーザー情報の検証
トークン管理 - IDトークンの取得
- トークンの更新
- APIリクエストへのトークン付与
- トークンのキャッシュ管理
- トークンの有効性検証
- トークンの失効処理
- セキュリティルールの適用
状態管理 - 認証状態の監視
- ユーザー情報の状態管理
- ログイン状態のUI反映
- ミドルウェア
- ユーザーセッションの管理
- アクセス制御
- ユーザー状態の検証
エラーハンドリング - 認証エラーのUI表示
- トークン期限切れの処理
- 再試行ロジックの実装
- ユーザーへのフィードバック
- エラーレスポンスの生成
- セキュリティ違反の検出
- エラーログの記録

トークン検証のミドルウェア

Express 側にトークンの検証をするためのミドルウェアを実装します。

  • すべてのリクエストは最初に認証ミドルウェアを通過
  • トークンの存在確認とフォーマット検証
  • Firebase Adminを使用してトークンの有効性を検証
  • 検証成功時:デコードされたユーザー情報をリクエストオブジェクトに追加
  • 検証失敗時:401エラーを返却

Firebase クライアント アプリがカスタム バックエンド サーバーと通信する場合、そのサーバーに現在ログインしているユーザーを特定する必要が生じる場合があります。これを安全に行うために、正常なログイン後、ユーザーの ID トークンを HTTPS を使ってサーバーに送信します。次に、サーバー上で ID トークンの完全性と信頼度を確認し、ID トークンの uid を取得します。サーバーで現在ログインしているユーザーを安全に特定するために、この方法で送信された uid を使用できます。

Firebase Admin SDK には、ID トークンを確認してデコードするための組み込みのメソッドが用意されています。提供された ID トークンが正しい形式で、期限切れではなく、適切に署名されていれば、メソッドはデコードされた ID トークンを返します。デコードされたトークンからユーザーまたはデバイスの uid を取得できます。

src/middleware/auth.ts
// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import { DecodedIdToken } from 'firebase-admin/lib/auth/token-verifier';
import { auth } from '../config/firebase-admin';
import * as admin from 'firebase-admin';

// リクエストにユーザー情報を検証するための型定義
interface AuthRequest extends Request {
  user?: DecodedIdToken;
}

export const verifyAuth = async (
  req: AuthRequest,
  res: Response,
  next: NextFunction
): Promise<void> => {
  try {
    // リクエストヘッダーからトークンを取得
    const token = req.headers.authorization;
    if (!token) {
      return res.status(401).json({
        error: 'Authorization token required'
      });
    }
    // カスタムクレームのチェックがあれば追加

    // トークンを検証し、デコードされたユーザー情報を取得
    const decodedToken = await auth().verifyIdToken(token);

    // リクエストオブジェクトにユーザー情報を追加
    req.user = decodedToken;

    next();
  } catch (error) {
    console.error('Auth error:', error);

    return res.status(401).json({
      error: 'Authentication error'
    });
  }
};

https://firebase.google.com/docs/auth/admin/verify-id-tokens?hl=ja#ios+

保護されたリソースへのアクセス許可

認証ミドルウェアを実装されたら、アプリケーションへ統合します。
認証済みユーザーのみがアクションを実行できるようにapp.tsに追加します

decondedTokenからuidを使用して、そのユーザーに関連するデータのみにアクセスできるようになります。

src/app.ts
// src/app.ts
import express from 'express';
import { verifyAuth } from './middleware/auth';
import actionRoutes from './routes/actions';

const app = express();

app.use(express.json());
// ミドルウェア使用
app.use('/api/actions', verifyAuth, actionRoutes);

startServer();

export default app;

https://expressjs.com/en/guide/using-middleware.html

終わりに

Firebaseが提供する認証基盤を使用し、Expressは認証ロジック自体は実装せず、トークンの検証とユーザー情報の取得を行うことによってフロントとの安全な通信を確保し、認証済みユーザーのみがアクションを実行できる仕組みを実現することができました。

Firebase認証への依存に関するリスクが存在しますが、プロトタイプのため、現時点でのFirebaseの利点(管理と実装のコスト、セキュリティ、スケーラビリティ)は、これらのリスクを上回ることが多いため、プロジェクトの要件に応じて判断しました。

Discussion