mcpField
F
firebase-auth

firebase-auth

github.com/jezwebセキュリティ1.1k363 回インストール3 週間前に更新
リポジトリ

firebase-auth について

Firebase Authentication を利用する開発者向けの、クライアントとサーバーの実装ガイドです。ログイン方式、トークンとセッション管理、ロールのクレーム、セキュリティ上の注意点を扱います。

主な機能

  1. 01メール・パスワードと OAuth のログイン例
  2. 02電話認証と多要素認証のガイド
  3. 03ID トークン検証とセッション Cookie 管理
  4. 04カスタムクレームと保護されたルート
  5. 05一般的なエラー処理とセキュリティの助言

活用シーン

  1. 01登録、ログイン、パスワード再設定の追加
  2. 02サーバーでのユーザートークン検証
  3. 03ロール設定と管理ルートの保護

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jezweb/claude-skills --skill firebase-auth

インストール後、Claude Code で/firebase-authを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

README.md
.claude-plugin/
plugin.json
rules/
firebase-auth.md
templates/
api-session.ts
auth-context.tsx
middleware.ts
protected-route.tsx

詳細

Firebase Authentication

Status:本番環境対応済み Last Updated:2026-01-25 Dependencies:なし(単独のスキル) Latest Versions:[email protected], [email protected]


クイックスタート(5 分)

1. Firebase Console で認証プロバイダーを有効にする
  1. Firebase Console > Authentication > Sign-in method に移動します
  2. 使用するプロバイダー(Email/Password、Google など)を有効にします
  3. OAuth プロバイダーのクライアント ID/シークレットを設定します
2. Firebase Auth を初期化する(クライアント)
// src/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,
  // ... other config
};

const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
3. Firebase Admin を初期化する(サーバー)
// src/lib/firebase-admin.ts
import { initializeApp, cert, getApps } from 'firebase-admin/app';
import { getAuth } from 'firebase-admin/auth';

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 const adminAuth = getAuth();

メールアドレス/パスワード認証

アカウント登録
import { createUserWithEmailAndPassword, sendEmailVerification, updateProfile } from 'firebase/auth';
import { auth } from './firebase';

async function signUp(email: string, password: string, displayName: string) {
  try {
    const userCredential = await createUserWithEmailAndPassword(auth, email, password);
    const user = userCredential.user;

    // Update display name
    await updateProfile(user, { displayName });

    // Send verification email
    await sendEmailVerification(user);

    return user;
  } catch (error: any) {
    switch (error.code) {
      case 'auth/email-already-in-use':
        throw new Error('Email already registered');
      case 'auth/invalid-email':
        throw new Error('Invalid email address');
      case 'auth/weak-password':
        throw new Error('Password must be at least 6 characters');
      default:
        throw new Error('Sign up failed');
    }
  }
}
サインイン
import { signInWithEmailAndPassword } from 'firebase/auth';
import { auth } from './firebase';

async function signIn(email: string, password: string) {
  try {
    const userCredential = await signInWithEmailAndPassword(auth, email, password);
    return userCredential.user;
  } catch (error: any) {
    switch (error.code) {
      case 'auth/user-not-found':
      case 'auth/wrong-password':
      case 'auth/invalid-credential':
        throw new Error('Invalid email or password');
      case 'auth/user-disabled':
        throw new Error('Account has been disabled');
      case 'auth/too-many-requests':
        throw new Error('Too many attempts. Try again later.');
      default:
        throw new Error('Sign in failed');
    }
  }
}
サインアウト
import { signOut } from 'firebase/auth';
import { auth } from './firebase';

async function handleSignOut() {
  await signOut(auth);
  // Redirect to login page
}
パスワードのリセット
import { sendPasswordResetEmail, confirmPasswordReset } from 'firebase/auth';
import { auth } from './firebase';

// Send reset email
async function resetPassword(email: string) {
  await sendPasswordResetEmail(auth, email);
}

// Confirm reset (from email link)
async function confirmReset(oobCode: string, newPassword: string) {
  await confirmPasswordReset(auth, oobCode, newPassword);
}

OAuth プロバイダー(Google、GitHub など)

Google でサインイン
import { signInWithPopup, signInWithRedirect, GoogleAuthProvider } from 'firebase/auth';
import { auth } from './firebase';

const googleProvider = new GoogleAuthProvider();
googleProvider.addScope('email');
googleProvider.addScope('profile');

