Firebase Authentication With Next.js

Feb 13, 2023
--- views
Firebase Authentication With Next.js - Banner Image

The demo of the project is live at: https://firebase-auth-with-nextjs.netlify.app/

Authentication is an important feature of every web application nowadays. In the past, implementing authentication was a bit difficult where developers have had to write codes to cover security and middlewares for the applicaiton. Thanfully with the modern tech stacks today, implementing authentication in a web project has become easier than before. In particular, Firebase is a great tool for handling authentication and user management.

Firebase has a lot of benefits when it comes to NoSQL databases, and one of them is authenication that we will cover in this tutorial. Firebase can be integrated with different technologies, but in this article, we focus on Next.js which is another fantastic tool for bulding modern web applications.

Let's learn together.

1. Create a Next.js project with TypeScript

To begine with, we need to create a Next.js project with TypeScript. Copy the command below to your terminal or visit the Next.js Docs to learn more.

npx create-next-app@latest --typescript

Next.js will ask you to give your project a name, if you want to enable eslint, and a few other questions. Once all done, create-next-app will create your Next.js project.

Firebase Authentication With Next.js - Banner Image

2. Add Tailwind CSS to your project

Tailwind CSS is an awesome framework for styling your projects. We will use it for styling our forms and the layout. Add it to your project using these commands:

npm install -D tailwindcss postcss autoprefixer

Create a Tailwind config file:

npx tailwindcss init -p

Replace the content section in your Tailwind config file with this code:

content: [
    "./app/**/*.{js,ts,jsx,tsx}",
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",

    // Or if using `src` directory:
    "./src/**/*.{js,ts,jsx,tsx}",
],

3. Create a Firebase project

Now, we need to create a Firebase project. Creating a Firebase project is super simple, just follow the below steps.

Visit Google Firebase Console, make sure you sign in with your Gmail account.

Once you are there, click on the Add project button in your Firebase console to create a new project. Give your project a name, in our case, firebase-auth-nextjs is a good name, and then click continue.

Firebase Authentication With Next.js - Banner Image

Next, Firebase will ask you if you want to enable analytics for your project. If you wish to enable analytics, make sure to turn it on and then click continue.

Firebase Authentication With Next.js - Banner Image

If you enabled Google analytics for your Firebase project, Google will ask you to add an analytics account to your project. If you don't have one, just create a new Google analytics account and connect it to your project. Once eveything is done, click Create Project so that Firebase creates your project.

Firebase Authentication With Next.js - Banner Image

4. Add Firebase to your Next.js project

Visit the dashboard of your newly created Firebase project and click on the configure for web icon in your project as circled in this image:

Firebase Authentication With Next.js - Banner Image

Next, register your app as below. You can give any name to your app.

Firebase Authentication With Next.js - Banner Image

Next, we need to add Firebase SDK to our project by simply running the following command:

npm install firebase

Firebase will give our project unqiue configuration details that we will need when we sign up and sign in the user.

Create a config folder in your project root and then create a firebase.config.js file and past the code from your Firebase project.

import { getApps, initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';

// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
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
};

// Initialize Firebase
if (!getApps().length) {
    initializeApp(firebaseConfig);
}
// Initialize Firebase auth
export const auth = getAuth();

We need to create a .env.local file in our project root and add the Firebase keys and ids and then we read it from our env file. Make sure to include the .env.local file in your .gitignore file so that it's not pushed to Github.

As you can see, we import the getApps to our config file in addition to the initializeApp function. We check for our apps if it's already initialized, if not, we then initialize the app and pass in the firebaseConfig.

At the end of the Firebase authentication file above, we create an auth and export it so that we can simply import it any where we want.

The last step for our Firebase project is to add a sign in provider. Go to your Firebase project dashboard and click on authenication, then click on sign-in-methods and click on Email/Password and then enable the provider.

Firebase Authentication With Next.js - Banner Image

5. Create pages

Replace your code in the index.tsx page with this code:

