DEV Community

Cover image for User Authentication with Next.js
Juan Olvera
Juan Olvera

Posted on • Edited on • Originally published at jolvera.dev

User Authentication with Next.js

Note: I wrote this post before API routes was released. I need to update the post to use the latest Next.js features. Meanwhile, you should read The Ultimate Guide to Next.js Authentication with Auth0 which is a great guide describing all the authentication patterns you can use with Next.js. This post focuses only on one method and explains you how to build it. I consider there's value in keeping both guides, so I will work on keep it up to date.

Previously published on my blog

User authentication with Next.js has been one of the most requested examples by the community. The GitHub issue had more than 300 likes and hundreds of comments with recommendations and proposals.

The issue asked the community to contribute an example with certain requirements:

  • re-usable authentication helper across pages
  • session synchronization among tabs
  • simple passwordless email backend hosted on now.sh

The primary purpose of this example was to have a starting point for newcomers.

With the release of Next.js 8 an example was finally accepted and merged into the examples repository. In this post, we will create the example from scratch.

You can find the code in the Next.js examples repository or play with the working demo deployed in Now 2.

Project setup

We'll set up the project as a monorepo with the recommended folder structure along with a now.json file so that we can deploy it to Now.

$ mkdir project
$ cd project
$ mkdir www api
$ touch now.json

Backend

We will use micro to handle our incoming requests and isomoprhic-unfetch to make our outoing API requests.

$ cd api
$ npm install isomorphic-unfetch micro --save

To simplify our example, we'll use the GitHub API as a passwordless backend. Our backend will call the /users/:username endpoint and retrieve the users’ id, then from now on, this id will be our token.

In our app, we'll create two functions that will work as endpoints: login.js to return a token, and profile.js to return the user information from a given token.

// api/login.js

const { json, send, createError, run } = require('micro')
const fetch = require('isomorphic-unfetch')

const login = async (req, res) => {
  const { username } = await json(req)
  const url = `https://api.github.com/users/${username}`

  try {
    const response = await fetch(url)
    if (response.ok) {
      const { id } = await response.json()
      send(res, 200, { token: id })
    } else {
      send(res, response.status, response.statusText)
    }
  } catch (error) {
    throw createError(error.statusCode, error.statusText)
  }
}

module.exports = (req, res) => run(req, res, login);
// api/profile.js

const { send, createError, run } = require('micro')
const fetch = require('isomorphic-unfetch')

const profile = async (req, res) => {
  if (!('authorization' in req.headers)) {
    throw createError(401, 'Authorization header missing')
  }

  const auth = await req.headers.authorization
  const { token } = JSON.parse(auth)
  const url = `https://api.github.com/user/${token}`

  try {
    const response = await fetch(url)

    if (response.ok) {
      const js = await response.json()
      // Need camelcase in the frontend
      const data = Object.assign({}, { avatarUrl: js.avatar_url }, js)
      send(res, 200, { data })
    } else {
      send(res, response.status, response.statusText)
    }
  } catch (error) {
    throw createError(error.statusCode, error.statusText)
  }
}

module.exports = (req, res) => run(req, res, profile)

With this, we have everything we need to handle our simplified Authentication/Authorization strategy in the backend.

Frontend

Now, inside our www/ folder, we need to install our Next.js app and dependencies,

$ cd www/
$ npm create-next-app .
$ npm install
$ npm install isomorphic-unfetch next-cookies js-cookie --save

create our pages,

$ touch pages/index.js
$ touch pages/profile.js

the file that will contain our authentication helpers,

$ mkdir utils
$ touch utils/auth.js

and the file that will contain our custom server for local development. We'll need this later to replicate the monorepo setup locally.

$ touch server.js

At this point, our www/ folder structure should look like this.

.
├── components
│   ├── header.js
│   └── layout.js
├── package-lock.json
├── package.json
├── pages
│   ├── index.js
│   ├── login.js
│   └── profile.js
├── server.js
└── utils
    └── auth.js

Our frontend structure is ready.

Login Page and Authentication

The login page will contain the form that will authenticate our users. The form will send a POST request to the /api/login.js endpoint with a username, then if the username exists the backend will return a token.

