Next.js - authentication
NextAuth.js is a complete open-source authentication solution for Next.js applications. It is designed from the ground up to support Next.js.
- Source (NextAuth.js docs)
npm install next-auth@4.24.11# macOS
openssl rand -base64 32
# Windows can use https://generate-secret.vercel.app/32Generate random encoded string
Add environment variables in .env
DATABASE_URL="file:./dev.db"
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=yourencodedstring// File: /src/app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
const handler = NextAuth({
// our configuration will go here
});
export { handler as GET, handler as POST };Next.js - authentication
Example provider: username & password (Credentials)
npm install bcrypt
npm install -D @types/bcryptbcrypt: used for hashing a user's password
User model should have at least following fields:
model User {
id String @id @default(cuid())
email String @unique
hashedPassword String
}Oefening
import NextAuth, { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import prisma from "@/lib/client";
import bcrypt from "bcrypt";
/* We define authOptions as a separate constant and export it,
* so it can be used as an argument for other functions that require it (e.g. getServerSession)
*/
export const authOptions = {
providers: [
CredentialsProvider({
// Properties "name" and "credentials" define what we see on the /api/auth/signin page
name: "e-mail address and password",
credentials: {
email: {
label: "E-mail address",
type: "text",
placeholder: "john.doe@example.com",
},
password: { label: "Password", type: "password" },
},
/* Function below captures the credentials from the fields defined above and checks if they are valid.
* If not, return null -> this will show an error on the log-in page. If valid, return a user object. */
async authorize(credentials, req) {
if (!credentials?.email || !credentials?.password) {
return null;
}
const user = await prisma.user.findUnique({
where: { email: credentials.email },
});
if (!user) {
return null;
}
const passwordsMatch = await bcrypt.compare(
credentials.password,
user.hashedPassword!
);
return passwordsMatch ? user : null;
},
}),
],
// This defines that we use a JWT instead of a database to capture session data
session: {
strategy: "jwt",
},
}
const handler: NextAuthOptions = NextAuth(authOptions);
export { handler as GET, handler as POST };
Oefening
Debugging: wat zit er in onze session & token?
// File: /src/app/api/auth/token/route.ts
import { getServerSession } from "next-auth";
import { getToken } from "next-auth/jwt";
import { NextRequest, NextResponse } from "next/server";
import { authOptions } from "../[...nextauth]/route";
export async function GET(request: NextRequest) {
const token = await getToken({ req: request });
const session = await getServerSession(authOptions);
return NextResponse.json({
token: token,
session: session,
});
}Debugging: wat zit er in onze session & token?
{
"token": {
"name": "Karel De Smet",
"email": "karel.desmet@arteveldehs.be",
"sub": "cmgjddudj000004jxgup94s7n",
"iat": 1760034597,
"exp": 1762626597,
"jti": "aa145055-0019-482e-8202-3cf8060c8e4a"
},
"session": {
"user": {
"name": "Karel De Smet",
"email": "karel.desmet@arteveldehs.be"
},
}
}Debugging: wat zit er in onze session & token?
So should I use session or token data?
Next.js - authentication
To register middleware in Next.js:
export const config = {
// *: zero or more
// +: one or more
// ?: zero or one
// This will match /posts, /posts/1 and /posts/1/a
matcher: ["/posts/:id*"],
// This will match /posts and /posts/1
matcher: ["/posts/:id?"],
// This will match /posts/1 and /posts/1/a
matcher: ["/posts/:id+"],
};NextAuth.js has built-in middleware that automatically redirects to the sign-in page if the user is not logged in
// The line below will import the default object and export it in one line
export { default } from "next-auth/middleware";
export const config = {
matcher: ["/posts"],
};Oefening
Next.js - authentication
We're still stuck with the default sign-in page.
You can customize it with Theming or a custom page.
Oefening
Next.js - authentication
⚠️ NextAuth default middleware:
Oefening (demo repository):
Add the role to session and token data
// In route.ts:
export const authOptions: NextAuthOptions = {
// leave providers untouched,
// leave session untouched,
callbacks: {
// This adds the userId to the session data so we can use it on client (useSession) and server (getServerSession)
session: async ({ session, token }) => {
const user = await prisma.user.findUnique({
where: { email: session?.user?.email ?? undefined },
});
if (user?.id) {
session.role = user?.role;
}
return session;
},
// This adds the user's role to the token data so we can use it in middleware
jwt: async ({ token, user }) => {
if (user && "role" in user && user.role) {
token.role = user.role;
}
return token;
},
},
}
Inform TS about the extended session object
→ this is called module augmentation (TS docs)
import NextAuth from "next-auth";
import { Role } from "@/lib/client";
declare module "next-auth" {
interface Session {
role: Role;
}
}
// Create src/middleware.ts
import { withAuth } from "next-auth/middleware";
export default withAuth(
// `withAuth` augments your `Request` with the user's token.
function middleware(req) {
if (!req.nextauth.token || !req.nextauth.token.role) {
// Anonymous user
}
if (req.nextauth.token.role === "STANDARD") {
// Default user
}
if (req.nextauth.token.role === "ADMIN") {
// Administrator
}
}
);
export const config = {
matcher: ["/secure-page", "/admin-page"],
};User should not have access?
→ redirect to an appropriate page (example below)
import { NextResponse } from "next/server";
/* This will redirect a user from our middleware or API route to the sign-in page
* Attention: always use relative URLs -> https://nextjs.org/docs/messages/middleware-relative-urls */
function middleware(req) {
return NextResponse.redirect(new URL("/api/auth/signin", req.url));
}
export const config = {
matcher: ["/secure-page", "/admin-page"],
};
Putting it all together
import { withAuth } from "next-auth/middleware";
import { NextResponse } from "next/server";
const adminRoutes = ["/admin-page"];
export default withAuth(
// `withAuth` augments your `Request` with the user's token.
function middleware(req) {
if (!req.nextauth.token || !req.nextauth.token.role) {
return NextResponse.redirect(new URL("/api/auth/signin", req.url));
}
if (req.nextauth.token.role === "STANDARD") {
if (adminRoutes.includes(req.nextUrl.pathname)) {
return NextResponse.redirect(new URL("/api/auth/error", req.url));
}
}
}
);
export const config = {
matcher: ["/secure-page", "/admin-page"],
};
Oefening (deel 2):
Need session data on a server page or client component?
// On the client
const { data: session, status } = useSession();
// On the server
const session = await getServerSession(authOptions);Another approach for role-based access: