Building Custom Authentication for Your Next.js Apps
Tags: javascript, nextjs, webdev, react
Published: 2022-03-26 04:56:07

[Next.js](https://nextjs.org/) is an open-source web development framework built on top of Node.js enabling you to build SEO-friendly React-based web apps by utilizing its features like server-side rendering and static-site generation.

## Authentication
Handling authentication and authorization in your next.js app can be a bit tricky. There are so many options when it comes to adding auth like [NextAuth](https://next-auth.js.org/), [Auth0](https://auth0.com/), [Firebase](https://firebase.google.com/), etc. And these are pretty easy to start with. But sometimes you might need to build your own authentication feature to suit our web app needs rather than using these pre-built solutions. So in this article, we'll be building our own authentication feature using jwt, cookies, and mongodb.

## Let's Get Started
First, you need to have node.js installed on your machine. You can get it from [their official website](https://nodejs.org/).
Next, create an empty next.js project by running the following command:

```
npx create-next-app next-custom-auth --example "https://github.com/vercel/next-learn/tree/master/basics/learn-starter"
``` 
Now we need the following packages for our app:

- [axios](https://www.npmjs.com/package/axios)

- [cookies-next](https://www.npmjs.com/package/cookies-next)

- [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken)

- [mongoose](https://www.npmjs.com/package/mongoose)


Install the packages:

```
npm i axios cookies-next jsonwebtoken mongoose
``` 
## Setting Up MongoDB
Create a '.env.local' file in the root of your project and add the environment variable for mongodb uri.

![.env.local file](https://cdn.hashnode.com/res/hashnode/image/upload/v1647854536993/H4D-WBPlk.png)

Now create a 'lib' folder in the root of your project, inside of that create a 'dbConnect.js'. Add the following code in it:

```
// lib/dbConnect.js

import mongoose from "mongoose";

async function dbConnect() {
  return await mongoose.connect(process.env.MONGODB_URI);
}

export default dbConnect;

``` 
Here, we're creating an asynchronous function that uses mongoose to connect to the MongoDB database. We will call this function whenever we need to perform database operations.

## Building the UI
Now let's build the UI for the home, signup, and signin page.

### Layout Component
Create a 'components' directory in the root of the project and then add a 'Layout.js' file.

```
// components/Layout.js

import Link from "next/link";

export default function Layout({ children }) {
  return (
    <>
      <nav>
        <Link href="/">
          <a>Home Page</a>
        </Link>

        <Link href="/signup">
          <a>SignUp</a>
        </Link>

        <Link href="/signin">
          <a>SignIn</a>
        </Link>
      </nav>

      <section>{children}</section>
    </>
  );
}

``` 
We will use this component for all of our pages. It adds a navigation bar and renders its children.

### Home Page
Inside of your 'index.js' file in the pages directory add the following code:

```
// pages/index.js

import Layout from "../components/Layout";

export default function HomePage(props) {

  const signoutHandler = () => {
      // logic for signout
  };

  return (
    <Layout>
      <h1>Home Page</h1>
      <p>
        This is the home page and it is protected. Only authenticated users can
        access this page.
      </p>

      <p>
        <strong>Name</strong>: name
      </p>
      <p>
        <strong>Email</strong>: email
      </p>

      <button onClick={signoutHandler}>Sign out</button>
    </Layout>
  );
}
``` 
It will look like this:

![home page](https://cdn.hashnode.com/res/hashnode/image/upload/v1647860176838/jP19KDPX5.png)

### SignUp Page
Create a 'signup.js' file in the 'pages' directory and add the following code:

```
// pages/signup.js

import Layout from "../components/Layout";
import { useState } from "react";

export default function SignupPage() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const signupHandler = async (e) => {
    e.preventDefault();
    // signup logic
  };

  return (
    <Layout>
      <h1>SignUp</h1>

      <p>Only unauthenticated users can access this page.</p>

      <form onSubmit={signupHandler}>
        <input
          type="text"
          placeholder="Name"
          onChange={(e) => setName(e.target.value)}
          value={name}
        />
        <input
          type="email"
          placeholder="Email"
          onChange={(e) => setEmail(e.target.value)}
          value={email}
        />
        <input
          type="password"
          placeholder="Password"
          onChange={(e) => setPassword(e.target.value)}
          value={password}
        />
        <button>SignUp</button>
      </form>
    </Layout>
  );
}
``` 
It will look like this:

![signup paqe](https://cdn.hashnode.com/res/hashnode/image/upload/v1647861470300/5uTANGWPc.png)

### SignIn Page
Create a 'signin.js' file in the pages directory and add the following code:

```
// pages/signin.js

import Layout from "../components/Layout";
import { useState } from "react";

export default function SigninPage() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const signinHandler = async (e) => {
    e.preventDefault();
    // Signin logic
  };

  return (
    <Layout>
      <h1>SignIn</h1>

      <p>Only unauthenticated users can access this page.</p>

      <form onSubmit={signinHandler}>
        <input
          type="email"
          placeholder="Email"
          onChange={(e) => setEmail(e.target.value)}
          value={email}
        />
        <input
          type="password"
          placeholder="Password"
          onChange={(e) => setPassword(e.target.value)}
          value={password}
        />
        <button>SignIn</button>
      </form>
    </Layout>
  );
}
``` 
It will look like this:

![signin page](https://cdn.hashnode.com/res/hashnode/image/upload/v1647861789388/zx_qz8_p4.png)
I've added some CSS but I'm not going to show it here because styling isn't the focus of this article.

## Creating the User Model
Create a 'models' directory at the root of the project. Inside, add a 'user.js' file and add the following code:

```
// models/user.js

import mongoose from "mongoose";

const UserSchema = new mongoose.Schema({
  name: String,
  email: String,
  password: String,
});

export default mongoose.models.User || mongoose.model("User", UserSchema);
``` 
Here, we're creating a ***User*** schema using mongoose. It has the name, email, and password properties. Finally, we're creating a model and exporting it.

## Building the API
Now we will work on the API. To begin, first, let's create a directory name 'api' in the 'pages' directory where we will add all of our endpoints.

### Signup Endpoint
Create a 'signup.js' file in the 'api' directory and add the following code:

```
// pages/api/signup.js

import dbConnect from "../../lib/dbConnect";
import User from "../../models/user";
import jwt from "jsonwebtoken";
import { setCookies } from "cookies-next";

export default async function handler(req, res) {
  await dbConnect();

  const { name, email, password } = req.body;

  if (req.method === "POST") {
    const userExist = await User.findOne({ email });

    if (userExist)
      return res.status(422).json({ message: "Email already in use!" });

    const user = new User({ name, email, password });
    await user.save();

    const token = jwt.sign({ userId: user._id }, process.env.TOKEN_SECRET, {
      expiresIn: "1d",
    });

    setCookies("token", token, {
      req,
      res,
      maxAge: 60 * 60 * 24, // 1 day
      path: "/",
    });

    res.status(201).json(user);
  } else {
    res.status(424).json({ message: "Invalid method!" });
  }
}

``` 
Here, we created a handler function. Inside, we're using the ***dbConnect*** function to connect the database and destructed the properties from ***req.body***. After that, we're only accepting ***POST*** requests. Then, we're validating the ***email*** and saving the user. We're creating a ***jsonwebtoken*** and storing it in a cookie using the ***setCookies*** method. The ***maxAge*** means it will expire after 24 hours. Make sure to add the ***TOKEN_SECRET*** in your ***.env.local*** file that you can use for creating jwts.

You can learn more about ***cookies-next*** from [here](https://github.com/andreizanik/cookies-next).

### SignIn Endpoint
Create a 'signin.js' file in the 'api' directory and add the following code:

```
// pages/api/signin.js

import dbConnect from "../../lib/dbConnect";
import User from "../../models/user";
import jwt from "jsonwebtoken";
import { setCookies } from "cookies-next";

export default async function handler(req, res) {
  await dbConnect();

  const { email, password } = req.body;

  if (req.method === "POST") {
    const user = await User.findOne({ email, password });

    if (!user)
      return res.status(422).json({ message: "Wrong email or password!" });

    const token = jwt.sign({ userId: user._id }, process.env.TOKEN_SECRET, {
      expiresIn: "1d",
    });

    setCookies("token", token, {
      req,
      res,
      maxAge: 60 * 60 * 24, // 1 day
      path: "/",
    });

    res.status(200).json(user);
  } else {
    res.status(424).json({ message: "Invalid method!" });
  }
}
``` 
It is very similar to the ***signup*** endpoint. Here, we're accepting the email and password of a user and validating it before letting the user to signin.

## Connecting the API with Frontend
Now head over to the 'signup.js' file in the 'pages' directory and add the following code in the ***signupHandler*** function:

```
// pages/signup.js

const signupHandler = async (e) => {
    e.preventDefault();

    try {
      const res = await axios.post("/api/signup", {
        name,
        email,
        password,
      });

      router.push("/");
    } catch (error) {
      console.log(error);
    }
  };
``` 
Here we're using ***axios*** to send an HTTP request to the ***signup*** endpoint along with the name, email, and password of the user. Once the user signs up, we're redirecting to the home page using the ***router*** object provided by next.js. 

You can import router and axios like so:

```
import axios from "axios";
import { useRouter } from "next/router";

// inside of the component
const router = useRouter();
``` 
Now head over to the 'signin.js' file in the 'pages' directory and add the following code in the ***signinHandler*** function:

```
// pages/signin.js

 const signinHandler = async (e) => {
    e.preventDefault();

    try {
      const res = await axios.post("/api/signin", {
        email,
        password,
      });

      router.push("/");
    } catch (error) {
      console.log(error);
    }
  };
``` 
Here we're sending an HTTP request to the ***signin*** endpoint along with the email and password of the user. Once the user signs in, we're redirecting to the home page.

Now open the 'index.js' file in the 'pages' directory and add this code in ***signoutHandler*** function:

```
// pages/index.js

const signoutHandler = () => {
    removeCookies("token");
    router.push("/signin");
  };
``` 
It removes the 'token' cookie to signout the user.

## Fetching User Data
Now we need to verify whether the user is signed in or not to restrict the home page from being accessed by an unauthenticated user.

Create a file 'getUser.js' in the 'lib' directory and add the following code:

```
// lib/getUser.js

import { getCookie } from "cookies-next";
import jwt from "jsonwebtoken";
import User from "../models/user";

export default async function getUser(req, res) {
  const token = getCookie("token", { req, res });

  try {
    const data = jwt.verify(token, process.env.TOKEN_SECRET);
    let user = await User.findById(data.userId);
    user = JSON.parse(JSON.stringify(user));
    return user;
  } catch (error) {
    return null;
  }
}
``` 
We will use the ***getUser*** function to verify the user. It takes in the ***req*** and ***res*** object and fetches the cookie. Then it verifies the token using the ***jwt.verify*** method. Then it fetches the user from the database using its id.  
Remember, we stored the user's id in the token early in our article.  
Then there's some serializing and parsing. What's that? Here, we're serializing and parsing the user document, it's because the ***_id*** of a document is of type ***ObjectId*** and we need to convert it into a string. You can avoid it and run the code to see what's going to happen. After that, we finally returned the user. If we don't have any user we will simply return null.

Now add this code in the 'index.js' file in your 'pages' directory:

```
// pages/index.js

export async function getServerSideProps({ req, res }) {
  await dbConnect();
  const user = await getUser(req, res);
  if (!user) {
    return {
      redirect: {
        permanent: false,
        destination: "/signin",
      },
      props: {},
    };
  }
  return {
    props: {
      user,
    },
  };
}
``` 
Here we're using server-side rendering to fetch the user data. If there's no user, we'll simply redirect to the 'signin' page otherwise we're good to go. You can the user data that we're returning as props in your component.

Now we need to make sure that only unauthenticated users can access the signin and signup pages. For both of the pages add the following code:

```
// pages/signup.js and pages/signin.js

export async function getServerSideProps({ req, res }) {
  await dbConnect();

  const user = await getUser(req, res);
  if (user) {
    return {
      redirect: {
        permanent: false,
        destination: "/",
      },
      props: {},
    };
  }
  return {
    props: {},
  };
}

``` 
If the user is available we will redirect to the home page.  
That's it!

## Conclusion
Adding authentication from scratch can be a bit spiny but you can go through this article to quickly understand how to do it. One thing which we missed was password hashing. You can do that by using the [bcrypt](https://www.npmjs.com/package/bcryptjs) package. The main goal was to build using jwt, cookies, and mongodb.

Here's the:  
[Live version of the app.](https://next-custom-auth.vercel.app/)  
[Source code.](https://github.com/shahmirfaisal/next-custom-auth)

[Follow me on twitter.](https://twitter.com/codewithshahmir)  
[My Blog.](https://shahmir.me/)