Marie Starck Marie Starck is a fullstack software developer. Her specialty is JavaScript frameworks. In a perfect world, she would work for chocolate. Find her on Twitter @MStarckJS.

Implementing authentication in Next.js with Firebase

7 min read 2137

Implementing authentication in Next.js with Firebase

Authentication is crucial in web applications today. It’s a feature that many developers have had to implement in the past. Thankfully, many libraries have made this job easier by offering many built-in functionalities. In particular, Firebase is an excellent tool for handling user management and authentication.

In this tutorial, we will cover how to implement authentication using Firebase.

What is Firebase?

Acquired by Google in 2014, Firebase is a platform offering a suite of products including but not limited to:

  • Realtime Database
  • Authentication
  • Cloud Messaging

These products allow developers to create and run applications easily and quickly.

Creating a Firebase account

Before you write a line of code, you will need a Firebase account. Head here to create one.

In Firebase, if you want API keys, you will need to create apps. These apps have to belong to projects. As a result, if you don’t already have a project set up, you need to create one. Once you do, create an app to get your keys.

Creating a new app for our project in Firebase
iOS, Android, and web are some of the formats available.

Now, click on the settings icon right beside Project Overview (in the top left part of your screen). In Project Settings and under General, you should see your app with its config.

Before you head to your code, you will need to enable the sign-in methods you want to use. To do so, click on Authentication and then Sign in methods. Each one has different configurations, but for the sake of this tutorial, I will be focusing on the traditional email/password method.

Email/password sign in method enabled
Enable your sign-in providers.

Adding local environments to Next.js

Now that you have your keys, it’s time to add them to your Next.js project.

Tip: If you don’t have one already created, fret not. This command will get you going:

npx create-next-app
# or
yarn create next-app

A Next.js project automatically ignores .env.local thanks to its .gitignore file, so you can copy/paste your keys and not worry that they will be committed to GitHub accidentally.

NEXT_PUBLIC_FIREBASE_PUBLIC_API_KEY=<YOUR_API_KEY>
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=<YOUR_DOMAIN>
NEXT_PUBLIC_FIREBASE_PROJECT_ID=<YOUR_PROJECT_ID>

Don’t forget: In Next.js, the convention for naming environment variables is that they have to start with NEXT_PUBLIC.

Installing Firebase

Go ahead and install the Firebase library.

npm install --save Firebase
# or
yarn add Firebase

Creating a Firebase instance in Next.js

Great! The library is installed and your API keys are set up. Time to use those keys to create a Firebase instance. Of course, Firebase comes with many useful tools, but for the sake of this article, we will only focus on authentication. As a result, you will only need Firebase/auth and the apiKey, authDomain, and projectId credentials.



import Firebase from 'Firebase/app';
import 'Firebase/auth';

const FirebaseCredentials = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_PUBLIC_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID
}
// if a Firebase instance doesn't exist, create one
if (!Firebase.apps.length) {
  Firebase.initializeApp(FirebaseCredentials)
}

export default Firebase;

Listening for Firebase changes

The first thing you will need is an authUser that you can access throughout your app. This variable is not only helpful for user management but also for redirecting routes accordingly.

For example, if authUser is null, meaning the user hasn’t logged in, when that person tries to access a protected route (say, a dashboard), you should redirect them to the login page.

Thankfully, Firebase.auth keeps track of the state and comes with a built-in function called onAuthStateChanged that allows you to listen for state changes.

When the state changes, format the user depending on your needs, and, finally, set it to your authUser variable. Use the loading variable to indicate whether Firebase is fetching data or not.

import { useState, useEffect } from 'react'
import Firebase from './Firebase';

const formatAuthUser = (user) => ({
  uid: user.uid,
  email: user.email
});

export default function useFirebaseAuth() {
  const [authUser, setAuthUser] = useState(null);
  const [loading, setLoading] = useState(true);

  const authStateChanged = async (authState) => {
    if (!authState) {
      setAuthUser(null)
      setLoading(false)
      return;
    }

    setLoading(true)
    var formattedUser = formatAuthUser(authState);
    setAuthUser(formattedUser);    
    setLoading(false);
  };

// listen for Firebase state change
  useEffect(() => {
    const unsubscribe = Firebase.auth().onAuthStateChanged(authStateChanged);
    return () => unsubscribe();
  }, []);

  return {
    authUser,
    loading
  };
}

Creating a user context

To access authUser and loading variables throughout your app, you will be using the Context API.

Tip: Unfamiliar with React Context? Don’t hesitate to check out the official docs.