For this example, as long as we keep this token in the frontend, we can say that the user has an active session.

// www/pages/login.js

import { Component } from 'react'
import fetch from 'isomorphic-unfetch'
import Layout from '../components/layout'
import { login } from '../utils/auth'

class Login extends Component {
  static getInitialProps ({ req }) {
    const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'

    const apiUrl = process.browser
      ? `${protocol}://${window.location.host}/api/login.js`
      : `${protocol}://${req.headers.host}/api/login.js`

    return { apiUrl }
  }

  constructor (props) {
    super(props)

    this.state = { username: '', error: '' }
    this.handleChange = this.handleChange.bind(this)
    this.handleSubmit = this.handleSubmit.bind(this)
  }

  handleChange (event) {
    this.setState({ username: event.target.value })
  }

  async handleSubmit (event) {
    event.preventDefault()
    const username = this.state.username
    const url = this.props.apiUrl

    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username })
      })
      if (response.ok) {
        const { token } = await response.json()
        login({ token })
      } else {
        console.log('Login failed.')
        // https://github.com/developit/unfetch#caveats
        let error = new Error(response.statusText)
        error.response = response
        return Promise.reject(error)
      }
    } catch (error) {
      console.error(
        'You have an error in your code or there are Network issues.',
        error
      )
      throw new Error(error)
    }
  }

  render () {
    return (
      <Layout>
        <div className='login'>
          <form onSubmit={this.handleSubmit}>
            <label htmlFor='username'>GitHub username</label>

            <input
              type='text'
              id='username'
              name='username'
              value={this.state.username}
              onChange={this.handleChange}
            />

            <button type='submit'>Login</button>

            <p className={`error ${this.state.error && 'show'}`}>
              {this.state.error && `Error: ${this.state.error}`}
            </p>
          </form>
        </div>
        <style jsx>{`
          .login {
            max-width: 340px;
            margin: 0 auto;
            padding: 1rem;
            border: 1px solid #ccc;
            border-radius: 4px;
          }
          form {
            display: flex;
            flex-flow: column;
          }
          label {
            font-weight: 600;
          }
          input {
            padding: 8px;
            margin: 0.3rem 0 1rem;
            border: 1px solid #ccc;
            border-radius: 4px;
          }
          .error {
            margin: 0.5rem 0 0;
            display: none;
            color: brown;
          }
          .error.show {
            display: block;
          }
        `}</style>
      </Layout>
    )
  }
}

export default Login

Our getInitialProps() will generate a URL based on the environment we are and by checking if we are in the browser or the server.

The first line will set the protocol to https or https depending on the environment.

...
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'
...

Next, we get our host depending on whether we are in the browser or the server. This way, we will get the right URL even if we are in Now with a dynamically generated URL or in our local development using http://localhost:3000.

...
const apiUrl = process.browser
  ? `${protocol}://${window.location.host}/${endpoint}`
  : `${protocol}://${req.headers.host}/${endpoint}`;
...

Everything else is pretty standard with a form that makes a POST request on submission. We also use the local state to handle our simple validation error messages.

If our request is successful, we'll log in our user by saving the cookie with the token we got from the API, and redirect the user to our profile page.

...
cookie.set("token", token, { expires: 1 });
Router.push("/profile")
...

Profile Page and Authorization

With client-only SPAs, to Authenticate or Authorize a user, we have to let them request the page, load the JavaScript and then send a request to the server to verify the user’s session. Fortunately, Next.js gives us SSR, and we can check the user’s session on the server using getInitialProps();.

Authorization Helper Function

Before creating our profile page, we'll create a helper function in www/utils/auth.js that will restrict access to Authorized users.

// www/utils/auth.js

import Router from 'next/router'
import nextCookie from 'next-cookies'

export const auth = ctx => {
  const { token } = nextCookie(ctx)

  if (ctx.req && !token) {
    ctx.res.writeHead(302, { Location: '/login' })
    ctx.res.end()
    return
  }

  if (!token) {
    Router.push('/login')
  }

  return token
}

When a user loads the page, the function will try to get the token from the cookie using nextCookie, then if the session is invalid it will redirect the browser to the login page, otherwise Next.js will render the page normally.