// Popup method (recommended for desktop)
async function signInWithGoogle() {
  try {
    const result = await signInWithPopup(auth, googleProvider);
    const credential = GoogleAuthProvider.credentialFromResult(result);
    const token = credential?.accessToken;
    return result.user;
  } catch (error: any) {
    if (error.code === 'auth/popup-closed-by-user') {
      // User closed popup - not an error
      return null;
    }
    if (error.code === 'auth/popup-blocked') {
      // Fallback to redirect
      await signInWithRedirect(auth, googleProvider);
      return null;
    }
    throw error;
  }
}

// Redirect method (for mobile or popup-blocked)
async function signInWithGoogleRedirect() {
  await signInWithRedirect(auth, googleProvider);
}
リダイレクト結果を処理する
import { getRedirectResult, GoogleAuthProvider } from 'firebase/auth';
import { auth } from './firebase';

// Call on page load
async function handleRedirectResult() {
  try {
    const result = await getRedirectResult(auth);
    if (result) {
      const credential = GoogleAuthProvider.credentialFromResult(result);
      return result.user;
    }
  } catch (error) {
    console.error('Redirect sign-in error:', error);
  }
  return null;
}
その他の OAuth プロバイダー
import {
  GithubAuthProvider,
  TwitterAuthProvider,
  FacebookAuthProvider,
  OAuthProvider,
} from 'firebase/auth';

// GitHub
const githubProvider = new GithubAuthProvider();
githubProvider.addScope('read:user');

// Microsoft
const microsoftProvider = new OAuthProvider('microsoft.com');
microsoftProvider.addScope('email');
microsoftProvider.addScope('profile');

// Apple
const appleProvider = new OAuthProvider('apple.com');
appleProvider.addScope('email');
appleProvider.addScope('name');

認証状態の管理

認証状態の変更を監視する
import { onAuthStateChanged, User } from 'firebase/auth';
import { auth } from './firebase';

// React hook example
function useAuth() {
  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 { user, loading };
}
認証コンテキストプロバイダー(React)
// src/contexts/AuthContext.tsx
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { onAuthStateChanged, User } from 'firebase/auth';
import { auth } from '@/lib/firebase';

interface AuthContextType {
  user: User | null;
  loading: boolean;
}

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

