Firebase Authentication実践ガイド

中級 | 30分 read | 2025.01.10

Firebase Authenticationとは

Firebase Authenticationは、Googleが提供するBaaS(Backend as a Service)の認証サービスです。メール/パスワード認証、OAuth認証、電話番号認証などを簡単に実装できます。

セットアップ

# npm
npm install firebase

# yarn
yarn add firebase
// lib/firebase.ts
import { initializeApp } from 'firebase/app';
import { getAuth } 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,
  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,
};

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

メール/パスワード認証

ユーザー登録

// hooks/useAuth.ts
import {
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  signOut,
  sendPasswordResetEmail,
  updateProfile,
} from 'firebase/auth';
import { auth } from '@/lib/firebase';

export function useAuth() {
  const signUp = async (email: string, password: string, displayName: string) => {
    try {
      const { user } = await createUserWithEmailAndPassword(auth, email, password);

      // プロフィール更新
      await updateProfile(user, { displayName });

      return { user, error: null };
    } catch (error: any) {
      return { user: null, error: getErrorMessage(error.code) };
    }
  };

  const signIn = async (email: string, password: string) => {
    try {
      const { user } = await signInWithEmailAndPassword(auth, email, password);
      return { user, error: null };
    } catch (error: any) {
      return { user: null, error: getErrorMessage(error.code) };
    }
  };

  const logout = () => signOut(auth);

  const resetPassword = async (email: string) => {
    try {
      await sendPasswordResetEmail(auth, email);
      return { success: true, error: null };
    } catch (error: any) {
      return { success: false, error: getErrorMessage(error.code) };
    }
  };

  return { signUp, signIn, logout, resetPassword };
}

// エラーメッセージの日本語化
function getErrorMessage(code: string): string {
  const messages: Record<string, string> = {
    'auth/email-already-in-use': 'このメールアドレスは既に使用されています',
    'auth/invalid-email': '無効なメールアドレスです',
    'auth/weak-password': 'パスワードは6文字以上で入力してください',
    'auth/user-not-found': 'ユーザーが見つかりません',
    'auth/wrong-password': 'パスワードが正しくありません',
    'auth/too-many-requests': 'リクエストが多すぎます。しばらく待ってから再試行してください',
  };
  return messages[code] || '認証エラーが発生しました';
}

認証フォーム

// components/SignUpForm.tsx
'use client';

import { useState } from 'react';
import { useAuth } from '@/hooks/useAuth';

export function SignUpForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [name, setName] = useState('');
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);
  const { signUp } = useAuth();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError('');

    const { user, error } = await signUp(email, password, name);

    if (error) {
      setError(error);
    } else {
      // リダイレクトなど
    }

    setLoading(false);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="名前"
        value={name}
        onChange={(e) => setName(e.target.value)}
        required
      />
      <input
        type="email"
        placeholder="メールアドレス"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        required
      />
      <input
        type="password"
        placeholder="パスワード(6文字以上)"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        minLength={6}
        required
      />
      {error && <p className="error">{error}</p>}
      <button type="submit" disabled={loading}>
        {loading ? '登録中...' : 'アカウント作成'}
      </button>
    </form>
  );
}

OAuth認証(Google/GitHub)

// hooks/useOAuth.ts
import {
  signInWithPopup,
  GoogleAuthProvider,
  GithubAuthProvider,
} from 'firebase/auth';
import { auth } from '@/lib/firebase';

export function useOAuth() {
  const signInWithGoogle = async () => {
    const provider = new GoogleAuthProvider();
    provider.addScope('profile');
    provider.addScope('email');

    try {
      const { user } = await signInWithPopup(auth, provider);
      return { user, error: null };
    } catch (error: any) {
      return { user: null, error: error.message };
    }
  };

  const signInWithGitHub = async () => {
    const provider = new GithubAuthProvider();
    provider.addScope('read:user');
    provider.addScope('user:email');

    try {
      const { user } = await signInWithPopup(auth, provider);
      return { user, error: null };
    } catch (error: any) {
      return { user: null, error: error.message };
    }
  };

  return { signInWithGoogle, signInWithGitHub };
}
// components/OAuthButtons.tsx
'use client';

