Easy user authentication with Next.js
Tags: nextjs, javascript, webdev, security
Published: 2020-06-03 15:07:41

Over the past couple of releases, [Next.js](https://nextjs.org/) has made some impressive additions which have transformed the way I develop with it. One of my favourite new features is the [`getServerSideProps` function](https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering); a function which can be appended to any page component, is executed on the server for each request, and injects the resulting data into the page as `props`.

Why do I like this so much? Well, put simply, it makes my life dramatically easier - and no piece of functionality better illustrates this than the ubiquitous _user sign in_...

Up until now, authentication within a general React/SPA project has been a complicated task, fraught with danger. In basic cases, it involves various hooks and API calls; in more extreme cases, jargonistic acronyms like JWT and PKCE come into play. But not anymore! With `getServerSideProps`, secure server sessions are back on the menu. Hear that thud? That's the sound of 100s of lines of redundant code dropping out of my project.

# The code

Starting with a clean Next.js app, adding user sign in requires just three parts:

* A page for the user sign in form.
* An API endpoint for validating the user credentials and setting the user session.
* A page which validates the user session or rejects the request.

We'll start with the sessions API endpoint, and by creating `pages/api/sessions.js`:

```javascript
import { withIronSession } from "next-iron-session";

const VALID_EMAIL = "chris@decimal.fm";
const VALID_PASSWORD = "opensesame";

export default withIronSession(
  async (req, res) => {
    if (req.method === "POST") {
      const { email, password } = req.body;

      if (email === VALID_EMAIL && password === VALID_PASSWORD) {
        req.session.set("user", { email });
        await req.session.save();
        return res.status(201).send("");
      }

      return res.status(403).send("");
    }

    return res.status(404).send("");
  },
  {
    cookieName: "MYSITECOOKIE",
    cookieOptions: {
      secure: process.env.NODE_ENV === "production" ? true : false
    },
    password: process.env.APPLICATION_SECRET
  }
);
```

Let's break this down:

* There are two prerequisites here: first we use the fantastic [Next Iron Session](https://github.com/vvo/next-iron-session) package to simplify dealing with sessions, so you'll need to `npm install --save next-iron-session`; secondly you'll need to add a 32 character string called `APPLICATION_SECRET` to your `.env`, which is used to secure the session content.
* My credentials check is very crude (`email === VALID_EMAIL && password === VALID_PASSWORD`) to keep the example simple; in reality you'd likely be doing a datastore lookup (and _please_ use [password hashing](https://www.npmjs.com/package/bcryptjs#usage---async)).
* Next.js API routes aren't scoped by HTTP verb, hence I've added the `if (req.method === "POST")` check to lock this down a little.

Next we're going to create our private page, `pages/private.jsx`:

```javascript
import React from "react";
import { withIronSession } from "next-iron-session";

const PrivatePage = ({ user }) => (
  <div>
    <h1>Hello {user.email}</h1>
    <p>Secret things live here...</p>
  </div>
);

export const getServerSideProps = withIronSession(
  async ({ req, res }) => {
    const user = req.session.get("user");

    if (!user) {
      res.statusCode = 404;
      res.end();
      return { props: {} };
    }

    return {
      props: { user }
    };
  },
  {
    cookieName: "MYSITECOOKIE",
    cookieOptions: {
      secure: process.env.NODE_ENV === "production" ? true : false
    },
    password: process.env.APPLICATION_SECRET
  }
);

export default PrivatePage;
```

So what's happening here?

* Firstly, we're using `getServerSideProps` to check for the existence of the user session - which would have been set by our sessions API endpoint.
* If there's no session, we're sending an empty 404 back to the browser. You could redirect to the sign in page instead, but I prefer this approach as it deters snooping bots/crawlers.
* Finally, we're piping the contents of the user session into the page component as a prop.

So now we have a private page, and an API endpoint to open it up - we just need to add our sign in form to bring it all together, `pages/signin.jsx`:

```javascript
import React, { useRef } from "react";
import { useRouter } from "next/router";

const SignInPage = () => {
  const router = useRouter();
  const emailInput = useRef();
  const passwordInput = useRef();

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

    const email = emailInput.current.value;
    const password = passwordInput.current.value;

    const response = await fetch("/sessions", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email, password })
    });

    if (response.ok) {
      return router.push("/private");
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>
          Email: <input type="text" ref={emailInput} />
        </label>
      </div>
      <div>
        <label>
          Password: <input type="password" ref={passwordInput} />
        </label>
      </div>
      <div>
        <button type="submit">Sign in</button>
      </div>
    </form>
  );
};

export default SignInPage;
```

It might seem like there's a lot going on with this one, but it's actually the most basic piece:

* We're rendering out a form; using `ref`s to capture the email and password inputs, and registering an `onSubmit` callback.
* That onSubmit callback is then using [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to call our sessions API endpoint with the supplied value.
* If the sessions API endpoint responds with an `ok` header, we're assuming the user session has been set successfully, and redirecting the user to our private page.

> But hey... If we're just checking the users details exist in the session, what's to stop someone creating a fake session and pretending to be someone else?

Very good question! Remember that 32 character `APPLICATION_SECRET` we added to our `.env`? That's used to encrypt the session contents, so it's not readable (or spoofable) to the outside world. All the client will see is something like this:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/mlusi9xi0ah6bn76b541.png)

Just remember: it's called `APPLICATION_*SECRET*` for a reason, keep it that way.

# That's all folks

That's it; we've added a functional, and secure user sign in function to our Next.js app - with minimal code overhead.

# What's next?

I've kept the code deliberately blunt for the purpose of this article; there are some obvious and immediate improvements required to take this forward into a full application:

* We don't want to be repeating the session lookup for _all_ our secured pages - we could write a decorator/HoC that wraps `getServerSideProps` and performs the session validation in a reusable way.
* I've not abstracted the iron session cookie name (`cookieName: "MYSITECOOKIE"`) - this encourages developer error and bugs, so should be moved to a shared constant or the env.
* Our sign in form doesn't provide any kind of validation messaging to the end user; we could add a `useState` hook to display helpful errors.
* We've not added user sign out functionality; that could be added for `DELETE` calls to the sessions API endpoint.