import Head from 'next/head';
import LoginForm from '@/components/LoginForm';

export default function Home() {
    return (
        <>
            <Head>
                <title>Login - Firebase Authentication With Next.js</title>
                <meta
                    name="description"
                    content="Learn how to implement Google Firebase Authentication in your React, Next.js, TypeScript projects."
                />
                <meta
                    name="viewport"
                    content="width=device-width, initial-scale=1"
                />
                <link rel="icon" href="/favicon.ico" />
            </Head>
            <main className="m-0 bg-gradient-to-br from-primary-color to-blue-400 px-4">
                <LoginForm />
            </main>
        </>
    );
}

We are just imnporting the LoginForm component that we will create in a minute.

Then, create the register.tsx file and past the this code:

import RegistrationForm from '@/components/RegistrationForm';
import Head from 'next/head';

const register = () => {
    return (
        <>
            <Head>
                <title>Register - Firebase Authentication With Next.js</title>
                <meta
                    name="description"
                    content="Learn how to implement Google Firebase Authentication in your React, Next.js, TypeScript projects."
                />
                <meta
                    name="viewport"
                    content="width=device-width, initial-scale=1"
                />
                <link rel="icon" href="/favicon.ico" />
            </Head>

            <main className="m-0 min-h-screen bg-gradient-to-br from-primary-color to-blue-400 px-4">
                <RegistrationForm />
            </main>
        </>
    );
};

export default register;

Last but not least, we need to create a dashboard.tsx file where we will redirect the users once they are signed in or signed up.

import ProtectedRoute from '@/components/ProtectedRoute';
import { useAuth } from '@/context/AuthContext';
import Head from 'next/head';
import { useRouter } from 'next/router';

const Dashboard = () => {
    const { user, logOut } = useAuth();
    const router = useRouter();

    return (
        <ProtectedRoute>
            <Head>
                <title>Dashboard - Firebase Authentication With Next.js</title>
                <meta
                    name="description"
                    content="Learn how to implement Google Firebase Authentication in your React, Next.js, TypeScript projects."
                />
                <meta
                    name="viewport"
                    content="width=device-width, initial-scale=1"
                />
                <link rel="icon" href="/favicon.ico" />
            </Head>

            <div className="container mx-auto flex min-h-screen items-center py-2">
                <div className="mx-auto mt-24 overflow-y-hidden px-12 py-24 text-gray-600">
                    <h2 className="mb-4 text-2xl font-semibold">
                        You are logged in!
                    </h2>

                    <div className="mb-8 flex items-center justify-center">
                        <button
                            onClick={() => {
                                logOut();
                                router.push('/');
                            }}
                            className="rounded-md bg-green-600 px-10 py-3 text-white shadow-sm hover:bg-green-700"
                        >
                            Logout
                        </button>
                    </div>
                </div>
            </div>
        </ProtectedRoute>
    );
};

export default Dashboard;

Don't worry about the useAuth context, we are creating that in the next step.

6. Create AuthContext and AuthContextProvider

After we have created our pages, now we need to create the AuthContext where the actual logic of the firebase authentication goes in.

Create a context folder in the root folder of your project and then create an AuthContext.tsx file and then past this code in your AuthContext.tsx file:

import React, { createContext, useContext, useEffect, useState } from 'react';
import {
    createUserWithEmailAndPassword,
    onAuthStateChanged,
    signInWithEmailAndPassword,
    signOut
} from 'firebase/auth';
import { auth } from '../config/firebase.config';

// User data type interface
interface UserType {
    email: string | null;
    uid: string | null;
}

// Create auth context
const AuthContext = createContext({});

// Make auth context available across the app by exporting it
export const useAuth = () => useContext<any>(AuthContext);