import { useOAuth } from '@/hooks/useOAuth';

export function OAuthButtons() {
  const { signInWithGoogle, signInWithGitHub } = useOAuth();

  return (
    <div className="oauth-buttons">
      <button onClick={signInWithGoogle}>
        Googleでログイン
      </button>
      <button onClick={signInWithGitHub}>
        GitHubでログイン
      </button>
    </div>
  );
}

認証状態の監視

// contexts/AuthContext.tsx
'use client';

import { createContext, useContext, useEffect, useState } from 'react';
import { onAuthStateChanged, User } from 'firebase/auth';
import { auth } from '@/lib/firebase';

type AuthContextType = {
  user: User | null;
  loading: boolean;
};

const AuthContext = createContext<AuthContextType>({
  user: null,
  loading: true,
});

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      setUser(user);
      setLoading(false);
    });

    return () => unsubscribe();
  }, []);

  return (
    <AuthContext.Provider value={{ user, loading }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useUser = () => useContext(AuthContext);
// 使用例
function Dashboard() {
  const { user, loading } = useUser();

  if (loading) return <div>読み込み中...</div>;
  if (!user) return <div>ログインしてください</div>;

  return (
    <div>
      <p>ようこそ、{user.displayName}さん</p>
      <img src={user.photoURL || ''} alt="プロフィール画像" />
    </div>
  );
}

保護されたルート

// components/ProtectedRoute.tsx
'use client';

import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useUser } from '@/contexts/AuthContext';

export function ProtectedRoute({ children }: { children: React.ReactNode }) {
  const { user, loading } = useUser();
  const router = useRouter();

  useEffect(() => {
    if (!loading && !user) {
      router.push('/login');
    }
  }, [user, loading, router]);

  if (loading) {
    return <div>読み込み中...</div>;
  }

  if (!user) {
    return null;
  }

  return <>{children}</>;
}

IDトークンの取得

// APIリクエスト時の認証
async function fetchWithAuth(url: string) {
  const user = auth.currentUser;

  if (!user) {
    throw new Error('Not authenticated');
  }

  const token = await user.getIdToken();

  const response = await fetch(url, {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });

  return response.json();
}

サーバーサイドでの検証

// api/protected/route.ts (Next.js)
import { getAuth } from 'firebase-admin/auth';
import { initializeApp, getApps, cert } from 'firebase-admin/app';

// Firebase Admin初期化
if (!getApps().length) {
  initializeApp({
    credential: cert({
      projectId: process.env.FIREBASE_PROJECT_ID,
      clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
      privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
    }),
  });
}

export async function GET(request: Request) {
  const authorization = request.headers.get('Authorization');

  if (!authorization?.startsWith('Bearer ')) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const token = authorization.split('Bearer ')[1];

  try {
    const decodedToken = await getAuth().verifyIdToken(token);
    const userId = decodedToken.uid;

    // ユーザー固有のデータを返す
    return Response.json({ userId, data: '...' });
  } catch (error) {
    return Response.json({ error: 'Invalid token' }, { status: 401 });
  }
}

メール確認

import { sendEmailVerification } from 'firebase/auth';

// メール確認送信
async function sendVerificationEmail() {
  const user = auth.currentUser;
  if (user && !user.emailVerified) {
    await sendEmailVerification(user);
    alert('確認メールを送信しました');
  }
}

// 確認状態チェック
function isEmailVerified() {
  return auth.currentUser?.emailVerified ?? false;
}

セキュリティルール(Firestore)

// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // 認証済みユーザーのみ読み書き可能
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }

    // 公開データ(誰でも読める)
    match /public/{document=**} {
      allow read: if true;
      allow write: if request.auth != null;
    }
  }
}

関連記事

まとめ

Firebase Authenticationは、多様な認証方法を簡単に実装できるサービスです。クライアントサイドでの認証とサーバーサイドでの検証を組み合わせて、セキュアなアプリケーションを構築しましょう。

← Back to list