loading...
Cover image for Easy user authentication with Next.js

Easy user authentication with Next.js

chrsgrrtt profile image Chris Garrett Updated on ・4 min read

Over the past couple of releases, Next.js has made some impressive additions which have transformed the way I develop with it. One of my favourite new features is the getServerSideProps function; 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:

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 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).
  • 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:

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:

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 refs to capture the email and password inputs, and registering an onSubmit callback.
  • That onSubmit callback is then using fetch 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

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.

Posted on Jun 3 by:

chrsgrrtt profile

Chris Garrett

@chrsgrrtt

Founder, Full Stack Developer & Designer

Discussion

markdown guide
 

This is great, thanks Chris! And thanks for the introduction to next-iron-session