Authentication For NextJs App Router

Authentication is defined as a process or action of verifying the identity of a user. We will use the popular Next-Auth package for implementing Authentication for NextJs App Router. Next-Auth provides extensive support for different kinds of authentication mechanism, for this article, we will explore the most common pattern of authenticating user with username and password, called Credential Provider.

nextjs paging

Content

Stater Project

Create a NextJS app using

npx create-next-app@latest
npx create-next-app@latest

Name your project "auth-cp" and accept rest of the defaults.

Next install next-auth package

npm install next-auth

Authentication Initialization

The main entry point for authentication is the NextAuth method that you import from next-auth. It handles both Get and Post request and is defined as route handler (route.ts)

Create directory below app called api/auth/[...nextauth]. In the above dir, add file route.ts and copy following

import NextAuth from 'next-auth';
import { authOptions } from '@/app/auth';
 
const handler = NextAuth(authOptions);
 
export { handler as GET, handler as POST };
import NextAuth from 'next-auth';
import { authOptions } from '@/app/auth';
 
const handler = NextAuth(authOptions);
 
export { handler as GET, handler as POST };

Authentication Configuration

Add new file auth.ts to app dir and copy following

import { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
 
import { verifyLogin } from '@/models/user.server';
import { User } from '@/models/user';
 
export const authOptions: NextAuthOptions = {
  session: {
    strategy: 'jwt',
  },
  pages: {
    signIn: '/login',
  },
  providers: [
    CredentialsProvider({
      id: 'credentials',
      name: 'credentials',
      credentials: {},
      async authorize(credentials: { email?: string; password?: string }) {
        //
        if (!credentials?.email || !credentials?.password) return null;
        //
        try {
          const email = credentials?.email;
          const pwd = credentials?.password;
          if (email === "example@example.com" && pwd === "Example!234") {
            return {
              id: 1,
			userId: 1,
              email,
              isActiveFlag: true,
            };
          }
          return null;
        } catch (error) {
          return null;
        }
      },
    }),
  ],
  callbacks: {
    async signIn({ user, account, profile, email, credentials }) {
      return (user as any).isActiveFlag;
    },
    async jwt({ token, user }) {
      if (!token.userId && user) {
        token.userId = (user as User).userId;
      }
      return token;
    },
    async session({ token, session, user }) {
      if (token) {
        if (session.user) {
          (session.user as any).userId = token.userId;
        } else {
          session.user = {
            // id: token.id,
            email: token.email,
            //@ts-ignore
            userId: token.userId,
          };
        }
      }
      return session;
    },
  },
};
import { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
 
import { verifyLogin } from '@/models/user.server';
import { User } from '@/models/user';
 
export const authOptions: NextAuthOptions = {
  session: {
    strategy: 'jwt',
  },
  pages: {
    signIn: '/login',
  },
  providers: [
    CredentialsProvider({
      id: 'credentials',
      name: 'credentials',
      credentials: {},
      async authorize(credentials: { email?: string; password?: string }) {
        //
        if (!credentials?.email || !credentials?.password) return null;
        //
        try {
          const email = credentials?.email;
          const pwd = credentials?.password;
          if (email === "example@example.com" && pwd === "Example!234") {
            return {
              id: 1,
			userId: 1,
              email,
              isActiveFlag: true,
            };
          }
          return null;
        } catch (error) {
          return null;
        }
      },
    }),
  ],
  callbacks: {
    async signIn({ user, account, profile, email, credentials }) {
      return (user as any).isActiveFlag;
    },
    async jwt({ token, user }) {
      if (!token.userId && user) {
        token.userId = (user as User).userId;
      }
      return token;
    },
    async session({ token, session, user }) {
      if (token) {
        if (session.user) {
          (session.user as any).userId = token.userId;
        } else {
          session.user = {
            // id: token.id,
            email: token.email,
            //@ts-ignore
            userId: token.userId,
          };
        }
      }
      return session;
    },
  },
};

Auth Options above define the major part of how Next-Auth will do authentication. Here were are using JWT strategy for session and our sign in page for users can be found at url /login. We will be using our own custom sign in page.

Next we are configuring CredentialProvider. Main authentication happens in authorize method. We get email and password from user and verify it. In real word scenario, you will be verifying these values against values stored in a database!

Callbacks

Callbacks are asynchronous functions that are used to control what happens when an action is performed.

signIn signIn callback is used to control if a user is allowed to sign in. In our case, if user is active, as indicated by isActiveFlag, then they are allowed to sign in!

jwt jwt callback is called whenever a JSON Web Token is created (i.e. at sign in) or updated (i.e whenever a session is accessed in the client). The returned value will be encrypted, and it is stored in a cookie.

We use this call back to save userId of the signed in user, which is used throughout app to retrieve current logged in user.

session Finally the session callback is called whenever a session is checked. We use it to initialize session with logged in user identification info (userId)

You can find all options that NextAuthOptions takes

Once last file to complete next-auth config. Add .env file for Next-Auth related environment variables and copy following to it

NEXTAUTH_SECRET="secret"
NEXTAUTH_URL="http://localhost:3000"

User Model

We will be using a simple user model as defined below. Copy following code to models/user.ts

export interface User {
  id: number;
  email: string;
  isActiveFlag: boolean;
}
export interface User {
  id: number;
  email: string;
  isActiveFlag: boolean;
}

Note: In real world scenarios, you will be checking it against database, for a fulling working examples, create a free Stater app using CreateAppAI and download the app codebase to see code that verifies user against database using Prisma queries.

What To Protect

So far we have defined authentication, the how part, but haven’t told next-auth “what” to protect. This is where middleware comes in!

Add file called middleware.ts to the project root

export { default } from 'next-auth/middleware';
 
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     * - login
     * - reset
     */
    '/((?!_next/static|_next/image|favicon.ico|login).*)',
  ],
};
export { default } from 'next-auth/middleware';
 
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     * - login
     * - reset
     */
    '/((?!_next/static|_next/image|favicon.ico|login).*)',
  ],
};

