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.
Content
- Stater Project
- Authentication Initialization
- Authentication Configuration
- What To Protect
- Login
- Protected Routes
- Next Steps
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!