// Create the auth context provider
export const AuthContextProvider = ({
    children
}: {
    children: React.ReactNode;
}) => {
    // Define the constants for the user and loading state
    const [user, setUser] = useState<UserType>({ email: null, uid: null });
    const [loading, setLoading] = useState<Boolean>(true);

    // Update the state depending on auth
    useEffect(() => {
        const unsubscribe = onAuthStateChanged(auth, (user) => {
            if (user) {
                setUser({
                    email: user.email,
                    uid: user.uid
                });
            } else {
                setUser({ email: null, uid: null });
            }
        });

        setLoading(false);

        return () => unsubscribe();
    }, []);

    // Sign up the user
    const signUp = (email: string, password: string) => {
        return createUserWithEmailAndPassword(auth, email, password);
    };

    // Login the user
    const logIn = (email: string, password: string) => {
        return signInWithEmailAndPassword(auth, email, password);
    };

    // Logout the user
    const logOut = async () => {
        setUser({ email: null, uid: null });
        return await signOut(auth);
    };

    // Wrap the children with the context provider
    return (
        <AuthContext.Provider value={{ user, signUp, logIn, logOut }}>
            {loading ? null : children}
        </AuthContext.Provider>
    );
};

The AuthContext contains a few functions and useEffect that are explained below:

  1. First we import all necessary functions from firebase/auth
  2. Then, we import the auth from firebase.config.js (You remeber we have created and exported that in the firebase.config.js)
  3. Next, we create an interface for the user
  4. Then, we create the AuthContext and passing it to the useAuth
  5. After that, we create the AuthContextProvider where we define a few states for the user and a useEffect that updates the state depending on the auth
  6. Then we create separate functions for signUp, logIn and logut
  7. At the end, we return the AuthContext.Provider and we pass in all the values

Next we need to wrap our _app.tsx file in the pages folder with the AuthContextProvider:

import { AuthContextProvider } from '@/context/AuthContext';
import '@/styles/globals.css';
import type { AppProps } from 'next/app';

export default function App({ Component, pageProps }: AppProps) {
    return (
        <AuthContextProvider>
            <Component {...pageProps} />
        </AuthContextProvider>
    );
}

7. Create the components

If you don't see the components folder in your project, simply create a components folder at the root of your project.

Create a ProtectedRoute.tsx file and past this code in your ProtectedRoute.tsx file:

import { useAuth } from '@/context/AuthContext';
import { useRouter } from 'next/router';
import React, { useEffect } from 'react';

const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
    const router = useRouter();
    const { user } = useAuth();

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

    return <div>{user ? children : null}</div>;
};

export default ProtectedRoute;

The ProtectedRoute helps us protect our routes like the Dashboard from unauthenticated users.

Next, create a LoginForm.tsx file and past the code below in there:

import NextLink from 'next/link';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { FiChevronRight } from 'react-icons/fi';
import { useAuth } from '@/context/AuthContext';
import { LoginType } from '@/types/AuthTypes';