We are asking next-auth to ensure that all routes are protected with authentication except for those listed above. In particular, login route is accessible anonymously!

Login

We are now finally we are ready to start authenticating users. Lets add the login functionality. Add login folder to app dir, and add page.tsx as follows

import * as React from 'react';
import { LoginForm } from './_components/login-form';
 
export default function LoginPage() {
  return <LoginForm />;
}
import * as React from 'react';
import { LoginForm } from './_components/login-form';
 
export default function LoginPage() {
  return <LoginForm />;
}

Next create app/login/_components/login-form.tsx and copy following

"use client";
import { useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { signIn } from "next-auth/react";
 
export function LoginForm() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const redirectTo = searchParams.get("redirectTo") || "/";
  const [errors, setErrors] = useState<{ [key: string]: string | undefined }>({});
 
  async function handleAction(formData: FormData) {
    const result = await signIn("credentials", {
      email: formData.get("email"),
      password: formData.get("password"),
      redirect: false,
      callbackUrl: searchParams?.get("from") || "/",
    });
    if (result?.ok) {
      router.replace(result.url ?? "/");
    } else if (result?.error) {
      setErrors({ other: result?.error });
    }
  }
  //
  return (
    <div className="flex min-h-screen flex-col justify-center">
      <div className="mx-auto w-full max-w-md px-8">
        <form className="space-y-6" action={handleAction}>
          <fieldset>
            <input type="hidden" name="redirectTo" value={redirectTo} />
            <div className="flex flex-col gap-4 items-center">
              {errors?.other ? <div>{errors.other}</div> : null}
              <label className=" w-full flex gap-4 items-center">
                <span className=" w-20 text-slate-950 dark:text-slate-100">
                  Email
                </span>
                <input
                  name="email"
                  placeholder="Enter email address"
                  required={true}
                  autoFocus={true}
                  type="email"
                  autoComplete="email"
                  aria-describedby="email-error"
                  className=" bg-transparent border border-slate-200 dark:border-slate-900 p-2 rounded-md"
                />
              </label>
 
              <label className="w-full flex gap-4 items-center">
                <span className="w-20 text-slate-950 dark:text-slate-100">
                  Password
                </span>
                <input
                  name="password"
                  type="password"
                  placeholder="Enter password"
                  required={true}
                  autoComplete="current-password"
                  aria-describedby="password-error"
                  className=" bg-transparent border border-slate-200 dark:border-slate-900 p-2 rounded-md"
                />
              </label>
 
              <button
                type="submit"
                aria-label="Login"
                name="_method"
                value="post"
                className=" w-48 bg-sky-500 text-sky-100 p-2 rounded-md"
              >
                Login
              </button>
            </div>
          </fieldset>
        </form>
      </div>
    </div>
  );
}
"use client";
import { useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { signIn } from "next-auth/react";
 
export function LoginForm() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const redirectTo = searchParams.get("redirectTo") || "/";
  const [errors, setErrors] = useState<{ [key: string]: string | undefined }>({});
 
  async function handleAction(formData: FormData) {
    const result = await signIn("credentials", {
      email: formData.get("email"),
      password: formData.get("password"),
      redirect: false,
      callbackUrl: searchParams?.get("from") || "/",
    });
    if (result?.ok) {
      router.replace(result.url ?? "/");
    } else if (result?.error) {
      setErrors({ other: result?.error });
    }
  }
  //
  return (
    <div className="flex min-h-screen flex-col justify-center">
      <div className="mx-auto w-full max-w-md px-8">
        <form className="space-y-6" action={handleAction}>
          <fieldset>
            <input type="hidden" name="redirectTo" value={redirectTo} />
            <div className="flex flex-col gap-4 items-center">
              {errors?.other ? <div>{errors.other}</div> : null}
              <label className=" w-full flex gap-4 items-center">
                <span className=" w-20 text-slate-950 dark:text-slate-100">
                  Email
                </span>
                <input
                  name="email"
                  placeholder="Enter email address"
                  required={true}
                  autoFocus={true}
                  type="email"
                  autoComplete="email"
                  aria-describedby="email-error"
                  className=" bg-transparent border border-slate-200 dark:border-slate-900 p-2 rounded-md"
                />
              </label>
 
              <label className="w-full flex gap-4 items-center">
                <span className="w-20 text-slate-950 dark:text-slate-100">
                  Password
                </span>
                <input
                  name="password"
                  type="password"
                  placeholder="Enter password"
                  required={true}
                  autoComplete="current-password"
                  aria-describedby="password-error"
                  className=" bg-transparent border border-slate-200 dark:border-slate-900 p-2 rounded-md"
                />
              </label>
 
              <button
                type="submit"
                aria-label="Login"
                name="_method"
                value="post"
                className=" w-48 bg-sky-500 text-sky-100 p-2 rounded-md"
              >
                Login
              </button>
            </div>
          </fieldset>
        </form>
      </div>
    </div>
  );
}

