Skip to main content

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

  1. Go to Firebase Console
  2. Create a new project
  3. Go to Authentication → Sign-in method
  4. 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:

FeatureFreePaid (Blaze)
Email/password auth✅ Unlimited
Social logins✅ Unlimited
Phone auth10K/month$0.01-0.06/verification
Multi-factor auth✅ (Blaze plan)
Custom domains✅ (Blaze plan)

Common Mistakes

MistakeImpactFix
Not using onAuthStateChangedAuth state lost on refreshAlways listen for auth state changes
Client-side only protectionAPI endpoints unprotectedVerify ID tokens server-side
Not handling error codesGeneric error messagesMap Firebase error codes to user-friendly messages
Storing Firebase config in .env without NEXT_PUBLIC_Config unavailable in browserUse NEXT_PUBLIC_ prefix for client-side config
Not enabling providers in Firebase ConsoleSign-in methods failEnable each auth method in the console

Choosing auth? Compare Firebase Auth vs Clerk vs Auth0 on APIScout — free tier, features, and developer experience.

Comments