Strapi Email and Password Authentication with Next.js 15: Part 2
Introduction In the previous Part of this tutorial, you implemented Strapi email and password registration and login using the SendGrid email provider. You also learned how to perform Strapi email verification upon user registration, sending and resending confirmation email. With Next.js, you were able to create requests, server actions and handle form submissions. In this final Part of the Strapi email and password authentication tutorial, we will go further by implementing forgot password, password reset, user logout, and changing password. And in the Next.js frontend, we will implement authentication with session management, secure data access layer, and Middleware. Tutorial Series This tutorial is divided into two parts. Part 1 - Email and Password registration, Email confirmation, and Login Part 2 - Session Management, Data Access, password reset, and changing password GitHub Repository: Full Code for Strapi and Next.js Authentication Project The complete code for this project can be found in this repo: strapi-email-and-password-authentication Handling Session, Protecting Routes and Pages and, Data Access Layer in Next.js So far, the profile page is not protected from unauthenticated users. And if you look at the navigation bar, the "Sign-in" button remains the same instead of "Sign-out" for the user to log out. Also, the profile page should welcome the user by their username and not with the generic name "John Doe". Thus, you need to protect pages, track user authentication state, and secure data access. This is what we will do: Create session management to track user authentication state. Set up a middleware for public and protected routes. Create a Data Access Layer (DAL) to securely access user data. Step 1: Create a Session To Track User Authentication State in Next.js Recall that when a user logs in, Strapi returns a response that contains a JSON Web Token (JWT) as shown below. { "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTUsImlhdCI6MTc0NTM1MzMzMSwiZXhwIjoxNzQ3OTQ1MzMxfQ.zvx2Q2OexHIPkNA5aCqaOG3Axn0rlylLOpgiVPifi8c", "user": { "id": 15, "documentId": "npbi8dusjdsdwu5a0zq6ticv", "username": "Theodore", "email": "strapiUser@gmail.com", "provider": "local", "confirmed": true, "blocked": false, "createdAt": "2025-04-22T18:18:01.170Z", "updatedAt": "2025-04-22T19:04:51.091Z", "publishedAt": "2025-04-22T18:18:01.172Z" } } The JWT issued by Strapi is useful because it needs to be included in subsequent requests to Strapi. There are different ways to store the JWT, but in this tutorial, we will use Stateless Sessions that can be implemented using Next.js. First, create a session secret. Create Session Secret Generate a session secret by using the openssl command in your terminal which generates a 32-character random string that you can use as your session secret and store in your environment variables file: openssl rand -base64 32 Add Session Secret to Environment Variable # Path: ./.env # ... other environment variables STRAPI_ENDPOINT="http://localhost:1337" SESSION_SECRET=YOUR_SESSION_SECRET Encrypt and Decrypt Sessions In the previous part of this tutorial, we installed jose package which provides signing and encryption, and which provides support for JSON Web Tokens (JWT). Use jose to do the following: Encrypt and decrypt the JWT from Strapi. Create a session by storing the signed token in an httpOnly cookie named "session" with an expiration time., Delete the session by removing the session cookie to enable users to log out. Inside the nextjs-frontend/src/app/lib folder, create a new file session.ts: // Path: nextjs-frontend/src/app/auth/confirm-email/page.tsx import "server-only"; import { SignJWT, jwtVerify } from "jose"; import { cookies } from "next/headers"; import { SessionPayload } from "@/app/lib/definitions"; // Retrieve the session secret from environment variables and encode it const secretKey = process.env.SESSION_SECRET; const encodedKey = new TextEncoder().encode(secretKey); // Encrypts and signs the session payload as a JWT with a 7-day expiration export async function encrypt(payload: SessionPayload) { return new SignJWT(payload) .setProtectedHeader({ alg: "HS256" }) .setIssuedAt() .setExpirationTime("7d") .sign(encodedKey); } // Verifies and decodes the JWT session token export async function decrypt(session: string | undefined = "") { try { const { payload } = await jwtVerify(session, encodedKey, { algorithms: ["HS256"], }); return payload; } catch (error) { console.log(error); } } // Creates a new session by encrypting the payload and storing it in a secure cookie export async function createSession(payload: SessionPayload) { // Set cookie to expire in 7 days const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);