const LoginForm = () => {
    const [data, setData] = useState<LoginType>({
        email: '',
        password: ''
    });

    // Use the signIn method from the AuthContext
    const { logIn } = useAuth();
    const router = useRouter();

    const handleLogin = async (e: any) => {
        e.preventDefault();
        try {
            await logIn(data.email, data.password);
            router.push('/dashboard');
        } catch (error: any) {
            console.log(error.message);
        }
    };

    // Destructure data from the data object
    const { ...allData } = data;

    // Disable submit button until all fields are filled in
    const canSubmit = [...Object.values(allData)].every(Boolean);

    return (
        <div className="flex items-center justify-center">
            <div className="w-full max-w-sm rounded-lg border border-gray-200 bg-white p-4 py-8 shadow-md dark:border-gray-700 dark:bg-gray-800 sm:p-6 sm:py-10 md:p-8 md:py-14">
                <form action="" onSubmit={handleLogin} className="group">
                    <h5 className="mb-2 text-center text-2xl font-medium text-gray-900 dark:text-white sm:text-3xl sm:font-semibold">
                        Login
                    </h5>
                    <p className="text-md mb-8 text-center text-gray-500 dark:text-gray-200">
                        Please enter your login credentials to login to the
                        dashboard.
                    </p>
                    <div className="mb-5">
                        <label
                            htmlFor="email"
                            className="mb-2 block text-sm font-medium text-gray-900 dark:text-white"
                        >
                            Your email
                        </label>
                        <input
                            type="email"
                            name="email"
                            id="email"
                            className="valid:[&:not(:placeholder-shown)]:border-green-500 [&:not(:placeholder-shown):not(:focus):invalid~span]:block invalid:[&:not(:placeholder-shown):not(:focus)]:border-red-400 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 placeholder-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-500 dark:bg-gray-600 dark:text-white dark:placeholder-gray-400"
                            autoComplete="off"
                            required
                            pattern="[a-z0-9._+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
                            placeholder="name@company.com"
                            onChange={(e: any) => {
                                setData({
                                    ...data,
                                    email: e.target.value
                                });
                            }}
                        />
                        <span className="mt-1 hidden text-sm text-red-400">
                            Please enter a valid email address.{' '}
                        </span>
                    </div>
                    <div className="mb-5">
                        <label
                            htmlFor="password"
                            className="mb-2 block text-sm font-medium text-gray-900 dark:text-white"
                        >
                            Your password
                        </label>
                        <input
                            type="password"
                            name="password"
                            id="password"
                            placeholder="••••••••"
                            className="valid:[&:not(:placeholder-shown)]:border-green-500 [&:not(:placeholder-shown):not(:focus):invalid~span]:block invalid:[&:not(:placeholder-shown):not(:focus)]:border-red-400 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 placeholder-gray-300 focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-500 dark:bg-gray-600 dark:text-white dark:placeholder-gray-400"
                            pattern=".{8,}"
                            required
                            onChange={(e: any) => {
                                setData({
                                    ...data,
                                    password: e.target.value
                                });
                            }}
                        />
                        <span className="mt-1 hidden text-sm text-red-400">
                            Password must be at least 8 characters.{' '}
                        </span>
                    </div>

                    <button
                        type="submit"
                        disabled={!canSubmit}
                        className="mb-8 mt-2 w-full rounded-lg bg-green-600 px-5 py-3 text-center text-sm font-medium text-white hover:bg-green-700 focus:outline-none focus:ring-4 focus:ring-blue-300 disabled:cursor-not-allowed disabled:bg-gradient-to-br disabled:from-gray-100 disabled:to-gray-300 disabled:text-gray-400 group-invalid:pointer-events-none group-invalid:bg-gradient-to-br group-invalid:from-gray-100 group-invalid:to-gray-300 group-invalid:text-gray-400 group-invalid:opacity-70"
                    >
                        Login to your account
                    </button>

                    <div className="text-md flex items-center justify-center text-center font-medium text-gray-500 dark:text-gray-300">
                        <NextLink
                            href="/register"
                            className="flex w-20 items-center justify-between text-gray-500 hover:text-gray-800 hover:underline dark:text-gray-200 dark:hover:text-white"
                        >
                            Register <FiChevronRight className="text-lg" />
                        </NextLink>
                    </div>
                </form>
            </div>
        </div>
    );
};

export default LoginForm;

Finally, we need to create a RegisterForm.tsx file and past in the below code:

import { useAuth } from '@/context/AuthContext';
import { RegistrationType } from '@/types/AuthTypes';
import NextLink from 'next/link';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { FiChevronLeft } from 'react-icons/fi';

