← Blog

Next.js: restrict pages to authenticated users

Many websites have pages that need to be restricted to authenticated users, for example profile pages or dashboards.

Next.js web framework comes with a lot of functionality built-in: JS compilation, rendering code on the server, and caching.

When it comes to most other aspects, it’s up to a programmer to build, including such needed functionality as restricting access to some pages only to authenticated users.

In this article we are going to build just that: a function that augments a page object, that could be used like that:

const ProfilePage: NextPage = () => {
	return <PageLayout>{/* PAGE BODY */}</PageLayout>
}

// THE ACTUAL USAGE
requireAuth(ProfilePage)

export default ProfilePage

Here’s the implementation in TypeScript:

import { NextPage, NextPageContext } from "next"
import Router from "next/router"

const requireAuth = (page: NextPage) => {
	// make sure this function is safe run several times
	if (page.__authIsRequired) {
		return
	}
	page.__authIsRequired = true

	const originalGetInitialProps = page.getInitialProps

	page.getInitialProps = async (ctx: NextPageContext) => {
		const { res, req } = ctx
		// httpClient on server side needs to be smart enough to send cookies
		const fetchWithCookies = makeHttpClient(ctx)
		const user = await fetchWithCookies("/api/users/current")
		if (!user) {
			if (res) {
				const loginUrl = `/login?redirectTo=${encodeURIComponent(req.url)}`
				res.writeHead(302, "Not authenticated", { Location: loginUrl })
				res.end()
			} else {
				const loginUrl = `/login?redirectTo=${encodeURIComponent(window.location.pathname)}`
				await Router.push(loginUrl)
			}
			return {}
		}
		return originalGetInitialProps ? originalGetInitialProps(ctx) : {}
	}
}

export default requireAuth

In order to make it work on the server we are going to need one more utility function fetchWithCookies.

When making an HTTP request from inside a web browser, the browser automatically sends cookies with each request and then stores new cookies if a response comes in with a set-cookie header.

On server-side we need to build this feature ourselves, because neither Next.js nor NodeJS make any assumption about a request flow.

In order to do that we can leverage the ctx: { req, res } object that Next.js passes into getInitialProps during SSR.

// return an augmented `fetch` function that correctly sends
// and receives cookies during SSR
function makeHttpClient(ctx: NextPageContext): typeof fetch {
	return async function fetchWithCookies(input: RequestInfo, options: RequestInit = {}) {
		const actualOptions: RequestInit = {
			credentials: "include", // send cookies with request
			redirect: "manual", // don't follow redirects
			...options,
			headers: {
				cookie: ctx?.req?.headers?.cookie ?? "",
				...(options.headers ?? {}),
			},
		}
		// on server side we use isomorphic-unfetch NPM package,
		// on client side we use browser built-in `fetch` function
		let isomorphicFetch: typeof fetch
		if (typeof window === "undefined") {
			isomorphicFetch = (await import("isomorphic-unfetch")).default
		} else {
			isomorphicFetch = window.fetch
		}
		const result = await isomorphicFetch(input, actualOptions)

		// browsers (client-size) already handle this case automatically,
		// but in case some cookies are getting set in response,
		// we need to handle that use case on server-side as well
		if (ctx?.res) {
			const cookiesFromApi = result.headers.get("set-cookie")
			if (cookiesFromApi) {
				ctx?.res.setHeader("set-cookie", cookiesFromApi)
			}
		}
		return result
	}
}

Of course this implementation is only one way to do this, for example the HTTP client looks different if you are using Apollo Graphql.

Thank you for reading!