It is a standard login form with a submit action handler. It is in this submit handler that we are calling next-auth signIn method and passing in user credentials. Based on results of this method, we either redirect user to main app or display an error.

If you now run the project npm run dev, you should be able to login.

Protected Routes

Only authenticated users can access protected routes. While Next-Auth config defined above already ensures that, we can add extra layer of protection by ensuring that user is logged in and available.

Add app/session.server.ts file and copy following

import { redirect } from 'next/navigation';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/app/auth';
 
export async function getSession() {
  return await getServerSession(authOptions);
}
 
export async function getCurrentUser() {
  const session = await getSession();
  if (!session) return undefined;
  //
  return { ...session?.user };
}
export async function requireUser(redirectTo?: string) {
  const user = await getCurrentUser();
  if (user) return user;
  let loginUrl = '/login';
  if (redirectTo) {
    const searchParams = new URLSearchParams([['redirectTo', redirectTo]]);
    loginUrl = `/login?${searchParams}`;
  }
  redirect(loginUrl);
}
import { redirect } from 'next/navigation';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/app/auth';
 
export async function getSession() {
  return await getServerSession(authOptions);
}
 
export async function getCurrentUser() {
  const session = await getSession();
  if (!session) return undefined;
  //
  return { ...session?.user };
}
export async function requireUser(redirectTo?: string) {
  const user = await getCurrentUser();
  if (user) return user;
  let loginUrl = '/login';
  if (redirectTo) {
    const searchParams = new URLSearchParams([['redirectTo', redirectTo]]);
    loginUrl = `/login?${searchParams}`;
  }
  redirect(loginUrl);
}

Let’s add a protected route. Add (protected) folder and add following layout.tsx to it

import 'server-only';
 
import { requireUser } from '@/app/session.server';
 
export default async function PrivateLayout({
  children,
}: {
  children: React.ReactNode,
}) {
  // auth
  const user = await requireUser();
  //
  return (
    <div>
      <h1>Protected Layout</h1>
      <div>
        <p>Hello {user.email}</p>
      </div>
      <div>{children}</div>
      <div>
        <p>Footer</p>
      </div>
    </div>
  );
}
import 'server-only';
 
import { requireUser } from '@/app/session.server';
 
export default async function PrivateLayout({
  children,
}: {
  children: React.ReactNode,
}) {
  // auth
  const user = await requireUser();
  //
  return (
    <div>
      <h1>Protected Layout</h1>
      <div>
        <p>Hello {user.email}</p>
      </div>
      <div>{children}</div>
      <div>
        <p>Footer</p>
      </div>
    </div>
  );
}

requireUser ensures that user is logged in else it redirects use to ‘/login’ page. And finally add app/(protected)/test/page.tsx and copy following

export default function HomePage() {
  return (
    <div>
      <h1>Home Page</h1>
    </div>
  );
}
export default function HomePage() {
  return (
    <div>
      <h1>Home Page</h1>
    </div>
  );
}

Run the app rpm run dev and open localhost:3000 in a private browser. This should redirect you to login. Login with example@example.com and Example!234. After successful login, if you browse localhost:3000/test you should be at Home page. If you refresh browser, you should still see HomePage due to saved login session cookie.

The (protected) folder with layout.tsx requiring user is not strictly necessary since we are already protecting entire site with our next-auth config. However, it is nice to co-locate all pages that required authenticated user in one place and gives you option to change next-auth and auth flow down the line.

Next Steps

By now, you should have a good grasp of how to authenticate and protect routes in NextJs App Router. While we explored Next-Auth credentials provider, you can implement additional providers like social login by following same process and changing provider in next-auth (auth.ts)

If you will like to check out code that authenticates against a database, please create a FREE starter app using https://createappai.com/ and download the app codebase.

Find Source code with a ready to run project @ GitHub

CreateAppAI

CreateAppAI provides authentication against database using custom table, user sign-up, password reset/recovery and fine-grained authorization (RBAC), as well as Admin Panel to create roles and rights!