Introduction
In the previous Part of this tutorial, you implemented Strapi email and password registration and login using the SendGrid email provider. You also learned how to perform Strapi email verification upon user registration, sending and resending confirmation email.
With Next.js, you were able to create requests, server actions and handle form submissions.
In this final Part of the Strapi email and password authentication tutorial, we will go further by implementing forgot password, password reset, user logout, and changing password. And in the Next.js frontend, we will implement authentication with session management, secure data access layer, and Middleware.
Tutorial Series
This tutorial is divided into two parts.
- Part 1 - Email and Password registration, Email confirmation, and Login
- Part 2 - Session Management, Data Access, password reset, and changing password
GitHub Repository: Full Code for Strapi and Next.js Authentication Project
The complete code for this project can be found in this repo: strapi-email-and-password-authentication
Handling Session, Protecting Routes and Pages and, Data Access Layer in Next.js
So far, the profile page is not protected from unauthenticated users.
And if you look at the navigation bar, the "Sign-in" button remains the same instead of "Sign-out" for the user to log out. Also, the profile page should welcome the user by their username and not with the generic name "John Doe".
Thus, you need to protect pages, track user authentication state, and secure data access.
This is what we will do:
- Create session management to track user authentication state.
- Set up a middleware for public and protected routes.
- Create a Data Access Layer (DAL) to securely access user data.
Step 1: Create a Session To Track User Authentication State in Next.js
Recall that when a user logs in, Strapi returns a response that contains a JSON Web Token (JWT) as shown below.
{
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTUsImlhdCI6MTc0NTM1MzMzMSwiZXhwIjoxNzQ3OTQ1MzMxfQ.zvx2Q2OexHIPkNA5aCqaOG3Axn0rlylLOpgiVPifi8c",
"user": {
"id": 15,
"documentId": "npbi8dusjdsdwu5a0zq6ticv",
"username": "Theodore",
"email": "strapiUser@gmail.com",
"provider": "local",
"confirmed": true,
"blocked": false,
"createdAt": "2025-04-22T18:18:01.170Z",
"updatedAt": "2025-04-22T19:04:51.091Z",
"publishedAt": "2025-04-22T18:18:01.172Z"
}
}
The JWT issued by Strapi is useful because it needs to be included in subsequent requests to Strapi.
There are different ways to store the JWT, but in this tutorial, we will use Stateless Sessions that can be implemented using Next.js. First, create a session secret.
Create Session Secret
Generate a session secret by using the openssl
command in your terminal which generates a 32-character random string that you can use as your session secret and store in your environment variables file:
openssl rand -base64 32
Add Session Secret to Environment Variable
# Path: ./.env
# ... other environment variables
STRAPI_ENDPOINT="http://localhost:1337"
SESSION_SECRET=YOUR_SESSION_SECRET
Encrypt and Decrypt Sessions
In the previous part of this tutorial, we installed jose
package which provides signing and encryption, and which provides support for JSON Web Tokens (JWT).
Use jose
to do the following:
- Encrypt and decrypt the JWT from Strapi.
- Create a session by storing the signed token in an
httpOnly
cookie named "session" with an expiration time., - Delete the session by removing the session cookie to enable users to log out.
Inside the nextjs-frontend/src/app/lib
folder, create a new file session.ts
:
// Path: nextjs-frontend/src/app/auth/confirm-email/page.tsx
import "server-only";
import { SignJWT, jwtVerify } from "jose";
import { cookies } from "next/headers";
import { SessionPayload } from "@/app/lib/definitions";
// Retrieve the session secret from environment variables and encode it
const secretKey = process.env.SESSION_SECRET;
const encodedKey = new TextEncoder().encode(secretKey);
// Encrypts and signs the session payload as a JWT with a 7-day expiration
export async function encrypt(payload: SessionPayload) {
return new SignJWT(payload)
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("7d")
.sign(encodedKey);
}
// Verifies and decodes the JWT session token
export async function decrypt(session: string | undefined = "") {
try {
const { payload } = await jwtVerify(session, encodedKey, {
algorithms: ["HS256"],
});
return payload;
} catch (error) {
console.log(error);
}
}
// Creates a new session by encrypting the payload and storing it in a secure cookie
export async function createSession(payload: SessionPayload) {
// Set cookie to expire in 7 days
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
// Encrypt the session payload
const session = await encrypt(payload);
// Set the session cookie with the encrypted payload
const cookieStore = await cookies();
// Set the cookie with the session token
cookieStore.set("session", session, {
httpOnly: true, // Prevents client-side JavaScript from accessing the cookie
secure: false,
expires: expiresAt,
sameSite: "lax",
path: "/",
});
}
// Deletes the session cookie to log out the user
export async function deleteSession() {
const cookieStore = await cookies();
cookieStore.delete("session");
}
Let's break down the code above:
- The
encrypt
function creates a secure token from aSessionPayload
with a 7-day expiration - The
decrypt
verifies and decodes the token. - The
createSession
stores the signed JWT in anhttpOnly
cookie to protect it from client-side access. - The
deleteSession
removes the cookie to log the user out.
Step 2: Set Up Middleware to Protect Routes in Next.js
A Middleware allows you to perform business logic functions before a request is completed. With Middleware, we can protect routes/pages in Next.js
Locate the middleware file we created in the first part of this tutorial, nextjs-frontend/src/app/middleware.ts
, and add the following code:
// Path: nextjs-frontend/src/app/middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { decrypt } from "@/app/lib/session";
import { cookies } from "next/headers";
// 1. Specify protected and public routes
const protectedRoutes = ["/profile", "/auth/change-password"];
const publicRoutes = ["/auth/login", "/auth/signup", "/"];
export default async function middleware(req: NextRequest) {
// 2. Check if the current route is protected or public
const path = req.nextUrl.pathname;
const isProtectedRoute = protectedRoutes.includes(path);
const isPublicRoute = publicRoutes.includes(path);
// 3. Decrypt the session from the cookie
const cookie = (await cookies()).get("session")?.value;
const session = await decrypt(cookie);
// 4. Redirect to /login if the user is not authenticated
if (isProtectedRoute && !session?.jwt) {
return NextResponse.redirect(new URL("/auth/login", req.nextUrl));
}
// 5. Redirect to /profile if the user is authenticated
if (
isPublicRoute &&
session?.jwt &&
!req.nextUrl.pathname.startsWith("/profile")
) {
return NextResponse.redirect(new URL("/profile", req.nextUrl));
}
return NextResponse.next();
}
// Routes Middleware should not run on
export const config = {
matcher: ["/((?!api|_next/static|_next/image|.*\\.png$).*)"],
};
Let's break down the Middleware we created above:
- It first defines
protectedRoutes
(/profile
,/auth/change-password
) which should be available to users that are logged-in. And thepublicRoutes
(/auth/login
,/auth/signup
,/
) which is the login, signup and home pages respectively. - On each request, it checks if the route is protected or public, then attempts to read and decrypt the
session
cookie to retrieve the user's JWT. The JWT will be present if the user is logged in. - If a user tries to access a protected route without being authenticated, they are redirected to
/auth/login
. - Conversely, if an authenticated user accesses a public route (like
/auth/login
), they're redirected to/profile
. - The
config.matcher
ensures the middleware only runs on relevant routes, skipping static assets and API calls.
Now, when an unauthenticated user tries to access the profile page at localhost:3000/profile
, they get redirected to the login page as shown below:
Step 3: Create a Data Access Layer (DAL) to Secure Access to User Data.
Inside the nextjs-frontend/src/app/lib
folder, create a file dal.ts
that will allow secure access to user data.
// Path: nextjs-frontend/src/app/lib/dal.ts
import "server-only";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { decrypt } from "./session";
import { cache } from "react";
export const verifySession = cache(async () => {
const cookie = (await cookies()).get("session")?.value;
const session = await decrypt(cookie);
if (!session) {
return { isAuth: false, session };
}
if (!session?.jwt) {
redirect("/auth/login");
}
return { isAuth: true, session };
});
Here is a breakdown of the code above:
- You created a
verifySession
function that securely checks if a user is authenticated on the server. - It uses
cookies()
to retrieve thesession
cookie anddecrypt()
from the session we implemented previously to decode its contents. - If the session is missing, it returns
{ isAuth: false }
. - If the session exists but lacks a valid JWT, it redirects the user to the login page.
- Otherwise, it returns
{ isAuth: true, session }
. - The function is wrapped in
cache()
to avoid repeated execution in a single request lifecycle and marked as "server-only" to be used only in server components. The cache lets you cache the result of a data fetch or computation.
Step 4: Create Session During Login
Upon successful login, you want to create a session for the user.
Inside the nextjs-frontend/src/app/lib/session.ts
file, we created the createSession()
function that creates a new session by encrypting the payload and storing it in a secure cookie.
The createSession()
takes a payload as a parameter. The payload could be the user's name, ID, role, etc. In our case, the payload is the response returned from Strapi.
Import the createSession
function and pass the data returned by Strapi after user login, to the createSession()
function as the payload.
// Path: nextjs-frontend/src/app/profile/page.tsx
// ... other imports
import { createSession } from "../lib/session"; // import create session
// .. other server action functions.
export async function signinAction(
initialState: FormState,
formData: FormData
): Promise<FormState> {
// ... other code logic of siginAction function
await createSession(res.data); // create session for user
redirect("/profile");
}
Now, let's log in!
As you can see when a user logs in and tries to access the localhost:3000
home page, they get redirected to the profile page.
However, the "Sign-in" button on the navigation bar remains the same. It should change to "Sign-out". Let us correct this using the DAL we created above.
Modify Profile Page and Navigation Bar
Import the verifySession
inside both the navigation bar and the profile page.
Since verifySession
imports session
and isAuth
, we can now access the user data and the authentication state.
Update these files:
- Profile Page: Add the following code:
// Path: nextjs-frontend/src/app/profile/page.tsx
import Link from "next/link";
import React from "react";
import LogOutButton from "@/app/components/LogOutButton";
import { verifySession } from "../lib/dal";
export default async function Profile() {
const {
session: { user },
}: any = await verifySession();
return (
<div className="flex min-h-screen items-center justify-center bg-gray-100 px-4">
<div className="w-full max-w-md bg-white p-6 rounded-lg shadow-md text-center space-y-6">
{/* Username */}
<p className="text-xl font-semibold text-gray-800 capitalize">
Welcome, {user?.username}!
</p>
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row justify-center gap-4">
<Link
href="/auth/change-password"
className="w-full sm:w-auto px-6 py-2 bg-blue-500 text-white rounded-lg shadow-md hover:bg-blue-600 transition"
>
Change Password
</Link>
<LogOutButton />
</div>
</div>
</div>
);
}
Let's break down the code above:
- The profile page uses
verifySession()
to retrieve the authenticated user's session. - It displays a personalized welcome message using
user.username
and provides options to change passwords or log out. - Note that we also imported the
LogOutButton
component.
- Navigation Page: Add the following code:
// Path: nextjs-frontend/src/app/components/NavBar.tsx
import Link from "next/link";
import { redirect } from "next/navigation";
import React from "react";
import { verifySession } from "../lib/dal";
import LogOutButton from "./LogOutButton";
export default async function Navbar() {
const { isAuth }: any = await verifySession();
return (
<nav className="flex items-center justify-between px-6 py-4 bg-white shadow-md">
{/* Logo */}
<Link href="/" className="text-xl font-semibold cursor-pointer">
MyApp
</Link>
<div className="flex">
{isAuth ? (
<LogOutButton />
) : (
<Link
href="/auth/signin"
className="px-4 py-2 rounded-lg bg-blue-500 text-white font-medium shadow-md transition-transform transform hover:scale-105 hover:bg-blue-600 cursor-pointer"
>
Sign-in
</Link>
)}
</div>
</nav>
);
}
In the code above:
- The
Navbar
server Component checks if the user is authenticated by callingverifySession()
. - Based on the
isAuth
value, it conditionally renders a "LogOut" button or a "Sign-in" link. This ensures the navigation bar always reflects the user's current auth status on the initial page load.
This is what a user should see once they log in, their username and the "Sign-out" button.
Now that a user's authentication state can be tracked and data can be accessed, how does a user log out?
How to Log Out a User
When you implemented session management, you created a function called deleteSession()
. This function will allow users log out of the application.
Let's invoke this using the "Sign Out" button.
Step 1: Create Server Action to Log Out User
You can do this in several ways, but here is how we want to do it.
- First create a server action that will invoke the
deleteSession()
when called. - Call the server action inside the
LogOutButton
component.
Navigate to the server action file for authentication, nextjs-frontend/src/app/actions/auth.ts
and add the logoutAction
server action:
// Path: nextjs-frontend/src/app/actions/auth.ts
// ... other imprts
import { createSession, deleteSession } from "../lib/session";
// ... other server action functions : signupAction, resendConfirmEmailAction, signinAction
// Logout action
export async function logoutAction() {
await deleteSession();
redirect("/");
}
The logoutAction
above invokes the deleteSession
function which deletes the user session and redirects the user to the home page.
Step 2: Import and Call Log Out Server Action
Inside the LogOutButton
component, import the logoutAction()
server action and add it to the onClick
event handler of the "Sign Out" button.
Locate the nextjs-frontend/src/app/components/LogOutButton.tsx
file and add the following code:
// Path: nextjs-frontend/src/app/components/LogOutButton.tsx
"use client";
import React from "react";
import { logoutAction } from "../actions/auth";
export default function LogOut() {
return (
<button
onClick={() => {
logoutAction();
}}
className="cursor-pointer w-full sm:w-auto px-6 py-2 bg-red-500 text-white rounded-lg shadow-md hover:bg-red-600 transition"
>
Sign Out
</button>
);
}
Now, log in and log out as a new user.
Interesting! A user can now log out!
However, what happens when a user forgets their password? In the next section, we will implement forgot password and reset password.
How to Implement Forgot Password and Reset Password in Strapi
If a user forgets their password, they can reset it by making a forgot password request to Strapi.
const STRAPI_ENDPOINT = "http://localhost:1337";
await axios.post(`${STRAPI_ENDPOINT}/api/auth/forgot-password`, {
email: "User email"
});
To proceed, we need to first edit the email template for forgot password
Step 1: Edit Email Template for Password Reset
Navigate to USERS & PERMISSION PLUGIN > Email templates > Reset password and add the SendGrid email address you used when configuring the email plugin in the strapi-backend/config/plugins.ts
file.
Step 2: Add Reset Password Page
Because Strapi sends a password reset link, add the page that a user should be redirected for password reset once they click the password reset link.
Navigate to Settings > USERS & PERMISSIONS PLUGIN > Advanced Settings > Reset password page and add the link to the reset password page: http://localhost:3000/auth/reset-password
.
Step 2: Create Request Function For Forgot Password Link
Here, we will create a function that will send a request to Strapi to send a forgot password link.
// Path: nextjs-frontend/src/app/lib/requests.ts
import { Credentials } from "./definitions";
import axios from "axios";
const STRAPI_ENDPOINT = process.env.STRAPI_ENDPOINT || "http://localhost:1337";
// ... other request functions
export const forgotPasswordRequest = async (email: string) => {
try {
const response = await axios.post(
`${STRAPI_ENDPOINT}/api/auth/forgot-password`,
{
email, // user's email
}
);
return response;
} catch (error: any) {
return (
error?.response?.data?.error?.message ||
"Error sending reset password email"
);
}
};
Step 3: Create Server Action for Forgot Password
Create a server action that will handle the form submission by calling the forgotPasswordRequest
function above.
// Path: nextjs-frontend/src/app/actions/auth.ts
// ... other imports
import {
signUpRequest,
confirmEmailRequest,
signInRequest,
forgotPasswordRequest,
} from "../lib/requests";
export async function forgotPasswordAction(
initialState: FormState,
formData: FormData
): Promise<FormState> {
// Get email from form data
const email = formData.get("email");
const errors: Credentials = {};
// Validate the form data
if (!email) errors.email = "Email is required";
if (errors.email) {
return {
errors,
values: { email } as Credentials,
message: "Error submitting form",
success: false,
};
}
// Reqest password reset link
const res: any = await forgotPasswordRequest(email as string);
if (res.statusText !== "OK") {
return {
errors: {} as Credentials,
values: { email } as Credentials,
message: res?.statusText || res,
success: false,
};
}
return {
errors: {} as Credentials,
values: { email } as Credentials,
message: "Password reset email sent",
success: true,
};
}
Step 4: Set Up the Forgot Password Page
Next, create the forgot password page that will allow a user enter their email address that Strapi will send the reset password link to.
Locate the nextjs-frontend/src/app/auth/forgot-password/page.tsx
file and add the following code:
// Path: nextjs-frontend/src/app/auth/forgot-password/page.tsx
"use client";
import React, { useActionState, useEffect } from "react";
import { forgotPasswordAction } from "@/app/actions/auth";
import { FormState } from "@/app/lib/definitions";
import { toast } from "react-toastify";
export default function ResetPassword() {
const initialState: FormState = {
errors: {},
values: {},
message: "",
success: false,
};
const [state, formAction, isPending] = useActionState(
forgotPasswordAction,
initialState
);
useEffect(() => {
if (state.success) {
toast.success(state.message, { position: "top-center" });
}
}, [state]);
return (
<div className="flex min-h-screen items-center justify-center bg-gray-100 px-4">
<div className="w-full max-w-md p-6 space-y-6 bg-white rounded-lg shadow-md">
<h2 className="text-2xl font-semibold text-center">Forgot Password</h2>
<p className="text-sm text-gray-600 text-center">
Enter your email and we'll send you a link to reset your password.
{/* Back to Sign In */}
Remembered your password?{" "}
Sign In