It was almost 3 years ago that ZEIT released Next.js, a minimalist framework to build single-page Javascript applications in a simple yet customizable way. With a focus on performance and out of the box support for Server-Side Rendering (SSR), they reached over 280,000 weekly downloads on NPM and 40,000 stars on GitHub. The Next.js showcase confirms the success of the framework which is now being used by companies big and small, including Netflix, Scale.ai, Marvel, Jet, and even Auth0.
Providing a solution to support authentication in Next.js was one of the most requested features in the platform. But why is that? Can’t we use any of the tools that we’ve been using for so long in React and Node.js (e.g.: passport
, auth0.js
, …)? Next.js blurs the line between frontend and backend, making the existing ecosystem suboptimal if you want to use Next.js to its full potential.
One example is Passport, which depends on the availability of Express. And while you could technically use Express in your Next.js application, it will make all of the performance improvements just fade away. If you want to optimize for fast cold starts and want to improve your reliability and scalability, you need to shift to the serverless deployment model.
There are different ways to build and deploy applications with Next.js and, in this blog post, we’ll cover those use cases and explain what mechanism you can best use to authenticate.
When you’re building a Next.js application, authentication might be needed in the following cases:
/api/my/invoices
www.mycompany.com
to billing.mycompany.com/api
Now that we understand where and when our application might require authentication, let’s explore the authentication strategies that can be implemented for different Next.js deployment models.
Next.js allows to you generate a standalone static application without the need for a Node.js server. When you run next build
the command will generate HTML files for each page that supports it. You can use this generated output to deploy your site to any static hosting service, such as Now, Amazon S3 or Netlify.
This technique could be used to generate complete websites as static sites, like a company public front page, or when you’re creating an “admin dashboard”. The generated HTML could simply be the shell of your application — think of this shell as the header and footer of your application. The ZEIT dashboard is one of the best examples out there of how this could look like:
Once the “shell” has been served, the client-side will call the necessary APIs (carrying the user information), fetch user-specific content, and update the page:
This model has several advantages when it comes to hosting. Static hosting sites (like Now, Amazon S3, Azure Blob Storage, Netlify, and others) are battle-tested, inexpensive, but more importantly, they are extremely fast and play well with CDNs.
One thing that will be somewhat different is how we handle authentication. The model where a server is available can handle the interaction with Auth0 and create a session, but in this model, we don’t have a backend. All of the work happens on the frontend:
id_token
and access_token
which will be stored in memory.If your use case requires dynamic content or user-specific content, you will also need to deploy something else, like an API. This API won’t be able to run as part of your static hosting site so this is where you’ll be using a platform like AWS Lambda, Heroku, or Now, to deploy it. The client-side will then talk to that API directly by providing an access_token
, fetch the dynamic content, and enrich page that was served by the static hosting platform.
And this is very similar to how any single-page application is built, where the application doesn’t have an actual “backend” but instead calls one or more APIs. You’ll find a variety of examples in the community of how to sign in to this type of application:
useAuth
(using auth0.js
)auth0-spa-js
)use-auth0-hooks
(using auth0-spa-js
)With use-auth0-hooks
, for example, it’s as easy as configuring your application like so:
import { Auth0Provider } from 'use-auth0-hooks';
export default class Root extends App {
render () {
const { Component, pageProps } = this.props;
return (
<Auth0Provider
domain={'sandrino-dev.auth0.com'}
clientId={'9f6ClmBt37ZGCXNqToPbefKmzVBSOLa2'}
redirectUri={'http://localhost:3000/'}>
<Component {...pageProps} />
</Auth0Provider>
);
}
}
And then you can use React Hooks to retrieve the user and request an access token for one of your APIs. The access_token
is then sent along when you call your API, which the following example does through the useApi
hook:
import { useAuth } from 'use-auth0-hooks';
export default function MyShows() {
const { isAuthenticated, isLoading, accessToken } = useAuth({
audience: 'https://api/tv-shows',
scope: 'read:shows'
});
if (!isAuthenticated) {
return (
<div>You must first sign in to access your subscriptions.</div>;
)
}
if (isLoading) {
return (
<div>Loading your user information...</div>
);
}
const { response, loading } = useApi(
`${process.env.API_BASE_URL}/api/my/shows`,
accessToken
);
if (loading) {
return (
<h1>Subscriptions for {user.email}<h1>
<div>Loading your subscriptions ...</div>
);
}
return (
<h1>Subscriptions for {user.email}<h1>
<div>You have subscribed to a total of {response && response.shows && response.shows.length} shows...</div>
);
}
When using auth0-spa-js
the user will sign in using the Authorization Code Grant with PKCE. At a high level, the user will be redirected to Auth0 which will be handling all of the required authentication and authorization logic (sign-up, sign-in, MFA, consent, and so on). After the user completes the authentication process with Auth0, the user is redirected back to your application with an Authorization Code in the query string.
The client-side will exchange that code for an id_token
and optionally an access_token
(1,2). The access_token
can then be used to call your API. When the access_token
expires, the same flow will happen again under the covers, using an <iframe>
. This “silent authentication” approach will keep working for as long as the user is signed in — as long as the user has a session in Auth0. When the user’s session in Auth0 expires or a sign out takes place, this call will fail and the user will be required to sign in again.
Where Next.js shines is in the serverless deployment model, where every page and API route are deployed as separate serverless functions implemented using ZEIT Now or AWS Lambda, for example.
In this model, you don’t have a full-blown web framework running (like Express.js), but instead, the runtime will execute functions by passing them a request and a response object ((req, res) => { }
). And this is why we can’t use traditional web frameworks (like Express.js) or any of the building blocks they offer for authenticating users (like Passport.js) and creating sessions (express-sessions
).
The following diagram illustrates how this model works: Next.js pages and API routes are all running as separate serverless functions. When the browser tries to access the TV Shows page (1), a function will take care of rendering and serving the page, effectively performing server-side rendering. This function will also call any APIs needed to fetch the necessary data (2).
If the entire site has already been loaded, whenever you visit another page, all of the rendering happens on the client. At that point, all API calls are made directly from the browser. As you can see, this is where the line between the frontend and backend layers starts to become blurry.
Now, before we go into any specifics, it’s important to call out that there are two specific flavors of the serverless model when it comes to authentication, depending on where you need the user to be available.
One flavor which is very similar to the Static Site is shown in the diagram below. Whenever a page needs to be rendered on the server-side or when an API route is called, these calls will be executed in a serverless function. In this model, authentication takes place on the client-side:
id_token
and access_token
which will be stored in memory.Any page rendered by the serverless function will only be able to return content which is accessible by all users, without needing any form of authentication. Then, when the page is loaded, some logic can be executed on the client-side which will fetch user-specific content by calling API routes or by calling other APIs.
In the diagram above you can see an example of how this could work:
/account
page can be rendered by a serverless function (SSR)./api/pricing-tiers
API route which simply returns the different subscription types available in the application (for example, Free, Developer, Enterprise). This is public information so authentication is not required here./api/billing-info
API route and provide the user’s access token. The client-side can then render content that is specific to the userOnly the client-side and the API routes are aware of the user, while the server-side rendering of pages was only able to render public content (which is perfectly fine for SEO purposes).
The second flavor in this model is the one where the user is needed when the page is being rendered by the serverless function. When that happens, you can’t just rely on client-side authentication
This diagram is similar to the one from the frontend model, except for a few subtle but important differences:
/account
page can be rendered by a serverless function (SSR), but the browser will send the session cookie along./api/billing-info
by forwarding the session cookie, making server-side rendering of user content possible./api/pricing-tiers
API route (nothing changes here).In this example, the user’s account page can completely be rendered on the server side.
What you’ll also need to consider is the case where the site is already fully loaded and the user navigates to the account page. In that case, the client-side can call the endpoints directly and the cookie will automatically be provided to the API route:
In order to accommodate this use case, we’ve recently published an early access version of @auth0/nextjs-auth0
which takes care of authentication in the serverless deployment model using the Authorization Code Grant. This package also creates a session for the authenticated user using an HttpOnly
cookie which mitigates the most common XSS attack.
To use the library you’ll start by initializing an instance of the SDK:
import { initAuth0 } from '@auth0/nextjs-auth0';
export default initAuth0({
domain: '<AUTH0_DOMAIN>',
clientId: '<AUTH0_CLIENT_ID>',
clientSecret: '<AUTH0_CLIENT_SECRET>',
scope: 'openid profile',
redirectUri: 'http://localhost:3000/api/callback',
postLogoutRedirectUri: 'http://localhost:3000/',
session: {
cookieSecret: 'some-very-very-very-very-very-very-very-very-long-secret',
cookieLifetime: 60 * 60 * 8
}
});
Once an instance is created you’ll be adding a few API routes in your Next.js application which will be handling all of the necessary logic. Here is an example of the Login handler:
import auth0 from '../../utils/auth0';
export default async function login(req, res) {
try {
await auth0.handleLogin(req, res);
} catch(error) {
res.status(error.status || 500).end(error.message)
}
}
And that’s it! You can now access the user on the server side:
Profile.getInitialProps = async ({ req, res }) => {
if (typeof window === 'undefined') {
const { user } = await auth0.getSession(req);
if (!user) {
res.writeHead(302, {
Location: '/api/login'
});
res.end();
return;
}
return { user }
}
}
By implementing the Profile handler, you’ll also have an endpoint which exposes the user’s information to the client-side. As such, you can call API routes within your Next.js application without having to worry about access tokens or any of that. This is possible because the user’s session is stored in a cookie, which is sent along with every request your client makes to your API route.
async componentDidMount() {
const res = await fetch('/api/me');
if (res.ok) {
this.setState({
session: await res.json()
})
}
}
Note that in this model authentication takes place on the server, meaning that the client isn’t really aware that the user is signed in. You could make it aware by providing that information in the initial state or through an endpoint, but you won’t be exposing any id_token
or access_token
to the client. That information remains on the server side.
When using nextjs-auth0
, the user will sign in using the Authorization Code Grant. At a high level, the user will be redirected to Auth0 (1,2) which will be handling all of the required authentication and authorization logic (sign-up, sign-in, MFA, consent, and so on) after which the user is redirected back to your application with an Authorization Code in the query string (3).
The server-side (or better, the serverless function) will exchange (4) that code for an id_token
and optionally an access_token
and refresh_token
. After the id_token
has been validated, a session will be created and stored in an encrypted cookie (5). Each time a page is rendered (server-side) or an API route is called, the session cookie will be sent to the serverless functions which can then access the session and any relevant user information.
The pages and API routes can all access the user’s session, but that is not the case for external APIs which are typically hosted on other (sub-)domains. When accessing those APIs you’ll need to provide them with an access_token
to authorize the user.
When you need to call an external API on behalf of the user, you will be required to proxy that call through a Next.js API route. These routes will have access to the user’s session and, depending on how the user signed in, that session might contain the user’s information. Optionally, the session may also have the following:
id_token
access_token
refresh_token
When the Next.js API route needs to call an external API on behalf of the user, it can extract the access_token
from the session and add it to the Authorization
header.
The following example illustrates how you would create an API route which extracts the access_token
from the session and then uses it to call a downstream API.
import auth0 from '../../utils/auth0';
export default async function getBillingInfo(req, res) {
try {
const { accessToken } = await auth0.getSession(req);
const client = new BillingApiClient(accessToken);
return client.getBillingInfo();
} catch(error) {
console.error(error)
res.status(error.status || 500).end(error.message)
}
}
The biggest difference with the frontend model is that the server will also be aware of the user being signed in when rendering pages. And as such the server side will not be limited to loading public data, it will also be able to load data specific to the user and render that information server-side.
A full example can be found in the official Next.js repository.
A very common (but legacy) deployment model you’ll see with Next.js is where a custom server is used to host the Next.js application. A custom server in Next.js, which may be implemented using something like Express.js, accepts the request and forwards it to a request handler returned from calling the app.getRequestHandler()
method. With this approach the custom server can act as a proxy and have some processing take place before Next.js handles the request:
Middlewares, which run before the Next.js server side rendering, provide building blocks to your application like:
All of the building blocks and tools that you can use today with Express.js are available to you in this model. Here’s the most basic example of how you would host your Next.js application with Express.js:
const next = require('next');
const express = require('express');
...
const app = next({ dev });
const handle = app.getRequestHandler();
app.prepare()
.then(() => {
const server = express();
...
passport.use(auth0);
...
server.use(passport.initialize());
server.use(passport.session());
server.use(myApiRoutes);
...
server.get('*', (req, res) => {
return handle(req, res);
});
server.listen(3000, (err) => {
if (err) {
throw err;
}
console.log('Listening on http://localhost:3000')
});
})
.catch((ex) => {
console.error(ex.stack);
process.exit(1);
});
When it comes to authenticating users in this model you can use Passport.js (which is by far the most popular framework for authentication in Node.js) in combination with passport-auth0
. When the user signs in, a session will be created using express-session
and is then persisted in the browser using an HttpOnly
cookie.
Once the user has a session, they will be able to access pages or call API endpoints which require authentication using Next.js API routes or traditional Express endpoints. The session cookie will be sent along with each request which will automatically make the user information available on the server-side.
In this model, you are basically building a regular web application using Node.js. Authentication, database access, and other features are already a solved problem.
A full example of creating a Next.js application using a custom server can be found here: Next.js Authentication Tutorial
The Next.js docs no longer list this model because it’s the least optimal from a cost and performance point of view:
And with that, we’ve covered the different deployment models which exist today for the Next.js framework and we’ve explained the best way to authenticate in those models and why.
If this tutorial has helped you better decide what to use for your deployment model, let us know in the comments below!
#javascript #vue-js #security #web-development