First, create your context object with createContext with a default value (authUser as null and loading as true). Then, get the actual authUser and loading variables from useFirebaseAuth and pass it to the provider component.

You should also add a custom hook, in this case, useAuth, to access the current context value.


More great articles from LogRocket:


import { createContext, useContext, Context } from 'react'
import useFirebaseAuth from '../lib/useFirebaseAuth';

const authUserContext = createContext({
  authUser: null,
  loading: true
});

export function AuthUserProvider({ children }) {
  const auth = useFirebaseAuth();
  return <authUserContext.Provider value={auth}>{children}</authUserContext.Provider>;
}
// custom hook to use the authUserContext and access authUser and loading
export const useAuth = () => useContext(authUserContext);

Then, in our _app.js, wrap this provider around your application. This ensures that the children components will be able to access your user context.

import { AuthUserProvider } from '../context/AuthUserContext';

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

export default MyApp

Creating protected routes

Protected routes are pages or sections of your app that should only be accessed by certain users. In this case, only logged-in users should access this content. To set this up, get the authUser and loading from your custom useAuth() hook.

With these variables in place, check if Firebase is still fetching data (i.e., loading is true), and, if not, whether authUser is null. If that is the case, then the user isn’t logged in and you should redirect them to the login page.

Test it in your app and make sure the redirection is happening correctly.

import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useAuth } from '../context/AuthUserContext';

import {Container, Row, Col} from 'reactstrap';

const LoggedIn = () => {
  const { authUser, loading } = useAuth();
  const router = useRouter();

  // Listen for changes on loading and authUser, redirect if needed
  useEffect(() => {
    if (!loading && !authUser)
      router.push('/')
  }, [authUser, loading])

  return (
    //Your logged in page
  )
}

export default LoggedIn;

Adding login, sign-up, and sign-out functionalities in Next.js

Now, let’s move on to the juicy bit. One great thing about Firebase is that it comes with many built-in functions for signing in, creating users, and signing out.

So, let’s add them to the useFirebaseAuth function. Use Firebase.auth() to access the different functions (signInWithEmailAndPassword, createUserWithEmailAndPassword, and signOut):

export default function useFirebaseAuth() {
  // ...
  const clear = () => {
    setAuthUser(null);
    setLoading(true);
  };

  const signInWithEmailAndPassword = (email, password) =>
    Firebase.auth().signInWithEmailAndPassword(email, password);

  const createUserWithEmailAndPassword = (email, password) =>
    Firebase.auth().createUserWithEmailAndPassword(email, password);

  const signOut = () =>
    Firebase.auth().signOut().then(clear);

  useEffect(() => {
    const unsubscribe = Firebase.auth().onAuthStateChanged(authStateChanged);
    return () => unsubscribe();
  }, []);

  return {
    authUser,
    loading,
    signInWithEmailAndPassword,
    createUserWithEmailAndPassword,
    signOut
  };
}

Don’t forget to update your default value in your context file.

const authUserContext = createContext({
  authUser: null,
  loading: true,
  signInWithEmailAndPassword: async () => {},
  createUserWithEmailAndPassword: async () => {},
  signOut: async () => {}
});

export function AuthUserProvider({ children }) {
  const auth = useFirebaseAuth();
  return <authUserContext.Provider value={auth}>{children}</authUserContext.Provider>;
}

Creating the sign-up page

In your sign-up page, use your useAuth hook to retrieve your function for creating a user once again. createUserWithEmailAndPassword takes two parameters: email and password.

After finishing form validation, call this function. If it returns successfully with an authUser, then you can redirect the user accordingly.

import { useState } from 'react';
import { useRouter } from 'next/router';

import { useAuth } from '../context/AuthUserContext';

import {Container, Row, Col, Button, Form, FormGroup, Label, Input, Alert} from 'reactstrap';

