Skip to main content

Command Palette

Search for a command to run...

NextJS & Firebase Integration

Updated
5 min read
NextJS & Firebase Integration

Introduction to Next.js with Firebase Authentication

This project demonstrates how to integrate Firebase Authentication with a Next.js application, providing a secure and efficient way to manage user authentication. Firebase offers a robust backend-as-a-service platform, and its authentication module seamlessly handles user sign-ins, sign-outs, and session management.

Key Features:

  • Next.js Middleware: Protect routes by checking user authentication status using cookies.
  • Firebase Authentication: Use Firebase to manage user accounts, handle sign-in and sign-out, and monitor authentication state changes.
  • Global State Management: Implement a React Context (AuthContext) to manage and share authentication state across the application.
  • Environment Variables: Keep sensitive Firebase credentials secure using .env.local.

By following this guide, you'll set up a fully functional authentication system with protected routes and dynamic user session handling, all within the simplicity and performance of Next.js. Whether you're building a small project or a large-scale application, this setup is a solid foundation for adding authentication to your app.


Project Structure

Here's the suggested structure for a project that includes Firebase authentication in Next.js:

project-root/
├── components/
│   ├── AuthProvider.jsx
├── context/
│   ├── AuthContext.jsx
├── lib/
│   ├── firebase.js
├── middleware.ts
├── pages/
│   ├── _app.js
│   ├── index.js
│   ├── login.js
│   ├── dashboard.js
├── styles/
│   ├── globals.css
├── .env.local
├── next.config.js
├── package.json

Detailed Explanation

lib/firebase.js

Your firebase.js file initializes Firebase. The code you've provided is correct and initializes the Firebase app only once. It also sets up the auth and db exports for authentication and Firestore usage.

Here's the lib/firebase.js file that initializes Firebase for your Next.js project:

// lib/firebase.js
import { initializeApp, getApps } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";

// Firebase configuration using environment variables
const clientCredentials = {
  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,
};

// Initialize Firebase only if no apps are already initialized
let firebaseApp;
if (!getApps().length) {
  firebaseApp = initializeApp(clientCredentials);
} else {
  firebaseApp = getApps()[0];
}

// Export Firebase Auth and Firestore instances
export const auth = getAuth(firebaseApp);
export const db = getFirestore(firebaseApp);

Explanation:

  1. Environment Variables:

    • The configuration values (API key, Auth domain, etc.) are stored in .env.local and accessed using process.env.
    • This ensures sensitive information is not exposed in your codebase.
  2. Firebase Initialization:

    • The initializeApp function is called only if no Firebase apps are already initialized. This avoids reinitialization errors, which can occur during development due to hot reloading.
  3. Exported Modules:

    • auth: Provides Firebase Authentication features.
    • db: Connects to Firestore, Firebase's NoSQL database.

This file will serve as the core configuration for Firebase in your application. You can import auth and db from this file whenever you need to use Firebase Authentication or Firestore in your project.

context/AuthContext.jsx

This file will manage the authentication state using React Context.

import { createContext, useContext, useState, useEffect } from "react";
import { onAuthStateChanged } from "firebase/auth";
import { auth } from "../lib/firebase";

const AuthContext = createContext();

export function AuthProvider({ children }) {
  const [user, setUser] = useState(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 function useAuth() {
  return useContext(AuthContext);
}

middleware.ts

Your middleware ensures users cannot access protected pages without logging in.

The code you've provided works perfectly. Just ensure cookies like isLoggedIn are set properly during login.

Here's the middleware.ts file for your Next.js project:

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  // Check if the user is authenticated by looking for the 'isLoggedIn' cookie
  const isLoggedIn = request.cookies.get("isLoggedIn");

  console.log("Authentication status (isLoggedIn):", isLoggedIn);

  // Redirect to /login if the user is not logged in and tries to access protected routes
  if (!isLoggedIn && request.nextUrl.pathname !== "/login") {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  // Proceed to the requested route if the user is authenticated or on the login page
  return NextResponse.next();
}

// Apply middleware to all routes except /login
export const config = {
  matcher: ["/((?!login).*)"], // Matches all routes except /login
};

Explanation:

  1. isLoggedIn Cookie:

    • The middleware checks for a cookie named isLoggedIn, which should be set after a successful login.
  2. Redirection:

    • If the cookie is not present and the user is attempting to access a route other than /login, they are redirected to the /login page.
  3. NextResponse.next():

    • Allows the request to proceed if the user is authenticated or accessing /login.
  4. matcher Configuration:

    • Ensures the middleware runs for all routes except /login.

pages/_app.js

Wrap your application with the AuthProvider.

import { AuthProvider } from "../context/AuthContext";
import "../styles/globals.css";

function MyApp({ Component, pageProps }) {
  return (
    <AuthProvider>
      <Component {...pageProps} />
    </AuthProvider>
  );
}

export default MyApp;

pages/login.js

Create a login page using Firebase Authentication.

import { useState } from "react";
import { signInWithEmailAndPassword } from "firebase/auth";
import { useRouter } from "next/router";
import { auth } from "../lib/firebase";

export default function Login() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");
  const router = useRouter();

  const handleLogin = async (e) => {
    e.preventDefault();
    try {
      await signInWithEmailAndPassword(auth, email, password);
      document.cookie = "isLoggedIn=true"; // Set the cookie
      router.push("/dashboard");
    } catch (err) {
      setError("Invalid credentials");
    }
  };

  return (
    <div>
      <h1>Login</h1>
      {error && <p>{error}</p>}
      <form onSubmit={handleLogin}>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="Email"
          required
        />
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="Password"
          required
        />
        <button type="submit">Login</button>
      </form>
    </div>
  );
}

pages/dashboard.js

A protected page that requires authentication.

import { useAuth } from "../context/AuthContext";
import { useRouter } from "next/router";

export default function Dashboard() {
  const { user, loading } = useAuth();
  const router = useRouter();

  if (loading) return <p>Loading...</p>;

  if (!user) {
    router.push("/login");
    return null;
  }

  return (
    <div>
      <h1>Welcome, {user.email}</h1>
      <button
        onClick={() => {
          auth.signOut();
          document.cookie = "isLoggedIn=; Max-Age=0"; // Clear the cookie
          router.push("/login");
        }}
      >
        Logout
      </button>
    </div>
  );
}

.env.local

Environment variables for Firebase configuration.

NEXT_PUBLIC_FIREBASE_API_KEY=your-api-key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-auth-domain
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project-id
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your-storage-bucket
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your-messaging-sender-id
NEXT_PUBLIC_FIREBASE_APP_ID=your-app-id

Key Features of This Setup

  1. Firebase Authentication: Using AuthContext to manage authentication globally.
  2. Middleware: Protects routes using isLoggedIn cookie.
  3. Environment Variables: Ensures sensitive keys are not hard-coded.
  4. Protected Routes: The dashboard only allows access if the user is authenticated.

This structure provides a secure and scalable authentication system for your Next.js project using Firebase.

More from this blog

B

Blog Nishaanth

17 posts