// Implementation example
...
Profile.getInitialProps = async ctx => {
  // Check user's session
  const token = auth(ctx);

  return { token }
}
...

This helper is simple enough for our example and works on the server and the client. Optimally, we want to restrict access on the server, so we don't load unnecessary resources.

Authorization High Order Component

Another way to abstract this, is using a HOC that we can use in our restricted pages like Profile. We could use it like this:

import { withAuthSync } from '../utils/auth'

const Profile = props =>
  <div>If you can see this, you are logged in.</div>

export default withAuthSync(Profile)

Also, it will be useful later for our loggout functionality. Like so, we write our HOC the standard way and include our auth helper function to take care of the Authorization.

We create our HOC in our auth.js file as well.

// Gets the display name of a JSX component for dev tools
const getDisplayName = Component =>
  Component.displayName || Component.name || 'Component'

export const withAuthSync = WrappedComponent =>
  class extends Component {
    static displayName = `withAuthSync(${getDisplayName(WrappedComponent)})`

    static async getInitialProps (ctx) {
      const token = auth(ctx)

      const componentProps =
        WrappedComponent.getInitialProps &&
        (await WrappedComponent.getInitialProps(ctx))

      return { ...componentProps, token }
    }

    render () {
      return <WrappedComponent {...this.props} />
    }
}

Page Component with Authorized requests

Our profile page will show our GitHub avatar, name and bio. To pull this data from our API, we need to send an Authorized request. Our API will throw an error if the session is invalid and if so we will redirect our user to the login page.

With this, we create our restricted profile page with the authorized API calls.

// www/pages/profile.js

import Router from 'next/router'
import fetch from 'isomorphic-unfetch'
import nextCookie from 'next-cookies'
import Layout from '../components/layout'
import { withAuthSync } from '../utils/auth'

const Profile = props => {
  const { name, login, bio, avatarUrl } = props.data

  return (
    <Layout>
      <img src={avatarUrl} alt='Avatar' />
      <h1>{name}</h1>
      <p className='lead'>{login}</p>
      <p>{bio}</p>

      <style jsx>{`
        img {
          max-width: 200px;
          border-radius: 0.5rem;
        }
        h1 {
          margin-bottom: 0;
        }
        .lead {
          margin-top: 0;
          font-size: 1.5rem;
          font-weight: 300;
          color: #666;
        }
        p {
          color: #6a737d;
        }
      `}</style>
    </Layout>
  )
}

Profile.getInitialProps = async ctx => {
  // We use `nextCookie` to get the cookie and pass the token to the
  // frontend in the `props`.
  const { token } = nextCookie(ctx)
  const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'

  const apiUrl = process.browser
    ? `${protocol}://${window.location.host}/api/profile.js`
    : `${protocol}://${ctx.req.headers.host}/api/profile.js`

  const redirectOnError = () =>
    process.browser
      ? Router.push('/login')
      : ctx.res.writeHead(301, { Location: '/login' })

  try {
    const response = await fetch(apiUrl, {
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
        Authorization: JSON.stringify({ token })
      }
    })

    if (response.ok) {
      return await response.json()
    } else {
      // https://github.com/developit/unfetch#caveats
      return redirectOnError()
    }
  } catch (error) {
    // Implementation or Network error
    return redirectOnError()
  }
}

export default withAuthSync(Profile)

We send our GET request to our API with the credentials: "include" option to make sure our header Authorization is sent with our token in it. With this, we make sure our API gets what it needs to authorize our request and return the data.

Logout and Session Synchronization

In our frontend, to log out the user, we need to clear the cookie and redirect the user to the login page. We add a function in our auth.js file to do so.

// www/auth.js

import cookie from "js-cookie";
import Router from "next/router";

export const logout = () => {
  cookie.remove("token");
  Router.push("/login");
};

Every time we need to log out our user we call this function, and it should take care of it. However, one of the requirements was session synchronization, that means that if we log out the user, it should do it from all the browser tabs/windows. To do this we need to listen to a global event listener, but instead of setting something like a custom event we will use storage event.

To make it work we would have to add the event listener to all the restricted pages componentDidMount method, so instead of doing it manually, we'll include it in our withAuthSync HOC.