const SignUp = () => {
  const [email, setEmail] = useState("");
  const [passwordOne, setPasswordOne] = useState("");
  const [passwordTwo, setPasswordTwo] = useState("");
  const router = useRouter();
  const [error, setError] = useState(null);

  const { createUserWithEmailAndPassword } = useAuth();

  const onSubmit = event => {
    setError(null)
    //check if passwords match. If they do, create user in Firebase
    // and redirect to your logged in page.
    if(passwordOne === passwordTwo)
      createUserWithEmailAndPassword(email, passwordOne)
      .then(authUser => {
        console.log("Success. The user is created in Firebase")
        router.push("/logged_in");
      })
      .catch(error => {
        // An error occurred. Set error message to be displayed to user
        setError(error.message)
      });
    else
      setError("Password do not match")
    event.preventDefault();
  };

  return (
    <Container className="text-center custom-container">
      <Row>
        <Col>
          <Form 
            className="custom-form"
            onSubmit={onSubmit}>
          { error && <Alert color="danger">{error}</Alert>}
            <FormGroup row>
              <Label for="signUpEmail" sm={4}>Email</Label>
              <Col sm={8}>
                <Input
                  type="email"
                  value={email}
                  onChange={(event) => setEmail(event.target.value)}
                  name="email"
                  id="signUpEmail"
                  placeholder="Email" />
              </Col>
            </FormGroup>
            <FormGroup row>
              <Label for="signUpPassword" sm={4}>Password</Label>
              <Col sm={8}>
                <Input
                  type="password"
                  name="passwordOne"
                  value={passwordOne}
                  onChange={(event) => setPasswordOne(event.target.value)}
                  id="signUpPassword"
                  placeholder="Password" />
              </Col>
            </FormGroup>
            <FormGroup row>
              <Label for="signUpPassword2" sm={4}>Confirm Password</Label>
              <Col sm={8}>
                <Input
                  type="password"
                  name="password"
                  value={passwordTwo}
                  onChange={(event) => setPasswordTwo(event.target.value)}
                  id="signUpPassword2"
                  placeholder="Password" />
              </Col>
            </FormGroup>
            <FormGroup row>
             <Col>
               <Button>Sign Up</Button>
             </Col>
           </FormGroup>
          </Form>
        </Col>
      </Row>
    </Container>
  )
}

export default SignUp;

Adding a sign-out button

Signing out is also very straightforward. Grab the signOut() function from useAuth() and add it to a button or a link.

import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useAuth } from '../context/AuthUserContext';

import {Container, Row, Col, Button} from 'reactstrap';

const LoggedIn = () => {
  const { authUser, loading, signOut } = useAuth();
  const router = useRouter();

  // Listen for changes on loading and authUser, redirect if needed
  useEffect(() => {
    if (!loading && !authUser)
      router.push('/')
  }, [authUser, loading])

  return (
    <Container>
      // ...
      <Button onClick={signOut}>Sign out</Button>
      // ...
    </Container>
  )
}

export default LoggedIn;

Creating a login page

And finally, the login functionality! It’s exactly the same as the previous two. Retrieve signInWithEmailAndPassword() from useAuth() and pass in the user’s email and password. If they are correct, redirect the user, and, if not, display the correct error message.

import { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';

import { useAuth } from '../context/AuthUserContext';

import {Container, Row, Col, Button, Form, FormGroup, Label, Input, Alert} from 'reactstrap';

export default function Home() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState(null);
  const router = useRouter();
  const { signInWithEmailAndPassword } = useAuth();

  const onSubmit = event => {
    setError(null)
    signInWithEmailAndPassword(email, password)
    .then(authUser => {
      router.push('/logged_in');
    })
    .catch(error => {
      setError(error.message)
    });
    event.preventDefault();
  };

  return (
    <Container className="text-center" style={{ padding: '40px 0px'}}>
      <Row>
        <Col>
          <h2>Login</h2>
        </Col>
      </Row>
      <Row style={{maxWidth: '400px', margin: 'auto'}}>
        <Col>
          <Form onSubmit={onSubmit}>
          { error && <Alert color="danger">{error}</Alert>}
            <FormGroup row>
              <Label for="loginEmail" sm={4}>Email</Label>
              <Col sm={8}>
                <Input
                  type="email"
                  value={email}
                  onChange={(event) => setEmail(event.target.value)}
                  name="email"
                  id="loginEmail"
                  placeholder="Email" />
              </Col>
            </FormGroup>
            <FormGroup row>
              <Label for="loginPassword" sm={4}>Password</Label>
              <Col sm={8}>
                <Input
                  type="password"
                  name="password"
                  value={password}
                  onChange={(event) => setPassword(event.target.value)}
                  id="loginPassword"
                  placeholder="Password" />
              </Col>
            </FormGroup>
            <FormGroup row>
             <Col>
               <Button>Login</Button>
             </Col>
           </FormGroup>
           <FormGroup row>
            <Col>
              No account? <Link href="/sign_up">Create one</Link>
            </Col>
          </FormGroup>
          </Form>
        </Col>
      </Row>
    </Container>
  )
}

Conclusion

