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 2127

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:

We made a custom demo for .
No really. Click here to check it out.

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.

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!

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 apps, recording literally everything that happens on your Next 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.

2 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 🙂

Leave a Reply