Using Next.js and Auth0

Featured

Before you start

This guide walks you through setting up authentication and authorisation in an example application created with Next.js and Auth0.

If you're new to Auth0, please take a look at this overview.

Get the code

You can download the code for this tutorial from this GitHub repository: https://github.com/tpiros/nextjs-auth0.

What is Next.js?

Next.js is a JavaScript framework that allows for the creation of server-rendered or statically exported React.js apps. A standard React application would be generated by the browser, which would also eventually serve the final HTML to the user. With Next.js the generation of the final HTML is done at the server-side, so the last product that the browser receives is pure HTML, and all the browser needs to do is to present it to the user. (Please note that the browser still does updates to the content)

Furthermore, Next.js is easy to use as it requires three dependencies and we don't need to worry about setting up and configuring webpack - all that is done for us by Next.js.

Server-side rendering (SSR) is a concept that is available in other frameworks as well - for example, Angular2+ applications can utilise SSR as well via Angular Universal.

What is Auth0?

Auth0 provides Authentication as a Service - they aim to solve the challenges developers face when adding authentication to their application. It's straightforward to use their SDK or call one of their APIs and hook it up with an application. It's also possible to use multiple identity providers, and the good news is that there's a generous free tier available (up to 7,000 users).

In this post, we'll take a look at how to put together an application using Next.js and Auth0. Let's get started.

Get your application keys

Once you have signed up for Auth0, please go ahead and create a new client. Once you have done that, please collect the Client ID, and the Client Secret as these are details that we will be using in the application.

Configure callback URLs

Before we get started with the development we also need to configure callback URLs for the application - you can do this at the same location where you have collected the Client ID and the Client Secret in the previous step.

For this example to work, an Allowed Callback URL is required, as well as an Allowed Logout URL. Set the first one to have the value of http://localhost:3000/redirect and the second one to be http://localhost:3000.

Please note that if you use a different port during the development, you'll need to update the port numbers accordingly. Also, please remember to set these URLs as they are required; otherwise the application will display a mismatch error when attempting to log in.

Install libraries

The next step is to set up all the project dependencies that the application will require. To achieve this run the following command:

npm i auth0-js next react react-dom isomorphic-unfetch js-cookie jsonwebtoken

Once the installation is complete, please open up package.json and modify the scripts section so that it looks like this:

"scripts": {
  "dev": "next",
  "build": "next build",
  "start": "next start"
}

The application

The application will have a welcome page, a public page - both of which are going to be accessible by everyone. On top of this, a secret page is also going to be available for users who log in.

Creating the application

Next.js requires us to put pages that it will render into a folder called pages. On top of this, we will also add some static resources - these will be placed into a folder called static. And we will also create a component, which will be a reusable component - any of our pages will be able to use the component.

components
  - Header.js
pages
  - index.js
  - login.js
  - logout.js
  - public.js
  - redirect.js
  - secret.js
  - 
static
  - auth.js
  - auth0.js
  - secure-template.js
  - template.js
- settings.js

Testing the setup

The easiest way to test the setup is to create an index.js file under the pages folder, with the following content:

export default () => <div>Hello world!</div>

Then fire up a test application server by executing npm run dev from the command line. Open up a browser and navigate to http://localhost:3000 to see the "Hello world!" message returned.

Please note that hot reloading is enabled by the npm run dev command, which means that we don't need to restart the service while making changes to the application - the changes will immediately be visible to us in the browser.

The application flow

The application will allow users to log in to see a secret page. Once a user has successfully logged in, we want to store the JWT token used for authentication on the client side - in this case, we'll use a cookie to store this information. As long as the data in the cookie is valid and available, we can safely display the 'secret' page.

And this is where things will become a bit tricky. We need to do a check to see if a cookie existed with our information and based on that, allow Next.js to server-render the page for us, and furthermore, we also need to make sure that nobody has messed with the token - in other words, we need to verify the token.

settings.js

This file is just stores global settings that we will reuse in our application - make sure you've sign up for an Auth0 application. Copy your settings from the Auth0 dashboard to this file:

const clientID = process.env.CLIENTID || ''; // your clientID
const domain = process.env.DOMAIN || ''; // your domain

export {
  clientID,
  domain
};

components/Header.js

As mentioned earlier we'll create a component that is going to be used throughout the application. In our example this is going to be a navigation bar, here's some code excerpt:

import Link from 'next/link';
import PropTypes from 'prop-types';

const Header = ({ isLoggedIn }) => (
  <div>
  // ... some inline styles
  <nav>
      <ul>
        <li><Link href="/"><a>Home</a></Link></li>
        <li><Link href="/public"><a>Public</a></Link></li>
        { isLoggedIn ? ( <li><Link href="/secret"><a>Secret</a></Link></li> ) : ( <li><Link href="/login"><a>Login</a></Link></li> ) }
        { isLoggedIn ? ( <li><Link href="/logout"><a>Logout</a></Link></li> ) : ( '' ) }
      </ul>
    </nav>
    <h1>Auth0 & Next.js</h1>
  </div>
)

