firebase-auth について
Firebase Authentication を利用する開発者向けの、クライアントとサーバーの実装ガイドです。ログイン方式、トークンとセッション管理、ロールのクレーム、セキュリティ上の注意点を扱います。
主な機能
- 01メール・パスワードと OAuth のログイン例
- 02電話認証と多要素認証のガイド
- 03ID トークン検証とセッション Cookie 管理
- 04カスタムクレームと保護されたルート
- 05一般的なエラー処理とセキュリティの助言
活用シーン
- 01登録、ログイン、パスワード再設定の追加
- 02サーバーでのユーザートークン検証
- 03ロール設定と管理ルートの保護
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill firebase-authインストール後、Claude Code で/firebase-authを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.md.claude-plugin/plugin.jsonrules/firebase-auth.mdtemplates/api-session.tsauth-context.tsxmiddleware.tsprotected-route.tsx詳細
Firebase Authentication
Status:本番環境対応済み Last Updated:2026-01-25 Dependencies:なし(単独のスキル) Latest Versions:[email protected], [email protected]
クイックスタート(5 分)
1. Firebase Console で認証プロバイダーを有効にする
- Firebase Console > Authentication > Sign-in method に移動します
- 使用するプロバイダー(Email/Password、Google など)を有効にします
- 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 トークンにキャッシュされます。クレームを設定した後、ユーザーは次のいずれかを行う必要があります。
- サインアウトして、再度サインインする、または
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 件のエラーを防ぎます。
| 問題 # | エラー/問題 | 説明 | 回避方法 | 出典 |
|---|---|---|---|---|
| #1 | auth/invalid-credential | メールアドレスまたはパスワードが間違っている場合の一般的なエラー | 「メールアドレスまたはパスワードが無効です」という一般的なメッセージを表示する | 一般 |
| #2 | auth/popup-blocked | ブラウザーが OAuth ポップアップをブロックする | リダイレクトへのフォールバックを実装する | ドキュメント |
| #3 | auth/requires-recent-login | 機密性の高い操作には直近のログインが必要 | パスワード変更やアカウント削除の前に再認証する | ドキュメント |
| #4 | カスタムクレームが更新されない | クレームが ID トークンにキャッシュされる | クレーム設定後にトークンを強制的に更新する | ドキュメント |
| #5 | トークンの有効期限切れ | ID トークンは 1 時間後に期限切れになる | API 呼び出しの前に必ず getIdToken() を呼び出す | 一般 |
| #6 | 認証リスナーによるメモリリーク | onAuthStateChanged の購読を解除していない | useEffect 内でクリーンアップ関数を返す | 一般 |
| #7 | localhost での CORS エラー | 認証ドメインが一致していない | コンソールで localhost を承認済みドメインに追加する | 一般 |
| #8 | 秘密鍵の改行に関する問題 | 環境変数内のエスケープされた \n | .replace(/\\n/g, '\n') を使用する | 一般 |
| #9 | セッション Cookie が保持されない | Cookie の設定が正しくない | 本番環境では secure: true と sameSite: 'lax' を設定する | 一般 |
| #10 | OAuth の state の不一致 | OAuth 中にユーザーがページを離れる | auth/popup-closed-by-user を適切に処理する | 一般 |
| #11 | レート制限 | 認証の試行回数が多すぎる | 指数バックオフを実装する | ドキュメント |
| #12 | メールアドレスの列挙 | 登録済みと未登録のメールアドレスでエラーが異なる | どちらの場合も同じメッセージを使用する | セキュリティのベストプラクティス |
セキュリティのベストプラクティス
必ず行うこと
- 本番環境では HTTPS を使用する - セキュア Cookie に必須です
- サーバー側でトークンを検証する - クライアントのクレームだけを決して信用しないでください
- トークンの有効期限切れに対処する - API 呼び出しの前に更新します
- レート制限を実装する - ブルートフォース攻撃を防ぎます
- メールアドレスまたはパスワードが間違っている場合は同じエラーメッセージを使用する - メールアドレスの列挙を防ぎます
- メールアドレスの確認を有効にする - ユーザーがメールアドレスの所有者であることを確認します
- SSR ではセッション Cookie を使用する - Cookie 内の ID トークンより安全です
- パスワード変更時にトークンを失効させる - 古いセッションを無効にします
決して行わないこと
- クライアントコードで秘密鍵を公開しない
- クライアントから渡されたクレームを信用しない - 必ずサーバー側で検証してください
- ID トークンを localStorage に保存しない - httpOnly Cookie を使用してください
- 本番環境でメールアドレス列挙の保護を無効にしない
- ドメインの 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"
}
}
公式ドキュメント
- Firebase Auth の概要: https://firebase.google.com/docs/auth
- Web の開始方法: https://firebase.google.com/docs/auth/web/start
- Admin Auth: https://firebase.google.com/docs/auth/admin
- カスタムクレーム: https://firebase.google.com/docs/auth/admin/custom-claims
- セッション Cookie: https://firebase.google.com/docs/auth/admin/manage-cookies
- セキュリティルール: https://firebase.google.com/docs/rules
最終検証日: 2026-01-25 | スキルのバージョン: 1.0.0
コメント
Markdown 対応