Next.js 14 authentication
February 26, 2024 (65d ago)
This is a brief tutorial of how to create an authentication flow in Next.js 14 using NextAuth v5 using the middleware file. These are the steps we are going to follow:
-
Creating the project: Creating a new Next.js project with NextAuth v5.
-
Setting up NextAuth: Configuring NextAuth to use the credentials provider.
-
Protecting routes: Creating a middleware file to protect routes.
You can also find this article on DEV.
Creating the project
First, we need to create a new Next.js project. We can do that by running the following command:
npx create-next-app@latest
After that, we need to install NextAuth v5:
pnpm add next-auth@beta
If you face any issues with the installation, you can check the official documentation for Next.js and NextAuth.
Setting up NextAuth
Next, we need to create a new file called app/api/auth/[...nextauth]/route.ts
and add the following code:
export { GET, POST } from "@/auth";
Inside src
folder, create a file called auth.ts
, where we are going to create a mock function to simulate the access to the database. This is the function that will be called whenever the signIn
method from NextAuth is called.
import NextAuth from "next-auth"; import { authConfig } from "./auth.config"; import Credentials from "next-auth/providers/credentials"; async function getUser(email: string, password: string): Promise<any> { return { id: 1, name: "test user", email: email, password: password, }; } export const { auth, signIn, signOut, handlers: { GET, POST }, } = NextAuth({ ...authConfig, providers: [ Credentials({ name: "credentials", credentials: { email: { label: "email", type: "text" }, password: { label: "password", type: "password" }, }, async authorize(credentials) { const user = await getUser( credentials.email as string, credentials.password as string, ); return user ?? null; }, }), ], });
Also, create a file called auth.config.ts
and add the following code:
import type { NextAuthConfig } from "next-auth"; export const authConfig = { session: { strategy: "jwt", }, pages: { error: "/", signIn: "/", signOut: "/", }, callbacks: { authorized({ auth }) { const isAuthenticated = !!auth?.user; return isAuthenticated; }, }, providers: [], } satisfies NextAuthConfig;
Now, we need to define the routes of our application. We are going to create two pages, the root page where all users can access, and the /protected page, where only logged users can access.
First, create a file in src/lib/routes.ts
. This constants will be used in the middleware verification.
export const ROOT = "/"; export const PUBLIC_ROUTES = ["/"]; export const DEFAULT_REDIRECT = "/protected";
Then, create the two pages, with a Form
component
import Form from "@/components/form"; const Root = () => { return ( <main className="flex items-center justify-center h-screen w-screen"> <Form /> </main> ); }; export default Root;
import { auth } from "@/auth"; import { logout } from "@/lib/actions"; import { Button } from "@/components/ui/button"; const Protected = async () => { const session = await auth(); session?.user?.email; return ( <form action={logout} className="h-screen w-screen flex flex-col justify-center items-center gap-10" > <div> <p className="text-white">{session?.user?.name}</p> <p className="text-white">{session?.user?.email}</p> </div> <Button type="submit" className="w-40" variant="secondary"> logout </Button> </form> ); }; export default Protected;
"use client"; import { login } from "@/lib/actions"; import { useFormState } from "react-dom"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; const loginInitialState = { message: "", errors: { email: "", password: "", credentials: "", unknown: "", }, }; const Form = () => { const [formState, formAction] = useFormState(login, loginInitialState); return ( <form action={formAction} className="space-y-4 w-full max-w-sm"> <Input required name="email" placeholder="email" /> <Input required name="password" type="password" placeholder="password" /> <Button variant="secondary" className="w-full" type="submit"> submit </Button> </form> ); }; export default Form;
We are using zod to validate the our form with the following schema:
import { z } from "zod"; export const loginSchema = z.object({ email: z .string() .trim() .min(1, { message: "Email required!" }) .email({ message: "Invalid email!" }), password: z .string() .trim() .min(1, { message: "Password required!" }) .min(8, { message: "Password must have at least 8 characters!" }), });
Now, we need to create a file called actions.ts
in src/lib
folder. This file will contain the server action that will be used in the form.
"use server"; import { AuthError } from "next-auth"; import { signIn, signOut } from "@/auth"; import { loginSchema } from "@/types/schema"; const defaultValues = { email: "", password: "", }; export async function login(prevState: any, formData: FormData) { try { const email = formData.get("email"); const password = formData.get("password"); const validatedFields = loginSchema.safeParse({ email: email, password: password, }); if (!validatedFields.success) { return { message: "validation error", errors: validatedFields.error.flatten().fieldErrors, }; } await signIn("credentials", formData); return { message: "success", errors: {}, }; } catch (error) { if (error instanceof AuthError) { switch (error.type) { case "CredentialsSignin": return { message: "credentials error", errors: { ...defaultValues, credentials: "incorrect email or password", }, }; default: return { message: "unknown error", errors: { ...defaultValues, unknown: "unknown error", }, }; } } throw error; } } export async function logout() { await signOut(); }
Protecting routes
Finally, we need to create a middleware file to protect the routes. Create a file called middleware.ts
in src/lib
folder and add the following code:
import NextAuth from "next-auth"; import { authConfig } from "@/auth.config"; import { DEFAULT_REDIRECT, PUBLIC_ROUTES, ROOT } from "@/lib/routes"; const { auth } = NextAuth(authConfig); export default auth((req) => { const { nextUrl } = req; const isAuthenticated = !!req.auth; const isPublicRoute = PUBLIC_ROUTES.includes(nextUrl.pathname); if (isPublicRoute && isAuthenticated) return Response.redirect(new URL(DEFAULT_REDIRECT, nextUrl)); if (!isAuthenticated && !isPublicRoute) return Response.redirect(new URL(ROOT, nextUrl)); }); export const config = { matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], };
Here, we are retrieving if the user is authenticated for every route in our application and verifying the current path. If the user has a current session and tried to go back to the login page, they will be redirected back to the /protected page. If the user tries to access any route that isn’t public without a session, they will be redirected to the login page. You can use any matcher suitable for your application.
Now, you have a complete authentication flow using middleware in Next.js 14 with NextAuth v5! If you face any problems, you can check the GitHub repository with the full code to help you out.