DEV Community

renan leonel
renan leonel

Posted on

Next.js 14 authentication

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:

  1. Creating the project: Creating a new Next.js project with NextAuth v5.

  2. Setting up NextAuth: Configuring NextAuth to use the credentials provider.

  3. Protecting routes: Creating a middleware file to protect routes.

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
Enter fullscreen mode Exit fullscreen mode

After that, we need to install NextAuth v5:

pnpm add next-auth@beta
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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;
      },
    }),
  ],
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
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;
Enter fullscreen mode Exit fullscreen mode
"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;
Enter fullscreen mode Exit fullscreen mode

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!" }),
});
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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).*)"],
};
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)