Firebase Authentication実践ガイド

中級 | 30分 で読める | 2025.01.10

公式ドキュメント

Firebase Authenticationとは

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

セットアップ

実践メモ: Firebase Consoleでプロジェクトを作成後、Web appを追加するとfirebaseConfigが自動生成されます。

# 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);

注意: Web向け`firebaseConfig`のAPIキーはクライアント識別用で、認可を担う秘密鍵ではありません。ただし、これだけでデータは保護されません。Security Rules、Authentication、APIキー制限、App Checkなどを用途に応じて設定してください。Firebase Adminの秘密鍵は絶対に公開しないでください

メール/パスワード認証

ユーザー登録

// 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>
 );
}

ポイント: signInWithPopupはポップアップ、signInWithRedirectはリダイレクト方式です。モバイルではリダイレクト方式が安定します。

認証状態の監視

// 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}</>;
}

onAuthStateChangedのクリーンアップ関数を必ずreturnしてメモリリークを防いでください。

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

実践メモ: getIdToken()は自動的にトークンをリフレッシュします。期限切れを気にせず毎回呼び出してOKです。

セキュリティルール(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 Emulatorで行ってください。本番に直接デプロイして確認するのは危険です

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

参考リソース

← 一覧に戻る
PR
PR
PR
PR