// www/utils/auth.js

// Gets the display name of a JSX component for dev tools
const getDisplayName = Component =>
  Component.displayName || Component.name || 'Component'

export const withAuthSync = WrappedComponent =>
  class extends Component {
    static displayName = `withAuthSync(${getDisplayName(WrappedComponent)})`

    static async getInitialProps (ctx) {
      const token = auth(ctx)

      const componentProps =
        WrappedComponent.getInitialProps &&
        (await WrappedComponent.getInitialProps(ctx))

      return { ...componentProps, token }
    }

    // New: We bind our methods
    constructor (props) {
      super(props)

      this.syncLogout = this.syncLogout.bind(this)
    }

    // New: Add event listener when a restricted Page Component mounts
    componentDidMount () {
      window.addEventListener('storage', this.syncLogout)
    }

    // New: Remove event listener when the Component unmount and
    // delete all data
    componentWillUnmount () {
      window.removeEventListener('storage', this.syncLogout)
      window.localStorage.removeItem('logout')
    }

    // New: Method to redirect the user when the event is called
    syncLogout (event) {
      if (event.key === 'logout') {
        console.log('logged out from storage!')
        Router.push('/login')
      }
    }

    render () {
      return <WrappedComponent {...this.props} />
    }
}

Then, we add the event that will trigger the log out on all windows to our logout function.

// www/utils/auth.js

import cookie from "js-cookie";
import Router from "next/router";

export const logout = () => {
  cookie.remove("token");
  // To trigger the event listener we save some random data into the `logout` key
  window.localStorage.setItem("logout", Date.now()); // new
  Router.push("/login");
};

Finally, because we added this functionality to our Authentication/Authorization HOC, we don't need to change anything in our Profile page.

Now, every time our user logs out, the session will be synchronized across all windows/tabs.

Deploy to Now 2

The only thing left is to write our configuration in our now.json file.

// now.json

{
  "version": 2,
  "name": "cookie-auth-nextjs", //
  "builds": [
    { "src": "www/package.json", "use": "@now/next" },
    { "src": "api/*.js", "use": "@now/node" }
  ],
  "routes": [
    { "src": "/api/(.*)", "dest": "/api/$1" },
    { "src": "/(.*)", "dest": "/www/$1" }
  ]
}

The configuration file tells Now how to route our requests and what builders to use. You can read more about it in the Deployment Configuration (now.json) page.

Local Development

In our API, the functions profile.js and login.js work correctly as lambdas when they are deployed in Now 2, but we can’t work with them locally as they are right now.

We can use them locally by importing the functions into a small server using basic routing. To accomplish this, we create a third file called dev.js that we'll use for local development only and install micro-dev as a development dependency.

$ cd api
$ touch dev.js
$ npm install micro-dev --save-dev
// api/dev.js

const { run, send } = require("micro");
const login = require("./login");
const profile = require("./profile");

const dev = async (req, res) => {
  switch (req.url) {
    case "/api/profile.js":
      await profile(req, res);
      break;
    case "/api/login.js":
      await login(req, res);
      break;

    default:
      send(res, 404, "404. Not found.");
      break;
  }
};

exports.default = (req, res) => run(req, res, dev);

The server will return the functions when a specific URLs is requested, this is a bit unconventional for routing, but it works for our example.

Then, in our frontend, we'll use a custom server for our Next.js app that will proxy certain requests to our API server. For this, we'll use http-proxy as a development dependency,

$ cd www
$ npm install http-proxy --save-dev
// www/server.js

const { createServer } = require("http");
const httpProxy = require("http-proxy");
const { parse } = require("url");
const next = require("next");

const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();

const proxy = httpProxy.createProxyServer();
const target = "http://localhost:3001";