const RegistrationForm = () => {
    const [data, setData] = useState<RegistrationType>({
        email: '',
        password: ''
    });

    // Use the signUp method from the AuthContext
    const { signUp } = useAuth();
    const router = useRouter();

    const handleRegistration = async (e: any) => {
        e.preventDefault();
        try {
            await signUp(data.email, data.password);
            router.push('/dashboard');
        } catch (error: any) {
            console.log(error.message);
        }
        console.log(data);
    };

    // Destructure data from the data object
    const { ...allData } = data;

    // Disable submit button until all fields are filled in
    const canSubmit = [...Object.values(allData)].every(Boolean);

    return (
        <div className="flex items-center justify-center">
            <div className="w-full max-w-sm rounded-lg border border-gray-200 bg-white p-4 py-8 shadow-md dark:border-gray-700 dark:bg-gray-800 sm:p-6 sm:py-10 md:p-8 md:py-14">
                <form action="" onSubmit={handleRegistration} className="group">
                    <h5 className="mb-2 text-center text-2xl font-medium text-gray-900 dark:text-white sm:text-3xl sm:font-semibold">
                        Register
                    </h5>
                    <p className="text-md mb-8 text-center">
                        Fill in the form below to create a new account
                    </p>
                    <div className="mb-5">
                        <label
                            htmlFor="email"
                            className="mb-2 block text-sm font-medium text-gray-900 dark:text-white"
                        >
                            Your email
                        </label>
                        <input
                            type="email"
                            name="email"
                            id="email"
                            className="valid:[&:not(:placeholder-shown)]:border-green-500 [&:not(:placeholder-shown):not(:focus):invalid~span]:block invalid:[&:not(:placeholder-shown):not(:focus)]:border-red-400 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 placeholder-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-500 dark:bg-gray-600 dark:text-white dark:placeholder-gray-400"
                            autoComplete="off"
                            required
                            pattern="[a-z0-9._+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
                            placeholder="name@company.com"
                            onChange={(e: any) => {
                                setData({
                                    ...data,
                                    email: e.target.value
                                });
                            }}
                        />
                        <span className="mt-1 hidden text-sm text-red-400">
                            Please enter a valid email address.{' '}
                        </span>
                    </div>
                    <div className="mb-5">
                        <label
                            htmlFor="password"
                            className="mb-2 block text-sm font-medium text-gray-900 dark:text-white"
                        >
                            Your password
                        </label>
                        <input
                            type="password"
                            name="password"
                            id="password"
                            placeholder="••••••••"
                            className="valid:[&:not(:placeholder-shown)]:border-green-500 [&:not(:placeholder-shown):not(:focus):invalid~span]:block invalid:[&:not(:placeholder-shown):not(:focus)]:border-red-400 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 placeholder-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-500 dark:bg-gray-600 dark:text-white dark:placeholder-gray-400"
                            pattern=".{8,}"
                            required
                            onChange={(e: any) => {
                                setData({
                                    ...data,
                                    password: e.target.value
                                });
                            }}
                        />
                        <span className="mt-1 hidden text-sm text-red-400">
                            Password must be at least 8 characters.{' '}
                        </span>
                    </div>

                    <button
                        type="submit"
                        disabled={!canSubmit}
                        className="mb-8 mt-2 w-full rounded-lg bg-green-600 px-5 py-3 text-center text-sm font-medium text-white hover:bg-green-700 focus:outline-none focus:ring-4 focus:ring-blue-300 disabled:cursor-not-allowed disabled:bg-gradient-to-br disabled:from-gray-100 disabled:to-gray-300 disabled:text-gray-400 group-invalid:pointer-events-none group-invalid:bg-gradient-to-br group-invalid:from-gray-100 group-invalid:to-gray-300 group-invalid:text-gray-400 group-invalid:opacity-70"
                    >
                        Create account
                    </button>
                    <div className="text-md flex items-center justify-center text-center font-medium text-gray-500 dark:text-gray-300">
                        <NextLink
                            href="/"
                            className="flex w-32 items-center justify-between text-gray-500 hover:text-gray-800 hover:underline dark:text-gray-200 dark:hover:text-white"
                        >
                            <FiChevronLeft className="text-xl" /> Login Instead
                        </NextLink>
                    </div>
                </form>
            </div>
        </div>
    );
};

export default RegistrationForm;

The source code is available in my Github account at https://github.com/realstoman/nextjs-firebase-auth

Conclusion

I hope this tutorial helps you with implemeting the Firebase authentication in the JavaScript ecosystem. If you found this helpful, give the Github repository a star and share it with your friends so they can benefit from it as well.

Thank you!