Recently, I migrated a frontend application from an older version of Next.js to Next.js 14. During this process, I decided to upgrade the session handling approach. After extensive research, I chose to use NextAuth.js, now known as Auth.js.
In the application, server-side authentication with AWS Cognito is utilized, and I wanted to maintain this setup. The goal was to seamlessly integrate authentication tokens and user data retrieved from successful authentications into Auth.js, ensuring session availability across the entire app and enabling authorization enforcement for different pages.
In this article, I'll guide you through the process of successfully integrating Auth.js with your Next.js application using the app router. We will cover both the credentials (email and password) approach and Google social login.
Note: Next-Auth v5 is still in beta mode, so I wouldn't fully recommend integrating it into a production application just yet. However, the final decision is up to you.
Prerequisites
- Familiarity with the basics of React.js
- Brief experience with Next.js app router
- Basic understanding of Typescript, as we'll be using Next.js with TypeScript
You don't need to have prior experience with Next-Auth.js (now Auth.js) to follow this article.
Note: For the rest of the article, I’ll be using next-auth
and Auth.js
interchangeably.
Let's get started!!
1. Create your Next.js application
Start by generating a new Next.js application:
npx create-next-app@latest
After running the command above, follow the prompts, accepting the defaults for TypeScript and Tailwind CSS as we'll be using TypeScript and will apply minor styling with Tailwind. Also choose Yes
for the import alias and accept the default alias configuration.
2. Install Auth.js
Install the latest version of auth.js using npm or yarn
npm install next-auth@beta
or
yarn add next-auth@beta
3. Generate and Configure Your Environment Variables
Create a .env.local file and add your Auth.js secret. Auth.js uses this for encryption of your JWTs and cookies.
touch .env.local
Generate the secret string by running any of the two commands:
# Linux/Mac terminal
openssl rand -base64 33
# Alternatively, use `auth` to generate the secret
npx auth secret
Add the generated secret to your .env.local
file:
AUTH_SECRET="your-secret"
You can escape running into major issues when you don't set this in your development environment. But it is compulsory to set it in the production environment.
You should also add your backend URL here
NEXT_PUBLIC_BASE_URL="your-backend-url"
4. Create your interface and types.
To manage your types effectively, create a new directory named types
in your root directory (or in your /src directory if you are using it), then inside it, add two new files: login.ts
and user.ts
.
├── app
├── public
├── types
│ ├── login.ts
│ ├── user.ts
├── .env.local
└── ...
Your app directory should look something like this with some other files and folders which are not shown here.
login.ts
Add the following types for different authentication scenarios:
type CredentialsType = {
username: string;
password: string;
};
type SocialCredentialsType = {
auth_code: string;
};
export type { CredentialsType, SocialCredentialsType };
CredentialsType
: Defines the structure for email and password login credentials.
SocialCredentialsType
: Defines the structure for social login credentials, specifically for Google authentication.
user.ts
Define interface and type for user data to manage session details:
interface UserType {
id: string;
name: string;
email: string;
avatar: string;
premiumSubscription: boolean;
accessToken: string;
refreshToken: string;
subId: string;
}
type UserResponseType = {
id: string;
name: string;
email: string;
avatar: string;
premium_subscription: boolean;
access_token: string;
refresh_token: string;
sub_id: string;
};
export type { UserType, UserResponseType };
-
UserType
: Interface specifying the expected properties for user session management. -
UserResponseType
: Type specifying the format of user data as received from the backend.
5. Create your Auth.js configuration.
Create a new file named auth.ts
in your root directory. If you are using the src/
directory approach, place the file directly within the src/
folder.
├── app
├── public
├── types
│ ├── login.ts
│ ├── user.ts
├── .env.local
├── auth.ts
└── ...
auth.ts
At the top of the file import the necessary libraries and types for
configuring Auth.js.
// library imports
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
// types imports
import type { NextAuthConfig, Session, User } from "next-auth";
import type { UserType, UserResponseType } from "@/types/user";
import { AdapterUser } from "next-auth/adapters";
import { CredentialsType, SocialCredentialsType } from "@/types/login";
import { JWT } from "next-auth/jwt";
Next, we have to modify some of the types in the next-auth library with some of our own properties.
declare module "next-auth" {
interface User extends UserType {}
}
declare module "next-auth/adapters" {
interface AdapterUser extends UserType {}
}
declare module "next-auth/jwt" {
interface JWT extends UserType {}
}
Here, we used the TypeScript module augmentation feature to extend the User
, AdapterUser
, JWT
interfaces from NextAuth with properties defined in UserType
from our user.ts
file. This ensures that our custom user properties are recognized throughout the Auth.js configuration.
Next we declare authOptions
object which we'll use to initialize NextAuth
at the end.
const authOptions = {
providers: [
// Add authentication providers here (e.g., CredentialsProvider, GoogleProvider)
],
callbacks: {
// Add custom authentication callbacks here (e.g., signIn, signOut, jwt, session)
},
pages: {
// Customize authentication-related pages (e.g., signIn, error)
},
session: {
// Configure session options (e.g., JWT settings, session management)
},
} satisfies NextAuthConfig;
export const { handlers, auth, signIn, signOut } = NextAuth(authOptions);
A.providers: accepts an array of providers such as CredentialsProvider, GoogleProvider, e.t.c.
B.callbacks: specifies custom callback functions that can be triggered at various points in the authentication process.
C.pages: auth.js has default authentication-related pages for signIn, error, signOut e.t.c. But in this block , we can override it with our customized pages by specifying their paths.
D.session: Configures session options, such as how sessions are handled and stored. e.g jwt or database approach
- Finally, we initialize
NextAuth
with theauthOptions
configuration and destructure the returned methods (handlers, auth, signIn, signOut) for use in our application.-
handlers
: Middleware for handling NextAuth requests. -
auth
: A universal method to interact with Auth.js in your Next.js app. -
signIn
: Method to trigger sign-in. -
signOut
: Method to trigger sign-out.
-
The code in the auth.ts
file should look like this now
// library imports
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
// types imports
import type { NextAuthConfig, Session, User } from "next-auth";
import type { UserType, UserResponseType } from "@/types/user";
import { AdapterUser } from "next-auth/adapters";
import { CredentialsType, SocialCredentialsType } from "@/types/login";
import { JWT } from "next-auth/jwt";
declare module "next-auth" {
interface User extends UserType {}
}
declare module "next-auth/adapters" {
interface AdapterUser extends UserType {}
}
declare module "next-auth/jwt" {
interface JWT extends UserType {}
}
const authOptions = {
providers: [
],
callbacks: {
},
pages: {
},
session: {
},
} satisfies NextAuthConfig;
export const { handlers, auth, signIn, signOut } = NextAuth(authOptions);
Now, let's complete the authOptions
object with the necessary values.
A. providers:
We will use CredentialsProvider
for handling authentication on the server side by making requests to our server URL. We will set it up twice: once for email and password login, and once for social login with Google. To distinguish between the two, we specify unique id values.
providers: [
CredentialsProvider({
id: "credentials",
name: "Credentials",
authorize: async (credentials) => {
try {
const user = await fetchUser(
`${process.env.NEXT_PUBLIC_BASE_URL}/auth/login`,
{
username:
typeof credentials.username === "string"
? credentials.username
: "",
password:
typeof credentials.password === "string"
? credentials.password
: "",
}
);
return user ? createUser(user) : null;
} catch (error) {
console.error("Error during authentication", error);
return null;
}
},
}),
CredentialsProvider({
id: "social",
name: "Custom Social Login",
authorize: async (credentials) => {
try {
const user = await fetchUser(
`${process.env.NEXT_PUBLIC_BASE_URL}/auth/social_login`,
{
auth_code:
typeof credentials.authCode === "string"
? credentials.authCode
: "",
}
);
return user ? createUser(user) : null;
} catch (error) {
console.error("Error during authentication", error);
return null;
}
},
}),
],
-
Email and Password Login
:- The first
CredentialsProvider
is configured for email and password login. - It makes a request to
${process.env.NEXT_PUBLIC_BASE_URL}/auth/login
with the provided credentials via the fetchUser function(replace this URL with your backend URL). - If a user is found,
createUser(user)
is called to format the user data.
- The first
-
Social Login with Google
:- The second
CredentialsProvider
is configured for social login using Google. - It makes a request to
${process.env.NEXT_PUBLIC_BASE_URL}/auth/social_login
with the provided auth code via thefetchUser
function(replace this URL with your backend URL). - If a user is found,
createUser(user)
is called to format the user data.
- The second
Each provider has a unique id
to distinguish between the two methods of authentication.
By using CredentialsProvider
twice with different id
values, we can manage multiple authentication methods seamlessly.
Here are the definitions of the fetchUser
and createUser
functions:
// Function to authenticate and fetch user details
async function fetchUser(
url: string,
body: CredentialsType | SocialCredentialsType
) {
try {
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const user = await res.json();
if (res.ok && user) {
return user;
} else {
console.error(`Failed to fetch user: ${res.status} ${res.statusText}`);
return null;
}
} catch (error) {
console.error(`Error during fetch: ${error}`);
return null;
}
}
// Function to create a user object
function createUser(user: UserResponseType) {
const userObject: UserType = {
id: user.id,
name: user.name,
email: user.email,
avatar: user.avatar,
premiumSubscription: user.premium_subscription,
accessToken: user.access_token,
refreshToken: "", //add subId from the auth service here
subId: "", // add refresh token here
};
return userObject;
}
B. callbacks:
The callbacks
section allows us to customize the behavior of the authentication process. Here, we’ll add two async functions: jwt
and session
.
callbacks: {
async jwt({ token, user }: { token: JWT; user: User }) {
// Add the user properties to the token after signing in
if (user) {
token.id = user.id as string;
token.avatar = user.avatar;
token.name = user.name;
token.email = user.email;
token.premiumSubscription = user.premiumSubscription;
token.accessToken = user.accessToken;
token.subId = user.subId;
token.refreshToken = user.refreshToken;
}
return token;
},
async session({ session, token }: { session: Session; token: JWT }) {
// Create a user object with token properties
const userObject: AdapterUser = {
id: token.id,
avatar: token.avatar,
name: token.name,
premiumSubscription: token.premiumSubscription,
accessToken: token.accessToken,
subId: token.subId,
refreshToken: token.refreshToken,
email: token.email ? token.email : "", // Ensure email is not undefined
emailVerified: null, // Required property, set to null if not used
};
// Add the user object to the session
session.user = userObject;
return session;
},
},
-
jwt callback
:- The jwt function is called during the authentication process to add user properties to the JWT token. When a user signs in, their properties, such as id, avatar, name, email, accessToken, subId, and refreshToken, are added to the token. This token is then used to securely manage user sessions.
-
session callback
:- The session function is called whenever a session is accessed to add user data to the session object. A user object is created using the properties from the token. This user object is then added to the session, ensuring that user data is available whenever the session is accessed. The email property is ensured to be a string, and emailVerified is set to null if not used.
C. pages:
In the pages
section of the authOptions
object, we define paths to our custom pages that will override the default authentication-related pages provided by Auth.js. This allows us to customize the user experience for specific authentication actions.
pages: {
signIn: "/auth/login", // Custom sign-in page
// error: "/auth/error", // Custom error page
},
-
signIn
: Specifies the path to a custom sign-in page. By setting this to/auth/login
, we direct users to our custom login page instead of the default Auth.js sign-in page. -
error
: Optionally, we can specify a custom error page. This is commented out here, but if we wanted to use a custom error page, we could set its path like so: error:/auth/error
.
D. session:
In the session section of the authOptions
object, we specify how sessions should be managed. In this case, we are using JSON Web Tokens (JWT) as the session strategy.
session: {
strategy: "jwt",
},
-
strategy
: By setting this to "jwt", we ensure that sessions are managed using JSON Web Tokens. This means that session data is encoded and stored in a JWT, which is then sent to the client.
Here is what the entire file should look like now
// library imports
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
// types imports
import type { NextAuthConfig, Session, User } from "next-auth";
import type { UserType, UserResponseType } from "@/types/user";
import { AdapterUser } from "next-auth/adapters";
import { CredentialsType, SocialCredentialsType } from "@/types/login";
import { JWT } from "next-auth/jwt";
// Modify NextAuth types with custom properties
declare module "next-auth" {
interface User extends UserType {}
}
declare module "next-auth/adapters" {
interface AdapterUser extends UserType {}
}
declare module "next-auth/jwt" {
interface JWT extends UserType {}
}
const authOptions = {
providers: [
CredentialsProvider({
id: "credentials",
name: "Credentials",
authorize: async (credentials) => {
try {
const user = await fetchUser(
`${process.env.NEXT_PUBLIC_BASE_URL}/auth/login`,
{
username:
typeof credentials.username === "string"
? credentials.username
: "",
password:
typeof credentials.password === "string"
? credentials.password
: "",
}
);
return user ? createUser(user) : null;
} catch (error) {
console.error("Error during authentication", error);
return null;
}
},
}),
CredentialsProvider({
id: "social",
name: "Custom Social Login",
authorize: async (credentials) => {
try {
const user = await fetchUser(
`${process.env.NEXT_PUBLIC_BASE_URL}/auth/social_login`,
{
auth_code:
typeof credentials.authCode === "string"
? credentials.authCode
: "",
}
);
return user ? createUser(user) : null;
} catch (error) {
console.error("Error during authentication", error);
return null;
}
},
}),
],
callbacks: {
async jwt({ token, user }: { token: JWT; user: User }) {
// Add the user properties to the token after signing in
if (user) {
token.id = user.id as string;
token.avatar = user.avatar;
token.name = user.name;
token.email = user.email;
token.premiumSubscription = user.premiumSubscription;
token.accessToken = user.accessToken;
token.subId = user.subId;
token.refreshToken = user.refreshToken;
}
return token;
},
async session({ session, token }: { session: Session; token: JWT }) {
// Create a user object with token properties
const userObject: AdapterUser = {
id: token.id,
avatar: token.avatar,
name: token.name,
premiumSubscription: token.premiumSubscription,
accessToken: token.accessToken,
subId: token.subId,
refreshToken: token.refreshToken,
email: token.email ? token.email : "", // Ensure email is not undefined
emailVerified: null, // Required property, set to null if not used
};
// Add the user object to the session
session.user = userObject;
return session;
},
},
pages: {
signIn: "/auth/login", // Custom sign-in page
// error: "/auth/error", // Custom error page
},
session: {
strategy: "jwt",
},
} satisfies NextAuthConfig;
// Function to authenticate and fetch user details
async function fetchUser(
url: string,
body: CredentialsType | SocialCredentialsType
) {
try {
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const user = await res.json();
if (res.ok && user) {
return user;
} else {
console.error(`Failed to fetch user: ${res.status} ${res.statusText}`);
return null;
}
} catch (error) {
console.error(`Error during fetch: ${error}`);
return null;
}
}
// Function to create a user object
function createUser(user: UserResponseType) {
const userObject: UserType = {
id: user.id,
name: user.name,
email: user.email,
avatar: user.avatar,
premiumSubscription: user.premium_subscription,
accessToken: user.access_token,
refreshToken: "", //add subId from the auth service here
subId: "", // add refresh token here
};
return userObject;
}
export const { handlers, auth, signIn, signOut } = NextAuth(authOptions);
6. Utilize the configuration in the Next.js API route.
To integrate the NextAuth
configuration into a Next.js API route, follow these steps:
Create a new file named route.ts
in the app directory at the following path:
├── app
│ ├── api
│ │ ├── auth
│ │ │ ├── [...nextauth]
│ │ │ │ ├── route.ts
├── auth.ts
└── ...
In the route.ts
file, add the following lines of code:
// library imports
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
At the top, we import the handlers
object from auth.ts
, then destructure it to extract the GET
and POST
methods and exports them. When a request is made to the /api/auth
endpoint, the route.ts
file delegates the request to the appropriate handler (GET
or POST
) provided by NextAuth. These handlers manage the authentication logic, session management, and other related tasks based on the configuration we defined earlier in auth.ts
.
We are done with setting up Auth.js
. Now let's use the magic we created and see it in action
7. Using Auth.js in our Next.js Application
Now that we have completed the setup for Auth.js
, it's time to see it in action. We'll integrate authentication into our application by creating email and password sign-in, social login and protected pages.
A. Create the authentication components
First, create a components directory in the root directory of your app (in the src/
directory if you are using this approach). Then add auth
and protected
directories in it.
- Create a new file named
login.tsx
in thecomponents/auth
directory and a new file namedprofile.tsx
in thecomponents/profile
directory:
├── app
├── components
│ ├── auth
│ │ ├── login.tsx
│ ├── profile
│ │ ├── profile.tsx
-
login.tsx
: Here we will add the following code to create a sign-in form and a google login button.
"use client";
// library imports
import React, { useState, useEffect } from "react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
export default function SignIn() {
const searchParams = useSearchParams();
const googleLogin = "Your google login url which will contain your client id and redirectURI";
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [authenticated, setAuthenticated] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
if (authenticated) {
// Redirect to previous page or home page
const next = searchParams.get("next") || "/";
window.location.href = next;
}
}, [authenticated]);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const res = await fetch("/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password, type: "credentials" }),
});
if (res.ok) {
setAuthenticated(true);
} else {
// handle error state here
setError("Invalid credentials");
}
} catch (error) {
// handle error state here
console.error("Error during sign-in", error);
setError("Internal server error");
}
};
return (
<div className="mx-auto w-[200px] h-full border-red-100">
<div>
<p className="text-xl w-full flex justify-center mt-3 mb-5">Sign In</p>
<form onSubmit={handleSubmit}>
<label>
Username:
<input
type="text"
className="w-full rounded-sm"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</label>
<label>
Password:
<input
className="w-full rounded-sm"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</label>
<button
className="w-full flex justify-center bg-teal-500 text-white mt-3 rounded-md"
type="submit"
>
Sign In
</button>
{error && <p style={{ color: "red" }}>{error}</p>}
</form>
</div>
<div className="my-2">
<div className="flex justify-center"> or </div>
</div>
<div className="w-full bg-red-700 rounded-md mb-2">
<Link href={googleLogin} className="flex ">
<p className="w-full text-white flex justify-center">
Sign in with Google
</p>
</Link>
</div>
</div>
);
}
We'll send a POST request to an API route (/api/login) to handle authentication. If the login is successful, the user will be redirected to the next page or home page. If the Sign in with Google
button is clicked, the user will be redirected to the google-login
page
Next, we will create the /api/login
endpoint to handle the authentication logic on the server side. Create a new folder named login
in the app/api
directory and create a route.ts
file in it.
├── app
│ ├── api
│ │ ├── auth
│ │ ├── login
│ │ │ ├── route.ts
├── components
├── auth.ts
└── ...
-
route.ts
:
// library imports
import { NextResponse, NextRequest } from "next/server";
// internal imports
import { signIn } from "@/auth";
export async function POST(req: NextRequest, res: NextResponse) {
const data = await req.json();
const { username, password, type } = data;
try {
const result =
type === "credentials"
? await signIn("credentials", { redirect: false, username, password })
: await signIn("social", { redirect: false, authCode: username });
// handle the result of the sign-in attempt
if (!result || result.error) {
return NextResponse.json({ error: "Invalid credentials" });
} else {
return NextResponse.json({ success: true });
}
} catch (error) {
console.error("Error during sign-in", error);
return NextResponse.error();
}
}
Here, we handle authentication for both credentials and social login types. Based on the type
of login, we call the signIn function with either credentials (username and password
) or social login (auth_code
).
Next, we will create the social login page. Create a new file named socialAuth.tsx
in the components/auth
directory.
├── app
├── components
│ ├── auth
│ │ ├── login.tsx
│ │ ├── socialAuth.tsx
│ ├── profile
-
socialAuth.tsx
: Add the following code to socialAuth.tsx.
"use client";
// library imports
import React, { useState, useEffect } from "react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
export default function SocialAuth() {
const googleLogin =
"Your google login url which will contain your client id and redirectURI";
const router = useRouter();
const searchParams = useSearchParams();
const [authSuccess, setAuthSuccess] = useState(true);
const [tokenStatus, setTokenStatus] = useState(false);
useEffect(() => {
// check query string for authentication code
if (authSuccess || tokenStatus) {
const url = window.location.href;
const code = url.match(/\?code=(.*)/);
if (!tokenStatus && authSuccess) {
if (code) {
authenticateUser(code[1]);
} else {
router.push("/auth/login");
}
} else if (tokenStatus) {
// Redirect to previous page or home page
const next = searchParams.get("next") || "/";
router.push(next);
} else {
router.push("/auth/login");
}
}
}, [tokenStatus, authSuccess]);
const authenticateUser = async (code: string) => {
try {
const res = await fetch("/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username: code, type: "social" }),
});
if (res.ok) {
setTokenStatus(true);
} else {
// handle error state here
setAuthSuccess(false);
}
} catch (error) {
// handle error state here
setAuthSuccess(false);
}
};
return (
<>
<div>
{authSuccess ? (
<div>
<h1>Authenticating...</h1>
</div>
) : (
<div>
<h1>
{" "}
An error occurred while attempting to authenticate your account
with Google{" "}
</h1>
<div>
<div>
<Link href={googleLogin}>Please try again</Link>
</div>
</div>
</div>
)}
</div>
</>
);
}
This ensures that users can authenticate via social login providers like Google and handles the authentication flow smoothly.
B. Create the Authentication Pages
Next, let's create the custom sign-in
and social login
pages that utilize the components we created above.
Create a new directory called auth
in the app/
directory. Inside auth
, create folders named login
and social
, and then add a page.tsx
file in both folders.
├── app
│ ├── auth
│ │ ├── login
│ │ │ ├── page.tsx
│ │ ├── social
│ │ │ ├── page.tsx
- In
login/page.tsx
, add the following code:
//component imports
import SignIn from "@/components/auth/login";
export default function LoginPage() {
return <SignIn />;
}
- In
social/page.tsx
, add the following code:
//component imports
import SocialAuth from "@/components/auth/socialAuth";
export default function SocialAuthPage() {
return <SocialAuth />;
}
We are now set with authentication, and you can confirm the state of what we have done so far.
- Attempt to navigate to this url
http://localhost:3000/api/auth/signin
. You should be redirected back to the custom sign-in page. This redirection occurs because of the URL path we specified in thepages
section of ourauthOptions
object while setting up the configuration inauth.ts
.
B. Create protected page
The final step is to create a protected page that users cannot access unless they are authenticated.
-profile.tsx
Let's add the code below to our empty file profile.tsx
in the components directory.
"use client";
// library imports
import React, { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { useRouter, usePathname } from "next/navigation";
export default function Profile() {
const router = useRouter();
const pathname = usePathname();
const { data: session } = useSession();
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;
const [loadingProfile, setLoadingProfile] = useState(false);
useEffect(() => {
if (session?.user?.accessToken) {
// fetch user profile if access token is available
getUserProfile(session.user.accessToken);
} else {
// Redirect to `/login` if no access token or no session
router.push("/auth/login?next=" + pathname);
}
}, []);
const getUserProfile = (token: string) => {
setLoadingProfile(true);
fetch(`${baseUrl}/auth/user_profile`, {
headers: { Authorization: "Bearer " + token },
})
.then((response) => {
setLoadingProfile(false);
})
.catch((error) => {
// handle error here
console.error(error);
});
};
return (
<>
{" "}
{loadingProfile ? (
<p>Loading...</p>
) : (
<div>
<p>User Profile</p>
<p>Name: {session?.user?.name}</p>
<p>Email: {session?.user?.email}</p>
</div>
)}
</>
);
}
Here we use the useSession
hook to check if a user session with an access token exists. If the token is present , we go ahead and make a call to the backend route /auth/user_profile
. In my case i have a protected route in the backend which an unauthenticated user cannot access without a token. While waiting for the profile data to load, a loading indicator is displayed. Once loaded, the component displays the user's name and email. If there is no active session or access token, it redirects the user back to the login page to authenticate.
Finally, we create the page which utilizes the profile.tsx
component above. Create a new folder in the app directory named profile
and in it a page.tsx
file.
-
page.tsx
:
// library imports
import { SessionProvider } from "next-auth/react";
// internal imports
import { auth } from "@/auth";
//component imports
import Profile from "@/components/profile/profile";
export default async function ProfilePage() {
const session = await auth();
return (
<SessionProvider session={session}>
<Profile />
</SessionProvider>
);
}
First we import the SessionProvider
component from next-auth/react
to serve as a context provider for managing the user session state throughout the application. Additionally, we import the auth
method from auth.js
. Since this operation requires asynchronous behavior, we utilize a server-side component, using await
to fetch the session using the auth
method. Subsequently, we pass this session object obtained from auth to the SessionProvider
component which acts as a wrapper around the Profile
component. We have to wrap the component with Session Provider here if we want the session to be accessible within profile.tsx
using the useSession
hook, similar to how we handled authentication state above.
Now if we try to access the /profile
route, we would get redirected to the /auth/login
page since we are not logged in. Once we login, we would be redirected back to the profile page, where we can see our user data.
Congratulations , you have successfully integrated your next js application using the app router with auth.js v5.
You might have noticed that in the profile.tsx component, we check the session for a token and then redirect the user back to the login page if the token is not present. This means that Next.js already starts rendering our page before redirecting to the login page if the token is absent. If you pay attention, you'll also notice this in the UI.
An alternative to this approach is to use middleware to handle authentication. Next.js has a middleware feature that is triggered when a user attempts to access a route. We can use this to protect our authenticated routes. This way, the protected page is not rendered if an unauthenticated user attempts to access it, as the middleware intercepts the request and redirects the user to the login route. This ensures we have a central location to check the session and redirect users if they are unauthenticated, thereby improving the user experience with the UI.
8. Adding a Middleware
Create a new file called middleware.ts
in your root directory. If you are using the src/
directory approach, place the file directly within the src/
folder.
├── app
├── public
├── types
├── .env.local
├── auth.ts
├── middleware.ts
└── ...
-middleware.ts
In the file add the following lines of code:
import { NextResponse } from "next/server";
import { auth } from "@/auth";
export default auth((req) => {
const currentPath = req.nextUrl.pathname;
// Redirect to login page if user is not authenticated
if (!req.auth) {
return NextResponse.redirect(
new URL(`/auth/login?next=${currentPath}`, req.url)
);
}
});
// Manage list of protected routes
export const config = {
matcher: ["/profile/:path*", "/another-protected-route/:path*"],
};
This middleware checks if the user is authenticated before allowing access to specified routes. If the user is not authenticated, they are redirected to the login page. The config object defines the routes that should trigger this middleware, ensuring that only protected routes are affected.
By setting up this middleware, you centralize the authentication logic, making your codebase cleaner and more maintainable as your application grows.
Note
: In my application, I have several authenticated and unauthenticated routes, which is why I am not triggering the middleware with all requests and instead maintaining a protected routes list. The little downside to this is that you have to manage a list of all your protected routes. This might end up becoming a long list and could become difficult to manage, or someone might implement a protected page and forget to update the middleware. If your base path is the only unprotected route, your job will be much easier.
It's been a long ride, but we have finally come to the end of this tutorial. With what you have learned, I believe you can seamlessly integrate Auth.js v5
into your `next.js application.
Here is the link to the github repo for this project. Cheers!!!
If you have any questions, feel free to drop them as a comment or send me a message on LinkedIn and I'll ensure I respond as quickly as I can. Ciao 👋
Top comments (0)