In this tutorial, we covered how to create a Firebase account, project, and app. Then, we learned how to use React Context to create a user context. To this context, we added user and loading variables along with logging, signing up, and signing out functions. Finally, we used those functions to implement authentication in our Next.js app thanks to Firebase!

The complete code for this tutorial is available on GitHub.

LogRocket: Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next.js app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — .

Marie Starck Marie Starck is a fullstack software developer. Her specialty is JavaScript frameworks. In a perfect world, she would work for chocolate. Find her on Twitter @MStarckJS.

26 Replies to “Implementing authentication in Next.js with Firebase”

  1. I think this blog will be more helpful if you mention the files and directories in which we have to copy + paste/ add these code snippets 🙂

      1. Hey Omib! In one of the previous comments, from back in June 2021, I added a comment and linked a git repository. Joel also added a comment on how to handle the newer version (aka 9) which should be very helpful.

    1. I had been trying to set up firebase. I had no idea that it was the latest version, Version 9. I could not for the life of me figure out why I was running into so many bugs. I had used the same set up numerous times.

      Finally, I dove deeper into the docs.

      For version 9 plus

      import firebase from ‘firebase/compat/app’;
      import ‘firebase/compat/auth’;
      import ‘firebase/compat/firestore’;

      Then it worked like a charm

      Just don’t want to see anyone else burn a day banging their head against the wall wondering why it was running into so many bugs.

  2. Thanks for this post but i have a question: if in situation the app has multiple page which is need to auth. Will you duplicate code of useEffect? 😀

    1. Unfortunately, Next.js doesn’t have an elegant way to handle auth and non-auth routes (or at least not one that I know of). The best way would be to create an isAuthenticated hook that does the listening on rendering. It’s not ideal but it’s all I got.

  3. Hi Marie,

    I must say that your blog is really helpful. I found it helpful in context of a regular react project.
    I am now learning NEXT JS and was reading the pre-rendering documentation, getStaticProps and getStaticPages. This made me wonder how pages will be pre-rendered if we have some sections to render that depend on authentication.
    Can you also add an ‘advanced’ section to your blog that covers authentication and pre-rendering.
    Thanks.

    1. An interesting point! I hadn’t considered that approach, but I will definitely add it to my list of topics. Thank you for your suggestion

  4. This is an excellent post. An important note though that it’s based on version 8.3.3 of the Firebase libraries. Lots of breaking changes in 9.x with respect to how to locate and call the auth functions.

    npm uninstall firebase
    npm install [email protected] —save

    Thanks for such a well written and informative piece!

    1. Please refer to my answer to Nguyen back in July 7th (aka third posts above you). He asked a similar question and I replied there.

  5. Hello Marie, Really awesome guide! Thought I’m getting an issue where createUserWithEmailAndPassword() runs, logs as success and the same function inside useFirebaseAuth() never gets executed (= no user actually gets created). Any chance you’ve come across a similar issue? Appreciate it!

    1. Hey Ruben! Glad to hear you liked it. As for your issue, it doesn’t ring a bell but there could be a thousand reason why it is not working and it’s hard to tell why without seeing the code. If you are having some issues debugging, I suggest you post a question with a link to your code on StackOverflow and tagged it firebase. Good luck!

  6. How to use this for conditional routing? My index page is marketing page. I want to automatically redirect the user to home page if he loggedin. How to do that?

    1. Unfortunately, Next.js isn’t like React (and React Router) where you can easily define routes and redirect when needed. The only workaround would be to have a check in your index.js if the user is authenticated and redirect if he is. Something like this:

      const { authUser, loading } = useAuth();
      const router = useRouter();

      // Redirect if user is authenticated
      useEffect(() => {
      if (!loading && authUser)
      router.push(‘/homepage’)
      }, [authUser, loading])

  7. Thanks for the project. I am just learning nextjs, firebase, and react and this nailed it for me. I updated the project to firebase v9 and I would be more than happy to submit a PR. I didn’t see these comments until today so I created an issue against the project in github.

    1. Hey Dan! Thanks for the comment and glad this article was helpful. I am replying to your issue on the PR. 🙂

  8. Hi Marie,
    But I have a question, why use jsx suffixed files instead of js?( useFirebaseAuth.jsx, logged_in.jsx , sign_up.jsx )
    I am a beginner T-T

  9. Hey,

    Great lessen! I just need to know some basic details. How would I actually code the app to only accept certain emails? Say I have 2 admin for a blog site. How would I set it to deny some emails. Thanks in advance.

Leave a Reply