DEV Community

Cover image for Easy user authentication with Next.js
Chris Garrett
Chris Garrett

Posted on • Edited on

Easy user authentication with Next.js

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
  }
);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (13)

Collapse
 
tsm012 profile image
Tsm012

Love the simplicity. However, after I am successfully authenticated, my user session is not available inside my getServerSideProps function, so a 404 is always returned even after a successful authentication and redirect. Any thoughts on why:

const user = req.session.get("user");

would work in the sessions.js file but be unavailable in the getServerSideProps function?

Collapse
 
mnengwa profile image
mnengwa • Edited

HI, @tsm012. How did you solve the above issue? I am experiencing the same issue.

Create a util in utils/cookies.js

import {withIronSession} from 'next-iron-session';

const cookie = {
    cookieName: process.env.COOKIE_NAME,
    password: process.env.COOKIE_SECRET,
    cookieOptions: {secure: process.env.NODE_ENV === 'production'},
};

export const guard = (handler) => {
    return withIronSession(handler, cookie);
};

export default cookie;
Enter fullscreen mode Exit fullscreen mode

Implement lookup logic

....
req.session.set('user', { user });

// persist session value
await req.session.save();

console.log(req.session.get('user'));
return res.status(201).json({success, message});
Enter fullscreen mode Exit fullscreen mode

Attempt to access the session value

export const getServerSideProps = guard(async (ctx) => {
    const {req} = ctx;

    const session = req.session.get();

    console.log({session});

    return {redirect: {destination: '/sign-in', permanent: false}};
});
Enter fullscreen mode Exit fullscreen mode

At this point, the session is empty e.g { }

Collapse
 
kibaekkimm profile image
Kibaek Kim

In my case, different cookieNames property.
because i just copied and paste code above.

Collapse
 
samuelgoldenbaum profile image
Samuel Goldenbaum • Edited

Returning a 404 for an unauthenticated user - surely this would be a 401/403. In your demo, SSR would just show a 404 which doesn't make any sense.

redirecting/or using res.end within getServerSideProps will result in the error: 'ERR_HTTP_HEADERS_SENT' so don't see how that works either.

Collapse
 
chrsgrrtt profile image
Chris Garrett • Edited

It's quite common to return a 404 on protected endpoints for an unauthed user - it makes it harder to "profile" an application from the outside. Github do this, for instance. Feel free to implement differently though, for instance redirecting to the sign in screen - it's just a demo afterall...

I'm using res.statusCode, res.send inside getServerSidePros without issue; are you basing your assumption on a statically compiled next app?

Collapse
 
samuelgoldenbaum profile image
Samuel Goldenbaum

Thanks for the reply Chris.

There is an RFC for this issue. Take a look at the codesandbox demo and you will see

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

Maybe this is handled somehow in next-iron-session

Thread Thread
 
chrsgrrtt profile image
Chris Garrett

Thanks Samuel - that is very bizarre! I'm using this exact code successfully in a project at the moment, but you're right - it is an issue in the codesandbox demo... I'll do some digging.

Thread Thread
 
samuelgoldenbaum profile image
Samuel Goldenbaum

It seems to be an RFC at the moment and would be a great solution to be able to set headers - which could allow a redirect in getServerSidePros which would be great.

Currently, I have to use getInitialProps in a HOC and check if we SSR/client and do something like:

getInitialProps = async (ctx) => {
        const {token} = nextCookie(ctx);

        if (!token) {
            if (typeof window === 'undefined') {
                ctx.res.writeHead(302, {Location: '/login'});
                ctx.res.end();
            } else {
                Router.push('/login');
            }
        }

        return {};
    }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
sheptang profile image
Anıl Yarkın Yücel

Is it possible to use withIronSession in _app.jsx and pass the session:true|false to pageProps to wrap around each and every possible page in an easier way? That being said, it'd be inside the function App({ Component, pageProps }){.... Please tell me "YES", I need this answer and how :')

Collapse
 
akanshsirohi profile image
Akansh Sirohi

Very great, simple and useful example, but am not able to understand that how we could write a decorator/HoC that wraps getServerSideProps and performs the session validation in a reusable way, can anyone here give an example of that related to this example here.

Collapse
 
_builtbyjay profile image
Jay Vincent

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

Collapse
 
jsbimra profile image
Jatinder Singh

Yes succinct and too the points ;) more words could be cutdown at what is it explanation blocks. :)