app.prepare().then(() => {
  createServer((req, res) => {
    const parsedUrl = parse(req.url, true);
    const { pathname, query } = parsedUrl;

    switch (pathname) {
      case "/":
        app.render(req, res, "/", query);
        break;

      case "/login":
        app.render(req, res, "/login", query);
        break;

      case "/api/login.js":
        proxy.web(req, res, { target }, error => {
          console.log("Error!", error);
        });
        break;

      case "/profile":
        app.render(req, res, "/profile", query);
        break;

      case "/api/profile.js":
        proxy.web(req, res, { target }, error => console.log("Error!", error));
        break;

      default:
        handle(req, res, parsedUrl);
        break;
    }
  }).listen(3000, err => {
    if (err) throw err;
    console.log("> Ready on http://localhost:3000");
  });
});

and the last step is to modify our package.json to run our custom server with npm run dev.

// www/package.json

...
 "scripts": {
    "dev": "node server.js",
    "build": "next build",
    "start": "next start"
},
...

With this setup we can deploy it to Now 2 running now at the root folder or use it locally running micro-dev dev.js -p 3001 inside the api/ folder and npm run dev inside the www/ folder.

Conclusion

This example is the result of going through the issue comments, proposals, code examples, blog posts, and existing implementations and extracting the best parts of each one.

The example ended being a minimal representation of how Authentication should work in the Frontend using Next.js, leaving out features you might need in a real-world implementation and third-party libraries that were strongly recommended like Redux and Apollo (with GraphQL). Also, the example is backend agnostic, making it easy to use with any language in the server.

Finally, one of the many discussions was whether to use localStorage or cookies. The example uses cookies so we can share the token between the server and the client.

Top comments (28)

Collapse
 
ethanbonsignori profile image
Ethan Bonsignori

Hey Juan, I'm new to Next and this guide seems like it will be a huge help. I'm working on a school project and currently I'm using a decoupled express backend with a mongo database. I was going to use passport for user auth, but your guide seems really great. I thought I'd check with you to see what is the better option. Is it possible to use your guide here and change the backend files to work with express? I'm pretty new to this stuff so I appreciate any guidance.

Collapse
 
jolvera profile image
Juan Olvera

Hey Ethan.

Yes, you can use Express and Passport. Although, you could write helper functions to issue and verify JWTs using jsonwebtoken, since the Next.js app will manage most of the user authentication/authorization logic.

If you are using Now 2 to deploy, you could even avoid Express and Passport altogether.

But answering your question, yes you can implement what I did in the tutorial to work with Express.

Collapse
 
flolege profile image
flolege • Edited

Hi Juan - first many thanks for this very useful article. Especially for beginners like me this helps a lot.

I am going through your description and have a question: in www-pages-login.js you generate a environment-dependent URL. Why is that actually needed? The login will always be triggered by the user via the frontend, right? So actually only the frontend URL is needed. Please let me know if I am wrong and if yes why :)

Many thanks!

Collapse
 
jolvera profile image
Juan Olvera • Edited

Hey flolege, you are very welcome!

When you are using Next.js, the first load of your site will always be from the server, then it will hydrate the site using React. So, we need to check where the request is coming from and get the host from either the server or the client.

Right now we are working on a simplified Authentication example with way less code and easier to understand.

However, that way to get the host should be for development only. And it's meant to be used with Now 2, because every time you deploy you get a new URL. In a regular application, you probably know what's your development and production URLs and you should use those, using environment variables.

I hope I answered your question if not let me know.

Collapse
 
flolege profile image
flolege

Hey Juan, thanks for your reply. I think I understand. Since the first load is on the server, we do not have access to the window object. So we have to retrieve it another way. many thanks!

Collapse
 
mustafaalfar profile image
mustafa-alfar • Edited

could you please clarify why you use micro package ?,
and are api/login and api/profile backend files ?
I've read its description and I didn't understand the main idea?

Collapse
 
jolvera profile image
Juan Olvera • Edited

Hey Mustafa!

Let's clarify some points before the explanation.

The application in the example is built as a monorepo and it's meant to be deployed at Now.

The files that are in the api/ folder compose the backend. The files inside the www/ folder are the frontend, in this case, the Next.js application.

In the backend, every file it's a single function and works as an API endpoint. They are serverless lambdas, you can think of them as a route in an Express.js application, but this is an oversimplification.

So, the Micro package contains utilities that make it easier to deploy this kind of applications.

For example, if I need to parse JSON, with Express.js I'd have to include the entire library, setup middlewares, and so on. With Micro we can use a function from the package and pass the request as a parameter, e.g.,

