Firebaseによる認証機能の実装

Firebaseプロジェクトの設定

Firebaseプロジェクトを作成する

Firebase 構築 > Authenticationを開く

プロバイダから「匿名」を選択し、有効にする

次に、プロバイダの追加で、「Google」を選択し、有効にする

GCPサービスアカウントの所得

  • Firebaseプロジェクトと同名で、GCPプロジェクトが作成されている
  • IAMには、Firebase管理用のサービスアカウントが作成されている
  • private keyを作成する、これは、後述のfirebase-adminの初期化に使用する

Next.jsアプリの構築

Next.jsアプリを生成する

% npx create-next-app@latest apps/web
✔ Would you like to use TypeScript? … No / Yes
✔ Which linter would you like to use? › ESLint
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like your code inside a `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to use Turbopack? (recommended) … No / Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No / Yes
Creating a new Next.js app in xxx

Using npm.

Initializing project with template: app-tw 


Installing dependencies:
- react
- react-dom
- next

Installing devDependencies:
- typescript
- @types/node
- @types/react
- @types/react-dom
- @tailwindcss/postcss
- tailwindcss
- eslint
- eslint-config-next
- @eslint/eslintrc


added 335 packages, and audited 336 packages in 17s

137 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
Success! Created web at xxx

apps/webフォルダ以下のファイル構成

apps/web
apps/web/app
apps/web/app/favicon.ico
apps/web/app/layout.tsx
apps/web/app/page.tsx
apps/web/app/globals.css
apps/web/postcss.config.mjs
apps/web/node_modules
apps/web/next-env.d.ts
apps/web/README.md
apps/web/public
apps/web/public/file.svg
apps/web/public/vercel.svg
apps/web/public/next.svg
apps/web/public/globe.svg
apps/web/public/window.svg
apps/web/.gitignore
apps/web/package-lock.json
apps/web/package.json
apps/web/tsconfig.json
apps/web/eslint.config.mjs
apps/web/next.config.ts

NestJS作成ブログと合わせて、以下の構成となった

package.jsonを持つアプリとしては以下の3つ

  • api
  • web
  • @app/adapters
root/
├─ apps/
│   ├─ api/      ← NestJS バックエンド(本物の API サーバー)
│   │   ├─ src/chat/        ← /chat エンドポイント関連
│   │   ├─ src/auth/        ← Firebase Guard, 認証まわり
│   │
│   └─ web/      ← Next.js フロントエンド
│       ├─ app/             ← Next.js App Router のページ
│       │   └─ api/ ← プロキシ用 API Routes(CORS回避が必要なら)
│       ├─ components/      ← Reactコンポーネント(ChatBox, Avatarなど)
│       ├─ lib/             ← フロント専用のユーティリティ(API呼び出し, firebase.ts)
│       └─ public/          ← 画像・静的ファイル
│
├─ libs/          ← 共通ライブラリ群(apps/api と apps/web 両方で使う)
│   └─ adapters/       ← LLM 抽象化 (LlmAdapter, OpenAiLlmAdapter, GeminiLlmAdapter)
│
└─ package.json   ← ルートの依存管理 (pnpm workspace)

Firebaseによる認証の実装

firebaseパッケージのインストール

% pnpm add firebase --filter web
% pnpm add firebase-admin --filter api
% pnpm add firebase-admin --filter @app/adapters

web: フロントエンド用のFirebase認証機能関数

import { initializeApp } from 'firebase/app';
import { 
  getAuth, 
  signInAnonymously, 
  GoogleAuthProvider, 
  signInWithPopup 
} from 'firebase/auth';

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!,
};

const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);

// 匿名ログイン
export async function loginAnonymously() {
  const result = await signInAnonymously(auth);
  return result.user;
}

// Google ログイン
export async function loginWithGoogle() {
  const provider = new GoogleAuthProvider();
  const result = await signInWithPopup(auth, provider);
  return result.user;
}

// IDトークン取得(APIコール時に使用)
export async function getIdToken() {
  if (!auth.currentUser) return null;
  return await auth.currentUser.getIdToken();
}

FirebaseでWebアプリの作成

  1. Firebase Console にログイン
    作成したプロジェクトを開く
  2. 左メニュー → ⚙️ [プロジェクトの設定] をクリック
  3. 「自分のアプリ」セクションで Web アプリ(</> マーク) を選択
    • もしまだ Web アプリを追加していないなら「アプリを追加」で Web を選ぶ
  4. 登録が終わると、Firebase SDK の設定スニペットが出ます:
const firebaseConfig = {
  apiKey: "AIza...xxxx",
  authDomain: "your-project-id.firebaseapp.com",
  projectId: "your-project-id",
  storageBucket: "your-project-id.appspot.com",
  messagingSenderId: "1234567890",
  appId: "1:1234567890:web:abcdef123456",
};

apps/web/.env.localにセットする

NEXT_PUBLIC_FIREBASE_API_KEY={apiKey}
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN={authDomain}
NEXT_PUBLIC_FIREBASE_PROJECT_ID={projectId}

@app/adapters: バックエンド用Firebase認証検証関数

  • verifyFirebaseTokenを実装
import * as admin from 'firebase-admin';

  const projectId   = process.env.FIREBASE_PROJECT_ID;
  const clientEmail = process.env.FIREBASE_CLIENT_EMAIL;
  const privateKey  = process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n');

  if (!admin.apps.length) {
    admin.initializeApp({
      credential: admin.credential.cert({ projectId, clientEmail, privateKey }),
    });
  }

export async function verifyFirebaseToken(token: string) {
  return await admin.auth().verifyIdToken(token);
}

api: NestJS用のFirebaseトークン検証用ミドルウェア

  • リクエストヘッダーから Authorization を取得し、トークンがなければ認証エラーを返します。
  • トークンを verifyFirebaseToken で検証し、正しければデコードしたユーザー情報を req.user にセットします。
  • トークンが無効なら認証エラーを返します。
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { verifyFirebaseToken } from '@app/adapters/firebase-auth.adapter';

@Injectable()
export class FirebaseAuthGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const req = context.switchToHttp().getRequest();
    const authHeader = req.headers['authorization'];

    if (!authHeader) throw new UnauthorizedException('Missing token');

    const token = authHeader.split(' ')[1];
    try {
      const decoded = await verifyFirebaseToken(token);
      req.user = decoded;
      return true;
    } catch (e) {
      throw new UnauthorizedException('Invalid token');
    }
  }
}

web: コントローラーの変更

  • @UseGuards(FirebaseAuthGuard)により認証
import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common';
import { ChatService } from './chat.service';
import { FirebaseAuthGuard } from '../auth/firebase-auth.guard';

@Controller('chat')
export class ChatController {
  constructor(private readonly chatService: ChatService) {}

  @UseGuards(FirebaseAuthGuard)
  @Post()
  async chat(@Body('message') message: string, @Req() req: any) {
    return this.chatService.getReply(message, req.user.uid);
  }
}

web: フロント用API 呼び出し関数

import { getIdToken } from './firebase';

export async function sendChat(message: string) {
  const token = await getIdToken();
  const res = await fetch('/api/chat', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({ message }),
  });

  if (!res.ok) throw new Error('API error');
  return res.json();
}

web: UI用のapi実装

  • UIかのリクエストを受取、バックエンドのapiとやりとりする
import { NextRequest, NextResponse } from 'next/server';

export async function POST(req: NextRequest) {
  const body = await req.json();
  const token = req.headers.get('authorization');

  const res = await fetch(process.env.NEST_API_URL + '/chat', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: token ?? '',
    },
    body: JSON.stringify(body),
  });

  return NextResponse.json(await res.json());
}

動作確認

NestJS, Next.jsアプリとFirebase, ChatGPTが連携することを確認しました。

注意

  • webがホットリロードされるので、@app/adaptersもホットリロードされると思い込み、デバッグに時間がかかった
  • monorepoで、3つのアプリのpackage.json, tsconfig.jsonの設定の方法が自由度が高すぎて困難。ビルドがうまくいったと思ったら、実行がうまくいかないとか、試行錯誤が数時間続きました。

関連記事

カテゴリー

アーカイブ

Lang »