Header.propTypes = {
  isLoggedIn: PropTypes.bool
};

export default Header;

Notice that our Header element will need an isLoggedIn property so that we can display the appropriate navigation items.

static/auth0.js

This file is solely responsible for handling the Auth0 specific functions: login(), logout() and parseHash(). This is how the login() function looks like:

import auth0 from 'auth0-js';
import * as settings from '../settings';

const clientID = settings.clientID;
const domain = settings.domain;

function webAuth(clientID, domain) {
  return new auth0.WebAuth({
    clientID: clientID,
    domain: domain
  });
}

function login() {
  const options = {
    responseType: 'id_token',
    redirectUri: 'http://localhost:3000/redirect',
    scope: 'openid profile email'
  };
  
  return webAuth(clientID, domain).authorize(options);
}
// continued implementation of the other functions

static/auth.js

Let's now take a look at the file that is responsible for handling the token verification and the cookie operations as well. The first thing that we need to do in this file is to bring in all the dependencies that we require:

import Cookie from 'js-cookie';
import jwt from 'jsonwebtoken';
import fetch from 'isomorphic-unfetch';
import * as settings from '../settings';

On top of this, we'll also need a few functions; we need to save and delete cookies, verify the token as well as return the JWK for our application.

Token verification and JWK

Auth0 supports two algorithms for signing JWTs - these are RS256 and HS256. In our example application, the signature is RS256 which means that when it comes to verifying the token, we need to retrieve a JSON Web Key (JWK).

A JWK is part of a JWKS (JSON Web Key Set). A JWKS contains a set of JWKs - Auth0 at the moment only supports a single JWK for signing a token. Please remember that the JWKs used for signing can be accessed in the JWKS as part of the keys property.

Luckily Auth0 exposes the JWKS file to us via an endpoint, which means we can quickly retrieve the JWK used for signing.

At a very high level the token verification consists of these steps:

  • Retrieve the JWKS and extract the key property for the JWK
  • Extract & decode the JWT and grab the kid property from the header
  • compare the previously mentioned kid with the kid property from the JWK
  • create a certificate based on the x5c property from the JWK
  • run the verification

To achieve the above, we'll create a function to retrieve the JWK, then create another function to do the actual token verification:

async function getJWK() {
  const res = await fetch(`https://${settings.domain}/.well-known/jwks.json`);
  const jwk = await res.json();
  return jwk;
}

async function verifyToken(token) {
  if (token) {
    const decodedToken = jwt.decode(token, { complete: true });
    const jwk = await getJWK();
    let cert = jwk.keys[0].x5c[0];
    cert = cert.match(/.{1,64}/g).join('\n');
    cert = `-----BEGIN CERTIFICATE-----\n${cert}\n-----END CERTIFICATE-----\n`;
    if (jwk.keys[0].kid === decodedToken.header.kid) {
      try {
        jwt.verify(token, cert);
        return true;
      } catch (error) {
        console.error(error);
        return false;
      }
    }
  }
}

Furthermore, we also need to implement the cookie save/delete functionality as part of this file. For this, we'll use the js-cookie package. (Read this GitHub Issue to understand why we require the js-cookie package.)

function saveToken(jwtToken, accessToken) {
  Cookie.set('user', jwtDecode(jwtToken));
  Cookie.set('jwtToken', jwtToken);
};

function deleteToken() {
  Cookie.remove('user');
  Cookie.remove('jwtToken');
};

The above two functions are very straight forward.

Last but not least we need to implement a function that will retrieve the token. In fact, we'll create two separate functions: one to be used to retrieve the token from the cookie either from the browser or be able to extract it from the cookie property from the request headers:

async function getTokenForBrowser() {
  const token = Cookie.getJSON('jwtToken');
  const validToken = await verifyToken(token);
  if (validToken) {
    return Cookie.getJSON('user');
  }
}

async function getTokenForServer(req) {
  if (req.headers.cookie) {
    const jwtFromCookie = req.headers.cookie.split(';').find(c => c.trim().startsWith('jwtToken='));
    if (!jwtFromCookie) {
      return undefined;
    }
    const token = jwtFromCookie.split('=')[1];
    const validToken = await verifyToken(token);
    if (validToken) {
      return jwt.decode(token);
    } else {
      return undefined;
    }
  }
}

Notice that how we call the verifyToken() method to make sure that nobody has fiddled with the token.

And the last thing in this file is to export all these functions so that we can use them in other files:

export {
  saveToken,
  deleteToken,
  getTokenForBrowser,
  getTokenForServer,
  verifyToken
};

So the good news is that the most complicated part is out of the way. Let's take a look at our templates.

static/template.js

We create a template that allows us to check whether a token existed and based on that build up the properties used by pages reusing this template:

