App Router Protection with Auth.js

These are some of the easy ways of protecting routes and code in Next.js 13's App Router.

August 25 2023

Blog post thumbnail for App Router Protection with Auth.js

With the introduction of the App Router being stable, the way of using Auth.js (formally NextAuth.js) has changed ever so slightly due to the introduction of server and client components.

Basic Set Up

First, we need to install next-auth:

npm install next-auth

Next, we will create the basic API route containing our authentication configurations.

app/api/auth/[...nextauth]/route.ts
/* app/api/auth/[...nextauth]/route.ts */
import NextAuth, { type AuthOptions } from "next-auth";
import GithubProvider, { type GithubProfile } from "next-auth/providers/github";

export const authOptions: AuthOptions = {
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

Protecting Server Code

The general format for protecting server code (ie: server components, API routes, server actions) is generally the same:

  1. Get the user's session from getServerSession(), in which we pass in our authOptions config.
  2. Check whether the session exists and serve the content if it does or throw an error or redirect if it doesn't.
    • If the session exists, there will be a user property containing information about the current user (ie: their name, email, and an image).
/* Protecting a server component page. */
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";

import { authOptions } from "@/app/api/auth/[...nextauth]/route";

export default async function ProtectedPage() {
const session = await getServerSession(authOptions);
if (!session) {
// Handle the case of an unauthenticated user.
redirect("/login");
}

/* Rest of code */
return (
<div>
<h1>This is a protected server component.</h1>
</div>
);
}

With the App Router, there's a special file called layout.tsx, which is used to share UI between routes. One interesting thing is that by default, they are server components. This means if we want protection on a nested route (ie: "/protectedRoute/..."), we can do that by simply putting the auth-protection code in the layout.tsx file instead of in each individual page.tsx file.

app/protectedRoute/layout.tsx
/* app/protectedRoute/layout.tsx */
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";

import { authOptions } from "@/app/api/auth/[...nextauth]/route";

export default async function Layout({ children } : { children: React.ReactNode }) {
const session = await getServerSession(authOptions);
if (!session) {
// Handle the case of an unauthenticated user.
redirect("/login");
}

/* Rest of code */
return <>{children}</>
}

Protecting Client Code

If we want to protect our client code, we need to do an additional step. We need to wrap our root layout.tsx file with a SessionProvider.

import Provider from "@/components/Provider";

export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Provider>
{children}
</Provider>
</body>
</html>
);
}

Then, unlike with server files where we obtained the session using getServerSession(), we'll now use useSession().

/* Protecting a client component page. */
"use client";
import { useSession } from "next-auth/react";
import { redirect } from "next/navigation";

export default async function ProtectedPage() {
const { data: session } = await useSession({
required: true,
onUnauthenticated() {
// Handle the case of an unauthenticated user.
redirect("/login");
},
});

/* Rest of code */
return (
<div>
<h1>This is a protected client component.</h1>
</div>
);
}