How to Add Firebase Auth to a React App
·APIScout Team
firebaseauthenticationreacttutorialapi integration
How to Add Firebase Auth to a React App
Firebase Authentication integrates with the Google ecosystem and offers a generous free tier (unlimited users). This guide covers email/password auth, Google sign-in, auth state management, and route protection.
What You'll Build
- Email/password sign-up and sign-in
- Google sign-in (one-click)
- Auth state listener (persistent login)
- Protected routes
- Password reset flow
Prerequisites: React 18+, Firebase project (free tier: unlimited auth users).
1. Setup
Create Firebase Project
- Go to Firebase Console
- Create a new project
- Go to Authentication → Sign-in method
- Enable: Email/Password, Google
Install
npm install firebase
Initialize 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,
};
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
2. Auth Context
// contexts/AuthContext.tsx
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
import { User, onAuthStateChanged } 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: 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 useAuth = () => useContext(AuthContext);
3. Sign Up
// components/SignUpForm.tsx
'use client';
import { useState } from 'react';
import { createUserWithEmailAndPassword, updateProfile } from 'firebase/auth';
import { auth } from '@/lib/firebase';
export function SignUpForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
try {
const { user } = await createUserWithEmailAndPassword(auth, email, password);
await updateProfile(user, { displayName: name });
} catch (err: any) {
switch (err.code) {
case 'auth/email-already-in-use':
setError('Email already registered');
break;
case 'auth/weak-password':
setError('Password must be at least 6 characters');
break;
default:
setError('Sign up failed. Please try again.');
}
}
};
return (
<form onSubmit={handleSignUp}>
<input value={name} onChange={e => setName(e.target.value)} placeholder="Name" required />
<input value={email} onChange={e => setEmail(e.target.value)} placeholder="Email" type="email" required />
<input value={password} onChange={e => setPassword(e.target.value)} placeholder="Password" type="password" required />
{error && <p className="text-red-500">{error}</p>}
<button type="submit">Sign Up</button>
</form>
);
}
4. Sign In
// components/SignInForm.tsx
'use client';
import { useState } from 'react';
import { signInWithEmailAndPassword, signInWithPopup, GoogleAuthProvider } from 'firebase/auth';
import { auth } from '@/lib/firebase';
const googleProvider = new GoogleAuthProvider();
export function SignInForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const handleEmailSignIn = async (e: React.FormEvent) => {
e.preventDefault();
try {
await signInWithEmailAndPassword(auth, email, password);
} catch (err: any) {
setError('Invalid email or password');
}
};
const handleGoogleSignIn = async () => {
try {
await signInWithPopup(auth, googleProvider);
} catch (err: any) {
setError('Google sign-in failed');
}
};
return (
<div>
<form onSubmit={handleEmailSignIn}>
<input value={email} onChange={e => setEmail(e.target.value)} placeholder="Email" type="email" required />
<input value={password} onChange={e => setPassword(e.target.value)} placeholder="Password" type="password" required />
{error && <p className="text-red-500">{error}</p>}
<button type="submit">Sign In</button>
</form>
<div className="divider">or</div>
<button onClick={handleGoogleSignIn}>
Sign in with Google
</button>
</div>
);
}
5. Sign Out
import { signOut } from 'firebase/auth';
import { auth } from '@/lib/firebase';
export function SignOutButton() {
return (
<button onClick={() => signOut(auth)}>
Sign Out
</button>
);
}
6. Protected Routes
// components/ProtectedRoute.tsx
'use client';
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}</>;
}
// Usage
export default function DashboardPage() {
return (
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
);
}
7. Password Reset
import { sendPasswordResetEmail } from 'firebase/auth';
import { auth } from '@/lib/firebase';
export function ForgotPasswordForm() {
const [email, setEmail] = useState('');
const [sent, setSent] = useState(false);
const handleReset = async (e: React.FormEvent) => {
e.preventDefault();
await sendPasswordResetEmail(auth, email);
setSent(true);
};
if (sent) return <p>Check your email for reset instructions.</p>;
return (
<form onSubmit={handleReset}>
<input value={email} onChange={e => setEmail(e.target.value)} placeholder="Email" type="email" required />
<button type="submit">Send Reset Email</button>
</form>
);
}
8. Server-Side Verification
For API routes, verify Firebase ID tokens:
// lib/firebase-admin.ts
import { initializeApp, cert } from 'firebase-admin/app';
import { getAuth } from 'firebase-admin/auth';
const app = initializeApp({
credential: cert(JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT!)),
});
export const adminAuth = getAuth(app);
// app/api/protected/route.ts
import { adminAuth } from '@/lib/firebase-admin';
export async function GET(req: Request) {
const token = req.headers.get('authorization')?.replace('Bearer ', '');
if (!token) {
return Response.json({ error: 'Missing token' }, { status: 401 });
}
try {
const decoded = await adminAuth.verifyIdToken(token);
return Response.json({ uid: decoded.uid, email: decoded.email });
} catch {
return Response.json({ error: 'Invalid token' }, { status: 401 });
}
}
Pricing
Firebase Auth is free for unlimited users. You only pay for:
| Feature | Free | Paid (Blaze) |
|---|---|---|
| Email/password auth | ✅ Unlimited | ✅ |
| Social logins | ✅ Unlimited | ✅ |
| Phone auth | 10K/month | $0.01-0.06/verification |
| Multi-factor auth | ❌ | ✅ (Blaze plan) |
| Custom domains | ❌ | ✅ (Blaze plan) |
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
Not using onAuthStateChanged | Auth state lost on refresh | Always listen for auth state changes |
| Client-side only protection | API endpoints unprotected | Verify ID tokens server-side |
| Not handling error codes | Generic error messages | Map Firebase error codes to user-friendly messages |
Storing Firebase config in .env without NEXT_PUBLIC_ | Config unavailable in browser | Use NEXT_PUBLIC_ prefix for client-side config |
| Not enabling providers in Firebase Console | Sign-in methods fail | Enable each auth method in the console |
Choosing auth? Compare Firebase Auth vs Clerk vs Auth0 on APIScout — free tier, features, and developer experience.