export function AuthProvider({ children }: { children: 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 useAuth = () => useContext(AuthContext);

トークン管理

ID トークンを取得する(API 呼び出し用)
import { auth } from './firebase';

async function getIdToken() {
  const user = auth.currentUser;
  if (!user) throw new Error('Not authenticated');

  // Force refresh to get fresh token
  const token = await user.getIdToken(/* forceRefresh */ true);
  return token;
}

// Use in API calls
async function callProtectedAPI() {
  const token = await getIdToken();

  const response = await fetch('/api/protected', {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });

  return response.json();
}
ID トークンを検証する(サーバー側)
// API route (Next.js example)
import { adminAuth } from '@/lib/firebase-admin';

export async function GET(request: Request) {
  const authHeader = request.headers.get('authorization');
  if (!authHeader?.startsWith('Bearer ')) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

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

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

    // User is authenticated, proceed with request
    return Response.json({ uid, message: 'Authenticated' });
  } catch (error) {
    return Response.json({ error: 'Invalid token' }, { status: 401 });
  }
}
セッション Cookie(サーバーサイドレンダリング)
import { adminAuth } from '@/lib/firebase-admin';

// Create session cookie
async function createSessionCookie(idToken: string) {
  const expiresIn = 60 * 60 * 24 * 5 * 1000; // 5 days

  const sessionCookie = await adminAuth.createSessionCookie(idToken, {
    expiresIn,
  });

  return sessionCookie;
}

// Verify session cookie
async function verifySessionCookie(sessionCookie: string) {
  try {
    const decodedClaims = await adminAuth.verifySessionCookie(sessionCookie, true);
    return decodedClaims;
  } catch (error) {
    return null;
  }
}

// Revoke session
async function revokeSession(uid: string) {
  await adminAuth.revokeRefreshTokens(uid);
}

カスタムクレームとロール

カスタムクレームを設定する(Admin SDK)
import { adminAuth } from '@/lib/firebase-admin';

// Set admin role
async function setAdminRole(uid: string) {
  await adminAuth.setCustomUserClaims(uid, {
    admin: true,
    role: 'admin',
  });
}

// Set custom permissions
async function setUserPermissions(uid: string, permissions: string[]) {
  await adminAuth.setCustomUserClaims(uid, {
    permissions,
  });
}
カスタムクレームを確認する(クライアント)
import { auth } from './firebase';

async function checkAdminStatus() {
  const user = auth.currentUser;
  if (!user) return false;

  // Force token refresh to get latest claims
  const tokenResult = await user.getIdTokenResult(true);
  return tokenResult.claims.admin === true;
}

// In component
const isAdmin = await checkAdminStatus();

重要: カスタムクレームは ID トークンにキャッシュされます。クレームを設定した後、ユーザーは次のいずれかを行う必要があります。

  1. サインアウトして、再度サインインする、または
  2. getIdTokenResult(true) でトークンを強制的に更新する

電話番号認証

import {
  signInWithPhoneNumber,
  RecaptchaVerifier,
  ConfirmationResult,
} from 'firebase/auth';
import { auth } from './firebase';

let confirmationResult: ConfirmationResult;

// Step 1: Setup reCAPTCHA (required)
function setupRecaptcha() {
  window.recaptchaVerifier = new RecaptchaVerifier(auth, 'recaptcha-container', {
    size: 'normal',
    callback: () => {
      // reCAPTCHA solved
    },
  });
}

// Step 2: Send verification code
async function sendVerificationCode(phoneNumber: string) {
  const appVerifier = window.recaptchaVerifier;
  confirmationResult = await signInWithPhoneNumber(auth, phoneNumber, appVerifier);
}

// Step 3: Verify code
async function verifyCode(code: string) {
  const result = await confirmationResult.confirm(code);
  return result.user;
}

多要素認証(MFA)

import {
  multiFactor,
  PhoneAuthProvider,
  PhoneMultiFactorGenerator,
  getMultiFactorResolver,
} from 'firebase/auth';
import { auth } from './firebase';

// Enroll phone as second factor
async function enrollPhoneMFA(phoneNumber: string) {
  const user = auth.currentUser;
  if (!user) throw new Error('Not authenticated');

  const multiFactorSession = await multiFactor(user).getSession();

  const phoneAuthProvider = new PhoneAuthProvider(auth);
  const verificationId = await phoneAuthProvider.verifyPhoneNumber(
    { phoneNumber, session: multiFactorSession },
    window.recaptchaVerifier
  );

  // Return verificationId to complete enrollment after user enters code
  return verificationId;
}

// Complete enrollment
async function completeEnrollment(verificationId: string, verificationCode: string) {
  const user = auth.currentUser;
  if (!user) throw new Error('Not authenticated');

  const credential = PhoneAuthProvider.credential(verificationId, verificationCode);
  const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(credential);

  await multiFactor(user).enroll(multiFactorAssertion, 'Phone Number');
}

// Handle MFA during sign-in
async function handleMFASignIn(error: any) {
  if (error.code !== 'auth/multi-factor-auth-required') {
    throw error;
  }

  const resolver = getMultiFactorResolver(auth, error);
  // Show UI to select MFA method and enter code
  return resolver;
}

保護されたルート(Next.js)

ミドルウェアによる保護
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const sessionCookie = request.cookies.get('session')?.value;

  // Protect /dashboard routes
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    if (!sessionCookie) {
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }

  // Redirect authenticated users away from /login
  if (request.nextUrl.pathname === '/login') {
    if (sessionCookie) {
      return NextResponse.redirect(new URL('/dashboard', request.url));
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/login'],
};
クライアント側のルートガード
// components/ProtectedRoute.tsx
import { useAuth } from '@/contexts/AuthContext';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

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

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

  if (loading) {
    return <div>Loading...</div>;
  }

  if (!user) {
    return null;
  }

  return <>{children}</>;
}

エラー処理

よくある認証エラー
エラーコード説明ユーザー向けメッセージ
auth/invalid-credentialメールアドレスまたはパスワードが間違っていますメールアドレスまたはパスワードが無効です
auth/user-not-foundメールアドレスが登録されていませんメールアドレスまたはパスワードが無効です
auth/wrong-passwordパスワードが正しくありませんメールアドレスまたはパスワードが無効です
auth/email-already-in-useメールアドレスは登録済みですこのメールアドレスはすでに登録されています
auth/weak-passwordパスワードが 6 文字未満ですパスワードは 6 文字以上にしてください
auth/invalid-emailメールアドレスの形式が正しくありません有効なメールアドレスを入力してください
auth/user-disabledアカウントが無効になっていますお客様のアカウントは無効になっています
auth/too-many-requestsリクエスト回数が制限されています試行回数が多すぎます。しばらくしてからもう一度お試しください
auth/popup-closed-by-userユーザーがポップアップを閉じましたサインインがキャンセルされました
auth/popup-blockedブラウザーがポップアップをブロックしましたポップアップを許可してください
auth/requires-recent-login機密性の高い操作ですもう一度サインインしてください
auth/network-request-failedネットワークエラーです接続を確認してください
エラーハンドラーのユーティリティ
export function getAuthErrorMessage(error: any): string {
  const errorMessages: Record<string, string> = {
    'auth/invalid-credential': 'Invalid email or password',
    'auth/user-not-found': 'Invalid email or password',
    'auth/wrong-password': 'Invalid email or password',
    'auth/email-already-in-use': 'This email is already registered',
    'auth/weak-password': 'Password must be at least 6 characters',
    'auth/invalid-email': 'Please enter a valid email address',
    'auth/user-disabled': 'Your account has been disabled',
    'auth/too-many-requests': 'Too many attempts. Please try again later',
    'auth/popup-closed-by-user': 'Sign-in was cancelled',
    'auth/network-request-failed': 'Network error. Please check your connection',
  };

  return errorMessages[error.code] || 'An unexpected error occurred';
}

既知の問題の防止

このスキルは、Firebase Auth に関する文書化された 12 件のエラーを防ぎます。

問題 #エラー/問題説明回避方法出典
#1auth/invalid-credentialメールアドレスまたはパスワードが間違っている場合の一般的なエラー「メールアドレスまたはパスワードが無効です」という一般的なメッセージを表示する一般
#2auth/popup-blockedブラウザーが OAuth ポップアップをブロックするリダイレクトへのフォールバックを実装するドキュメント
#3auth/requires-recent-login機密性の高い操作には直近のログインが必要パスワード変更やアカウント削除の前に再認証するドキュメント
#4カスタムクレームが更新されないクレームが ID トークンにキャッシュされるクレーム設定後にトークンを強制的に更新するドキュメント
#5トークンの有効期限切れID トークンは 1 時間後に期限切れになるAPI 呼び出しの前に必ず getIdToken() を呼び出す一般
#6認証リスナーによるメモリリークonAuthStateChanged の購読を解除していないuseEffect 内でクリーンアップ関数を返す一般
#7localhost での CORS エラー認証ドメインが一致していないコンソールで localhost を承認済みドメインに追加する一般
#8秘密鍵の改行に関する問題環境変数内のエスケープされた \n.replace(/\\n/g, '\n') を使用する一般
#9セッション Cookie が保持されないCookie の設定が正しくない本番環境では secure: true と sameSite: 'lax' を設定する一般
#10OAuth の state の不一致OAuth 中にユーザーがページを離れるauth/popup-closed-by-user を適切に処理する一般
#11レート制限認証の試行回数が多すぎる指数バックオフを実装するドキュメント
#12メールアドレスの列挙登録済みと未登録のメールアドレスでエラーが異なるどちらの場合も同じメッセージを使用するセキュリティのベストプラクティス

セキュリティのベストプラクティス

必ず行うこと
  1. 本番環境では HTTPS を使用する - セキュア Cookie に必須です
  2. サーバー側でトークンを検証する - クライアントのクレームだけを決して信用しないでください
  3. トークンの有効期限切れに対処する - API 呼び出しの前に更新します
  4. レート制限を実装する - ブルートフォース攻撃を防ぎます
  5. メールアドレスまたはパスワードが間違っている場合は同じエラーメッセージを使用する - メールアドレスの列挙を防ぎます
  6. メールアドレスの確認を有効にする - ユーザーがメールアドレスの所有者であることを確認します
  7. SSR ではセッション Cookie を使用する - Cookie 内の ID トークンより安全です
  8. パスワード変更時にトークンを失効させる - 古いセッションを無効にします
決して行わないこと
  1. クライアントコードで秘密鍵を公開しない
  2. クライアントから渡されたクレームを信用しない - 必ずサーバー側で検証してください
  3. ID トークンを localStorage に保存しない - httpOnly Cookie を使用してください
  4. 本番環境でメールアドレス列挙の保護を無効にしない
  5. ドメインの CORS 設定を省略しない

Firebase CLI コマンド

# Initialize Firebase project
firebase init

# Enable auth emulator
firebase emulators:start --only auth

# Export auth users
firebase auth:export users.json --format=json

# Import auth users
firebase auth:import users.json

パッケージのバージョン(2026-01-25 に検証済み)

{
  "dependencies": {
    "firebase": "^12.8.0"
  },
  "devDependencies": {
    "firebase-admin": "^13.6.0"
  }
}

公式ドキュメント


最終検証日: 2026-01-25 | スキルのバージョン: 1.0.0

コメント