Using Next.js and Auth0
Older Article
This article was published 8 years ago. Some information may be outdated or no longer applicable.
Before you start
This guide walks through setting up authentication and authorisation in an example application built with Next.js and Auth0.
If you’re new to Auth0, have a look at this overview.
Get the code
Grab the code from this GitHub repository: https://github.com/tpiros/nextjs-auth0.
What is Next.js?
Next.js is a JavaScript framework for building server-rendered or statically exported React.js apps. A standard React application gets generated in the browser, which then serves the final HTML to the user. With Next.js, the HTML gets built on the server. The browser receives pure HTML and just needs to render it. (The browser still handles content updates after the initial load.)
Next.js is lean to set up. Three dependencies, no webpack configuration. It’s all handled for you.
Server-side rendering (SSR) exists in other frameworks too. Angular2+ applications can use SSR via Angular Universal, for instance.
What is Auth0?
Auth0 provides Authentication as a Service. Their SDK and APIs slot into applications without much friction. You can wire up multiple identity providers, and there’s a generous free tier (up to 7,000 users).
In this post, we’ll build an application using Next.js and Auth0. Let’s get into it.
Get your application keys
Sign up for Auth0 and create a new client. Grab the Client ID and Client Secret from the dashboard. You’ll need both.
Configure callback URLs
Before writing any code, configure the callback URLs. You’ll find these settings in the same place as the Client ID and Client Secret.
Set the Allowed Callback URL to http://localhost:3000/redirect and the Allowed Logout URL to http://localhost:3000.
If you’re running on a different port, update the port numbers. These URLs are required. Without them, the application will throw a mismatch error on login.
Install libraries
Install the project dependencies:
npm i auth0-js next react react-dom isomorphic-unfetch js-cookie jsonwebtoken
Then open package.json and update the scripts section:
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
The application
The app has a welcome page and a public page, both accessible to everyone. There’s also a secret page that only appears after login.
Creating the application
Next.js requires pages to live in a pages folder. Static resources go in static. We’ll also create a reusable 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
Create an index.js file under pages with this content:
export default () => <div>Hello world!</div>;
Fire up the dev server with npm run dev. Open http://localhost:3000 in a browser and you should see “Hello world!”.
Hot reloading comes built in with
npm run dev. No need to restart anything when you change the code.
The application flow
Users log in to see the secret page. On successful login, we store the JWT token in a cookie. As long as that cookie holds valid data, we can show the secret page.
Here’s the tricky bit. We need to check whether the cookie exists and contains our data, then let Next.js server-render the page accordingly. We also need to verify the token hasn’t been tampered with.
settings.js
This file holds global settings. Sign up for Auth0 and copy your credentials from the dashboard:
const clientID = process.env.CLIENTID || ''; // your clientID
const domain = process.env.DOMAIN || ''; // your domain
export { clientID, domain };
components/Header.js
A navigation bar component used across the application. Here’s the gist:
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;
The Header takes an isLoggedIn prop to toggle the right navigation items.
static/auth0.js
This file handles the Auth0-specific functions: login(), logout() and parseHash(). Here’s the login() function:
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
This file handles token verification and cookie operations. First, pull in the dependencies:
import Cookie from 'js-cookie';
import jwt from 'jsonwebtoken';
import fetch from 'isomorphic-unfetch';
import * as settings from '../settings';
We need functions to save and delete cookies, verify the token, and retrieve the JWK.
Token verification and JWK
Auth0 supports two algorithms for signing JWTs: RS256 and HS256. Our example uses RS256, which means we need to fetch a JSON Web Key (JWK) to verify the token.
A JWK lives inside a JWKS (JSON Web Key Set). Auth0 currently supports a single JWK for signing. You can access the signing JWK via the
keysproperty in the JWKS.
Auth0 exposes the JWKS file through an endpoint, so we can grab the signing JWK easily.
At a high level, token verification goes like this:
- Retrieve the JWKS and extract the
keyproperty for the JWK - Decode the JWT and grab the
kidfrom the header - Compare that
kidwith thekidfrom the JWK - Build a certificate from the
x5cproperty of the JWK - Run the verification
Two functions handle this:
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;
}
}
}
}
We also need cookie save/delete functions. We’ll use js-cookie for this. (This GitHub issue explains why.)
function saveToken(jwtToken, accessToken) {
Cookie.set('user', jwtDecode(jwtToken));
Cookie.set('jwtToken', jwtToken);
}
function deleteToken() {
Cookie.remove('user');
Cookie.remove('jwtToken');
}
Both are dead simple.
Finally, we need functions to retrieve the token. Two of them: one reads the cookie from the browser, the other extracts it from request headers on the server:
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 the verifyToken() call. Nobody gets to tamper with the token unchecked.
Export everything so other files can use these functions:
export {
saveToken,
deleteToken,
getTokenForBrowser,
getTokenForServer,
verifyToken,
};
The hardest bit is behind us. Let’s look at the templates.
static/template.js
This template checks for a token and builds up the properties that pages will consume:
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 renders both the header and the page content based on the properties it sets up.
pages/index.js
The entry file. This loads first when someone hits http://localhost:3000. It uses our template, and if isLoggedIn is false, it shows 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 calls the login method from auth0.js:
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 the whole thing.
pages/redirect.js
Auth0 login works via a popup that asks for credentials. After a successful login, the user gets redirected to a callback URL (the one we set up in the Auth0 console). On that redirect, we save the JWT token to a cookie:
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;
}
}
We’re also using Router.push('/') to send the user back to the front page.
pages/logout.js
Logging out is simple. Call the right 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;
}
}
Don’t forget to delete the cookies during logout. Skip that step and you’ll get odd behaviour.
static/secure-template.js
For pages that should only appear after login, we create a separate template. It’s identical to template.js except the render() method swaps content based on login state:
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
The page we only show to logged-in users, built on the secure 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
The simplest page. A <div> that’s visible to everyone:
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 with Next.js gets interesting when you layer in authentication. The tricky part is making sure the server can access the JWT token so it can build the right pages. Cookies bridge that gap, and token verification keeps things secure.