import React from 'react';
import Header from '../components/Header';
import { getTokenForBrowser, getTokenForServer } from '../static/auth';

export default Page => class Template extends React.Component {
  static async getInitialProps({ req }) {
    const loggedInUser = process.browser ? await getTokenForBrowser() : await getTokenForServer(req);
    const pageProperties = await Page.getInitialProps && await Page.getInitialProps(req);
    return {
      ...pageProperties,
      loggedInUser,
      isLoggedIn: !!loggedInUser
    }
  }

  render() {
    return (
      <div>
        <Header { ...this.props } />
        <Page { ...this.props } />
      </div>
    )
  }
}

The template here will render both the header and the page (content) based on the properties specified by the template itself.

pages/index.js

Let's take a look at our entry file - this is the file that will be loaded first if someone navigates to http://localhost:3000. This file will make use of the template created earlier - if the isLoggedIn does not property exist we display a "You're not logged in yet" message:

import PropTypes from 'prop-types';
import { getToken } from '../static/auth.js';
import template from '../static/template';

const Index = ({ isLoggedIn }) => (
  <div>
    Hello, this is the main application.
    { !isLoggedIn && (
      <p>You're not logged in yet</p>
    )}
  </div>
);

Index.propTypes = {
  isLoggedIn: PropTypes.bool
}

export default template(Index);

pages/login.js

This file simply implements the login method that we have specified in the previously created auth0.js file:

import React from 'react';
import { login } from '../static/auth0';
import template from '../static/template';

class Login extends React.Component {
  componentDidMount () {
    login();
  }
  render() {
    return null;
  }
}

export default template(Login);

That's it; there's nothing more to it.

pages/redirect.js

The way our Auth0 login mechanism works is that we get a popup window asking for our credentials, once we enter those, we are redirected to a callback URL - this is the URL that we set up in the Auth0 management console as well earlier. This means that if we have a successful login, we can save the returned JWT token to a cookie - and we already have a function for that, all we need to do is to invoke it:

import React from 'react';
import Router from 'next/router';
import { parseHash } from '../static/auth0';
import { saveToken, verifyToken } from '../static/auth';

export default class extends React.Component {
  componentDidMount () {
    parseHash((err, result) => {
      if (err) {
        console.error('Error signing in', err);
        return;
      }
      verifyToken(result.idToken).then(valid => {
        if (valid) {
          saveToken(result.idToken, result.accessToken);
          Router.push('/');
        } else {
          Router.push('/')
        }
      });
    })
  }
  render() {
    return null;
  }
}

Please note that we are also using Router.push('/'); to redirect the user back to the front page of the application.

pages/logout.js

Logging out is going to be very straight forward - we just need to call the appropriate functions from auth.js:

import React from 'react';
import { logout } from '../static/auth0';
import { deleteToken } from '../static/auth';
import Router from 'next/router';

export default class extends React.Component {
  componentDidMount () {
    deleteToken();
    logout();
    Router.push('/');
  }
  render() {
    return null;
  }
}

It's important to remember that during the log out phase, we also delete the cookies stored - failing so could yield unexpected results.

static/secure-template.js

To be able to display "secret" pages (pages, once the user is logged in) it makes sense to create a new template. This template looks exactly like the previously created template.js file but has a different render() method that renders different content based on the fact that someone is logged in or not:

  render() {
    if (!this.props.isLoggedIn) {
      return (
        <div>
          <Header { ...this.props } />
          <p>You're not authorised. Try to <a href="/login">Login</a></p>  
        </div>
      )
    }
    return (
      <div>
        <Header { ...this.props } />
        <Page { ...this.props } />
      </div>
    )
  }

pages/secret.js

This is the page that we want to display only to people who are logged in, and this is the file that will make use of the previously created template:

import PropTypes from 'prop-types';
import SecureTemplate from '../static/secure-template';

const Secret = ({ loggedInUser }) => (
  <div>
    Hi { loggedInUser.nickname }! <img src={ loggedInUser.picture } width="100px" />
    <div>
      <style jsx>{`
         pre {
           width: 500px;
           background: #ddd;
           white-space: pre-wrap;
         }
       `}
       </style>
      <pre>{ JSON.stringify(loggedInUser, null, 2) }</pre>
    </div>
  </div>
)

Secret.propTypes = {
  loggedInUser: PropTypes.object
};

export default SecureTemplate(Secret);

pages/public.js

This page is the easiest to comprehend, and there's nothing new here - we return a <div> that's publicly visible to everyone accessing the page:

import template from '../static/template.js';

const Public = () => (
  <div>
    <p>This page is visible to everyone!</p>
  </div>
)

export default template(Public)

Conclusion

Server-side rendering via Next.js is indeed an interesting concept as we saw from the example outlined throughout this article. Adding authentication can be a bit tricky because we need to remember that the server-side also needs to be able to access the JWT token to be able to generate the appropriate pages.