// example.js

const { json } = require('micro');

const fn = (req, res) => json(req);

modules.export = fn;

so, if we save this code into a file called example.js we can deploy it to Now along with a now.json file and get a functional endpoint that we can call with fetch or axios.

You can clone the example repo and deploy it following its instructions. Once you see it working, take a look at the code, at the logs in Now and you will get a better idea of how it works.

Ping me on Twitter at @_jolvera if you have more questions and we can chat about it.

I hope this helps!

Collapse
 
mustafaalfar profile image
mustafa-alfar

thank you for this elaborating,
I'll try it out, and I'll tell you if I struggled with something.

Collapse
 
stokescomp profile image
Michael Stokes

Your link to monorepo doesn't exist.

Thread Thread
 
jolvera profile image
Juan Olvera

Hey, Michael.

ZEIT changed their repos. The replacement for the monorepo is gatsby-functions.

Collapse
 
helloitsm3 profile image
Sean

Good job on the tutorial. It really helped me to better understand how to do authentication in Nextjs when deploying to Now v2. I recently started a NextJS project and implemented Express as my backend to do user authentication, however, I only found out that it's not supported in Now v2 and have to re-write the code.

Collapse
 
jamesvibar profile image
vibar

Hi Juan! Thank you so much for this very detailed tutorial. Aside from helping me with auth, I also got a better understanding on how getInitialProps work! :D

I just have a question. I'm in Next 9 and when I try to use withAuthSync HOC. I get a UnhandledPromiseRejectionWarning: Error: No router instance found. error whenever I visit the page with the withAuthSync HOC.

You should only use "next/router" inside the client side of your app..

Collapse
 
harveyjones282 profile image
Harvey Jones • Edited

An incredibly detailed blog. I though I would have to gather scraps and bits from a dozen different places but you made my work easier. Thanks.

Another authentication which I wasn't able to find anywhere else was next.js with redux jwt authentication. Just like your blog I finally found it in one place. Just going to drop it here.
blog.carbonteq.com/next-js-redux-s...
Hope it helps someone else.

Collapse
 
jolvera profile image
Juan Olvera

This is awesome. Thank you for the link!

Collapse
 
giovannipds profile image
Giovanni Pires da Silva • Edited

Hi Juan! Excellent tutorial. The thing I most like it is the sync logout, that's freak!

I'll leave you one question: I'll be implementing this on my app, I'm already doing the HOCs, cookies, etc. but I'd like to redirect on logout only if I'm on a protected route. Do you have suggestion about handling this? A way to check if the page's using the HOC? If there's no other way, I'd probably end creating like a routes.js to know what page's protected but seems like a double logic for the same thing. Any approach would be good to hear.

Thank you very much!

Collapse
 
pedromagalhaes profile image
Pedro Magalhães

Nice tutorial! Would be even better to implement this example with React Hooks. Anyways good job!

Collapse
 
jolvera profile image
Juan Olvera

Hey Pedro,

When I started writing the tutorial, Hooks weren't released yet. I will wait a bit before I update them to Hooks, tho.

Thanks for the reply!

Collapse
 
pedromagalhaes profile image
Pedro Magalhães

No worries. Its pretty cool the tutorial.

Im using similiar tech stack in my company for more than an year and i really enjoy it, i.e. react+next+node+hoc+recompose+redux+eslint+styled components etc.

Now im currently doing a backoffice with same tech stack, but this time using hooks for component lifecycle, and context with reducers instead of redux. So far so good. I love it.

Anyways keep up with this kind of tutorials. I really enjoy to see them.

Cheers

Collapse
 
shikhaarora4 profile image
Shikha

I have tried to follow all the steps, but get this error when i try to deploy using now.
ENOENT: no such file or directory, stat '/Users/xxxxxxxx/SourceCode/node/my-app/node_modules/node-pre-gyp/node_modules/.bin/detect-libc'
Am I doing something wrong?

Collapse
 
kenshinman profile image
Kehinde Orilogbon

Hey, thanks for this wonderful explanation. Just wondering, how do I know current user or authentication in sub components? Thanks