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.
1{
2 "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTUsImlhdCI6MTc0NTM1MzMzMSwiZXhwIjoxNzQ3OTQ1MzMxfQ.zvx2Q2OexHIPkNA5aCqaOG3Axn0rlylLOpgiVPifi8c",
3 "user": {
4 "id": 15,
5 "documentId": "npbi8dusjdsdwu5a0zq6ticv",
6 "username": "Theodore",
7 "email": "strapiUser@gmail.com",
8 "provider": "local",
9 "confirmed": true,
10 "blocked": false,
11 "createdAt": "2025-04-22T18:18:01.170Z",
12 "updatedAt": "2025-04-22T19:04:51.091Z",
13 "publishedAt": "2025-04-22T18:18:01.172Z"
14 }
15}
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:
1openssl rand -base64 32
Add Session Secret to Environment Variable
1# Path: ./.env
2
3# ... other environment variables
4
5STRAPI_ENDPOINT="http://localhost:1337"
6SESSION_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
:
1// Path: nextjs-frontend/src/app/auth/confirm-email/page.tsx
2
3import "server-only";
4
5import { SignJWT, jwtVerify } from "jose";
6import { cookies } from "next/headers";
7import { SessionPayload } from "@/app/lib/definitions";
8
9// Retrieve the session secret from environment variables and encode it
10const secretKey = process.env.SESSION_SECRET;
11const encodedKey = new TextEncoder().encode(secretKey);
12
13// Encrypts and signs the session payload as a JWT with a 7-day expiration
14export async function encrypt(payload: SessionPayload) {
15 return new SignJWT(payload)
16 .setProtectedHeader({ alg: "HS256" })
17 .setIssuedAt()
18 .setExpirationTime("7d")
19 .sign(encodedKey);
20}
21
22// Verifies and decodes the JWT session token
23export async function decrypt(session: string | undefined = "") {
24 try {
25 const { payload } = await jwtVerify(session, encodedKey, {
26 algorithms: ["HS256"],
27 });
28 return payload;
29 } catch (error) {
30 console.log(error);
31 }
32}
33
34// Creates a new session by encrypting the payload and storing it in a secure cookie
35export async function createSession(payload: SessionPayload) {
36 // Set cookie to expire in 7 days
37 const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
38
39 // Encrypt the session payload
40 const session = await encrypt(payload);
41 // Set the session cookie with the encrypted payload
42 const cookieStore = await cookies();
43
44 // Set the cookie with the session token
45 cookieStore.set("session", session, {
46 httpOnly: true, // Prevents client-side JavaScript from accessing the cookie
47 secure: false,
48 expires: expiresAt,
49 sameSite: "lax",
50 path: "/",
51 });
52}
53
54// Deletes the session cookie to log out the user
55export async function deleteSession() {
56 const cookieStore = await cookies();
57 cookieStore.delete("session");
58}
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:
1// Path: nextjs-frontend/src/app/middleware.ts
2
3import { NextRequest, NextResponse } from "next/server";
4import { decrypt } from "@/app/lib/session";
5import { cookies } from "next/headers";
6
7// 1. Specify protected and public routes
8const protectedRoutes = ["/profile", "/auth/change-password"];
9const publicRoutes = ["/auth/login", "/auth/signup", "/"];
10
11export default async function middleware(req: NextRequest) {
12 // 2. Check if the current route is protected or public
13 const path = req.nextUrl.pathname;
14 const isProtectedRoute = protectedRoutes.includes(path);
15 const isPublicRoute = publicRoutes.includes(path);
16
17 // 3. Decrypt the session from the cookie
18 const cookie = (await cookies()).get("session")?.value;
19 const session = await decrypt(cookie);
20
21 // 4. Redirect to /login if the user is not authenticated
22 if (isProtectedRoute && !session?.jwt) {
23 return NextResponse.redirect(new URL("/auth/login", req.nextUrl));
24 }
25
26 // 5. Redirect to /profile if the user is authenticated
27 if (
28 isPublicRoute &&
29 session?.jwt &&
30 !req.nextUrl.pathname.startsWith("/profile")
31 ) {
32 return NextResponse.redirect(new URL("/profile", req.nextUrl));
33 }
34
35 return NextResponse.next();
36}
37
38// Routes Middleware should not run on
39export const config = {
40 matcher: ["/((?!api|_next/static|_next/image|.*\\.png$).*)"],
41};
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.
1// Path: nextjs-frontend/src/app/lib/dal.ts
2
3import "server-only";
4
5import { cookies } from "next/headers";
6
7import { redirect } from "next/navigation";
8import { decrypt } from "./session";
9
10import { cache } from "react";
11
12export const verifySession = cache(async () => {
13 const cookie = (await cookies()).get("session")?.value;
14
15 const session = await decrypt(cookie);
16
17 if (!session) {
18 return { isAuth: false, session };
19 }
20
21 if (!session?.jwt) {
22 redirect("/auth/login");
23 }
24
25 return { isAuth: true, session };
26});
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.
1// Path: nextjs-frontend/src/app/profile/page.tsx
2
3// ... other imports
4
5
6import { createSession } from "../lib/session"; // import create session
7
8// .. other server action functions.
9
10export async function signinAction(
11 initialState: FormState,
12 formData: FormData
13): Promise<FormState> {
14
15 // ... other code logic of siginAction function
16
17
18 await createSession(res.data); // create session for user
19
20 redirect("/profile");
21}
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:
1// Path: nextjs-frontend/src/app/profile/page.tsx
2
3import Link from "next/link";
4import React from "react";
5import LogOutButton from "@/app/components/LogOutButton";
6import { verifySession } from "../lib/dal";
7
8export default async function Profile() {
9 const {
10 session: { user },
11 }: any = await verifySession();
12
13 return (
14 <div className="flex min-h-screen items-center justify-center bg-gray-100 px-4">
15 <div className="w-full max-w-md bg-white p-6 rounded-lg shadow-md text-center space-y-6">
16
17 {/* Username */}
18 <p className="text-xl font-semibold text-gray-800 capitalize">
19 Welcome, {user?.username}!
20 </p>
21
22 {/* Action Buttons */}
23 <div className="flex flex-col sm:flex-row justify-center gap-4">
24 <Link
25 href="/auth/change-password"
26 className="w-full sm:w-auto px-6 py-2 bg-blue-500 text-white rounded-lg shadow-md hover:bg-blue-600 transition"
27 >
28 Change Password
29 </Link>
30 <LogOutButton />
31 </div>
32 </div>
33 </div>
34 );
35}
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:
1// Path: nextjs-frontend/src/app/components/NavBar.tsx
2
3import Link from "next/link";
4import { redirect } from "next/navigation";
5import React from "react";
6import { verifySession } from "../lib/dal";
7import LogOutButton from "./LogOutButton";
8
9export default async function Navbar() {
10 const { isAuth }: any = await verifySession();
11
12 return (
13 <nav className="flex items-center justify-between px-6 py-4 bg-white shadow-md">
14 {/* Logo */}
15 <Link href="/" className="text-xl font-semibold cursor-pointer">
16 MyApp
17 </Link>
18 <div className="flex">
19 {isAuth ? (
20 <LogOutButton />
21 ) : (
22 <Link
23 href="/auth/signin"
24 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"
25 >
26 Sign-in
27 </Link>
28 )}
29 </div>
30 </nav>
31 );
32}
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:
1// Path: nextjs-frontend/src/app/actions/auth.ts
2
3// ... other imprts
4import { createSession, deleteSession } from "../lib/session";
5
6// ... other server action functions : signupAction, resendConfirmEmailAction, signinAction
7
8// Logout action
9export async function logoutAction() {
10 await deleteSession();
11 redirect("/");
12}
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:
1// Path: nextjs-frontend/src/app/components/LogOutButton.tsx
2
3"use client";
4
5import React from "react";
6import { logoutAction } from "../actions/auth";
7
8export default function LogOut() {
9 return (
10 <button
11 onClick={() => {
12 logoutAction();
13 }}
14 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"
15 >
16 Sign Out
17 </button>
18 );
19}
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.
1const STRAPI_ENDPOINT = "http://localhost:1337";
2
3await axios.post(`${STRAPI_ENDPOINT}/api/auth/forgot-password`, {
4 email: "User email"
5});
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.
1// Path: nextjs-frontend/src/app/lib/requests.ts
2
3import { Credentials } from "./definitions";
4import axios from "axios";
5
6const STRAPI_ENDPOINT = process.env.STRAPI_ENDPOINT || "http://localhost:1337";
7
8// ... other request functions
9
10export const forgotPasswordRequest = async (email: string) => {
11 try {
12 const response = await axios.post(
13 `${STRAPI_ENDPOINT}/api/auth/forgot-password`,
14 {
15 email, // user's email
16 }
17 );
18
19 return response;
20 } catch (error: any) {
21 return (
22 error?.response?.data?.error?.message ||
23 "Error sending reset password email"
24 );
25 }
26};
Step 3: Create Server Action for Forgot Password
Create a server action that will handle the form submission by calling the forgotPasswordRequest
function above.
1// Path: nextjs-frontend/src/app/actions/auth.ts
2
3// ... other imports
4
5import {
6 signUpRequest,
7 confirmEmailRequest,
8 signInRequest,
9 forgotPasswordRequest,
10} from "../lib/requests";
11
12
13export async function forgotPasswordAction(
14 initialState: FormState,
15 formData: FormData
16): Promise<FormState> {
17 // Get email from form data
18 const email = formData.get("email");
19
20 const errors: Credentials = {};
21
22 // Validate the form data
23 if (!email) errors.email = "Email is required";
24 if (errors.email) {
25 return {
26 errors,
27 values: { email } as Credentials,
28 message: "Error submitting form",
29 success: false,
30 };
31 }
32
33 // Reqest password reset link
34 const res: any = await forgotPasswordRequest(email as string);
35
36 if (res.statusText !== "OK") {
37 return {
38 errors: {} as Credentials,
39 values: { email } as Credentials,
40 message: res?.statusText || res,
41 success: false,
42 };
43 }
44
45 return {
46 errors: {} as Credentials,
47 values: { email } as Credentials,
48 message: "Password reset email sent",
49 success: true,
50 };
51}
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:
1// Path: nextjs-frontend/src/app/auth/forgot-password/page.tsx
2
3"use client";
4
5import React, { useActionState, useEffect } from "react";
6import { forgotPasswordAction } from "@/app/actions/auth";
7import { FormState } from "@/app/lib/definitions";
8import { toast } from "react-toastify";
9
10export default function ResetPassword() {
11 const initialState: FormState = {
12 errors: {},
13 values: {},
14 message: "",
15 success: false,
16 };
17
18 const [state, formAction, isPending] = useActionState(
19 forgotPasswordAction,
20 initialState
21 );
22
23 useEffect(() => {
24 if (state.success) {
25 toast.success(state.message, { position: "top-center" });
26 }
27 }, [state]);
28
29 return (
30 <div className="flex min-h-screen items-center justify-center bg-gray-100 px-4">
31 <div className="w-full max-w-md p-6 space-y-6 bg-white rounded-lg shadow-md">
32 <h2 className="text-2xl font-semibold text-center">Forgot Password</h2>
33 <p className="text-sm text-gray-600 text-center">
34 Enter your email and we'll send you a link to reset your password.
35 </p>
36 <form action={formAction} className="space-y-4">
37 {/* Email Input */}
38 <div>
39 <label className="block text-gray-700 mb-1">Email</label>
40 <input
41 type="email"
42 name="email"
43 defaultValue={state.values?.email}
44 placeholder="your@email.com"
45 className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
46 />
47 <p className="text-red-500 text-sm">{state.errors?.email}</p>
48 </div>
49
50 {/* Submit Button */}
51 <button
52 type="submit"
53 disabled={isPending}
54 className="w-full cursor-pointer py-2 text-white bg-blue-500 rounded-lg hover:bg-blue-600 transition"
55 >
56 {isPending ? "Submitting..." : "Send Reset Link"}
57 </button>
58 </form>
59
60 {/* Back to Sign In */}
61 <p className="text-center text-gray-600 text-sm">
62 Remembered your password?{" "}
63 <a href="/auth/login" className="text-blue-500 hover:underline">
64 Sign In
65 </a>
66 </p>
67 </div>
68 </div>
69 );
70}
This is what the forgot password page should look like:
Now, when you type in the correct email address and click the "Send Reset Link" button, a reset password link will be sent to you in this format: http://localhost:3000/auth/reset-password?code=ad4276ecfa00ccf13ab1ba2f7fb68b461212f6fd15b25e948e85ab1829eb4cf543939fe4e18b83388e7a5c85fbfa12d865bcab216ca4065844f302c91aea9d97
Note the code
query parameter in this link.
The password reset link is the link to the reset password page you added in the second step above, and which we will create soon in the next section. We will use the code
in the link when making request to reset password in Strapi.
How to Reset User's Password in Strapi with Next.js
To reset a user's password after getting their forgot password link, here is the request example:
1const STRAPI_ENDPOINT = "http://localhost:1337";
2
3await axios.post(`${BASE_URL}/api/auth/reset-password`, {
4 code: "code from email link",
5 password: "new password",
6 passwordConfirmation: "confirm password",
7});
Here is what you will need
- The
code
which was added to the reset password link. - The
password
which is the new password. - The
confirmPassword
should be the same aspassword
.
The first step is to create a request function that will sends a request to the endpoint above to reset a user's password.
Step 1: Create Password Reset Request Function
Now, create a request function to reset a user's password:
1// Path: nextjs-frontend/src/app/lib/requests.ts
2
3// ... other codes
4
5export const resetPasswordRequest = async (credentials: Credentials) => {
6 try {
7 const response = await axios.post(
8 `${STRAPI_ENDPOINT}/api/auth/reset-password`,
9 {
10 code: credentials?.code,
11 password: credentials?.password,
12 passwordConfirmation: credentials?.confirmPassword,
13 }
14 );
15
16 return response;
17 } catch (error: any) {
18 return error?.response?.data?.error?.message || "Error resetting password";
19 }
20};
Step 2: Create Password Reset Server Action
Create a server action to handle form submission for reset password and to call the requestPasswordRequest()
function above.
1// Path: nextjs-frontend/src/app/actions/auth.ts
2
3// ... other imports
4
5import {
6 signUpRequest,
7 confirmEmailRequest,
8 signInRequest,
9 forgotPasswordRequest,
10 resetPasswordRequest,
11} from "../lib/requests";
12
13// ... other actions
14
15export async function resetPasswordAction(
16 initialState: FormState,
17 formData: FormData
18): Promise<FormState> {
19
20
21 const password = formData.get("password"); // password
22 const code = formData.get("code"); // code
23 const confirmPassword = formData.get("confirmPassword"); // confirm password
24
25 const errors: Credentials = {};
26
27 if (!password) errors.password = "Password is required";
28 if (!confirmPassword) errors.confirmPassword = "Confirm password is required";
29 if (!code) errors.code = "Error resetting password";
30 if (password && confirmPassword && password !== confirmPassword) {
31 errors.confirmPassword = "Passwords do not match";
32 }
33
34 if (Object.keys(errors).length > 0) {
35 return {
36 errors,
37 values: { password, confirmPassword, code } as Credentials,
38 message: "Error submitting form",
39 success: false,
40 };
41 }
42
43 // Call request
44 const res: any = await resetPasswordRequest({
45 code,
46 password,
47 confirmPassword,
48 } as Credentials);
49
50 if (res?.statusText !== "OK") {
51 return {
52 errors: {} as Credentials,
53 values: { password, confirmPassword, code } as Credentials,
54 message: res?.statusText || res,
55 success: false,
56 };
57 }
58
59 return {
60 errors: {} as Credentials,
61 values: {} as Credentials,
62 message: "Reset password successful!",
63 success: true,
64 };
65}
Step 3: Set Up Password Reset Page and Form
Inside the reset password page, you will have to create a form that will allow a user enter a new password and the confirm password. The form should include the code as shown in the resetPasswordRequest
function.
Find the nextjs-frontend/src/app/auth/reset-password/page.tsx
and add the following code:
1// Path: nextjs-frontend/src/app/auth/reset-password/page.tsx
2
3"use client";
4
5import { useActionState, useEffect } from "react";
6import { redirect, useSearchParams } from "next/navigation";
7import { resetPasswordAction } from "@/app/actions/auth";
8import { FormState } from "@/app/lib/definitions";
9import { toast } from "react-toastify";
10
11export default function ResetPassword() {
12 const searchParams = useSearchParams();
13 const code = searchParams.get("code");
14
15 const initialState: FormState = {
16 errors: {},
17 values: {},
18 message: "",
19 success: false,
20 };
21
22 const [state, formAction, IsPending] = useActionState(
23 resetPasswordAction,
24 initialState
25 );
26
27 useEffect(() => {
28 if (state.success) {
29 toast.success(state.message, { position: "top-center" });
30 redirect("/auth/login");
31 }
32 }, [state.success]);
33
34 return (
35 <div className="flex min-h-screen items-center justify-center bg-gray-100 px-4">
36 <div className="w-full max-w-md p-6 space-y-6 bg-white rounded-lg shadow-md text-center">
37 <h2 className="text-2xl font-semibold">Reset Your Password</h2>
38
39 <p className="text-gray-600 text-sm">
40 Enter your new password below to update your credentials.
41 </p>
42
43 <form action={formAction} className="space-y-4 text-left">
44 <p className="text-red-500 text-center text-sm">
45 {!state?.success && state?.message}
46 </p>
47 {/* New Password */}
48 <div>
49 <label className="block text-gray-700 mb-1">New Password</label>
50 <input
51 type="password"
52 name="password"
53 defaultValue={state.values?.password}
54 placeholder="Enter new password"
55 className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
56 />
57 {state?.errors.password && (
58 <p className="text-red-500 text-sm">{state?.errors.password}</p>
59 )}
60 </div>
61 {/* Confirm Password */}
62 <div>
63 <label className="block text-gray-700 mb-1">Confirm Password</label>
64 <input
65 type="password"
66 name="confirmPassword"
67 defaultValue={state.values?.confirmPassword}
68 placeholder="Confirm new password"
69 className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
70 />
71 {state?.errors.confirmPassword && (
72 <p className="text-red-500 text-sm">
73 {state?.errors.confirmPassword}
74 </p>
75 )}
76 </div>
77 {/* Reset Password Button */}
78 <button
79 type="submit"
80 disabled={IsPending}
81 className="cursor-pointer w-full py-2 px-4 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition"
82 >
83 Reset Password
84 </button>
85 <input type="hidden" name="code" value={code as string} />
86 <input type="hidden" name="passwordType" value="reset" />{" "}
87 </form>
88 </div>
89 </div>
90 );
91}
Here is a breakdown of the code above:
- The
useSearchParams
from Next.js is used to extract the resetcode
from the URL, which is required for Strapi’s password reset flow. - The form is connected to the server action
resetPasswordAction
we created usinguseActionState
, which handles validation, server response, and form state updates (errors
,values
,messages
). - When the form is submitted, a
POST
request is made to the server with the new password, confirmation, and reset code. - If the reset is successful, a success toast is displayed using
react-toastify
, and the user is redirected to the login page withredirect
. - If validation fails or the backend returns an error, the appropriate message is shown and the form values are preserved using
defaultValue
. - Additionally, while the form is submitted, the button is disabled and shows a loading state using
isPending
.
Here is a demo of resetting the password:
A user can now successfully reset their passwords without being authenticated.
How about a user that is authenticated? Well, they don't need to send an email of the reset link. They can safely do it as long as they are authenticated and authorized.
Let's implement changing passwords in Strapi and Next.js.
Changing User's Password in Strapi
Unlike an unauthenticated user who needs to be sent a password reset link, an authenticated user is authorized to change their password as long as they have the JWT credential that was issued by Strapi after a successful login.
Here is the request example for changing the password in Strapi.
1
2const STRAPI_ENDPOINT = "http://localhost:1337";
3
4const response = await axios.post(
5 `${STRAPI_ENDPOINT}/api/auth/change-password`,
6 {
7 currentPassword: "user password",
8 password: "user password",
9 passwordConfirmation: "user password",
10 },
11 {
12 headers: {
13 Authorization: `Bearer ${jwt}`,
14 },
15 },
16);
You will use the Bearer-token authentication scheme to include the Strapi JWT in the request headers.
Implement changing of the password by doing the following:
Step 1: Create Change Password Request Function
Head over to nextjs-frontend/src/app/auth/change-password/page.tsx
and add the following code:
1// Path: nextjs-frontend/src/app/lib/requests.ts
2
3// ... other imports
4
5import { verifySession } from "./dal";
6
7// ... other codes
8
9export const changePasswordRequest = async (credentials: Credentials) => {
10 try {
11 const {
12 session: { jwt },
13 }: any = await verifySession();
14
15 const response = await axios.post(
16 `${BASE_URL}/api/auth/change-password`,
17 {
18 currentPassword: credentials.password,
19 password: credentials.newPassword,
20 passwordConfirmation: credentials.confirmPassword,
21 },
22 {
23 headers: {
24 Authorization: `Bearer ${jwt}`,
25 },
26 }
27 );
28
29 return response;
30 } catch (error: any) {
31 return error?.response?.data?.error?.message || "Error resetting password";
32 }
33};
Here is what the changePasswordRequest
request function above does:
- Sends a
POST
request to Strapi’s/auth/change-password
endpoint with the user's current and new passwords. - Imports the
verifySession()
function which we created earlier. - Retrieves the authenticated user's
jwt
usingverifySession()
and includes it in the Authorization header.
Step 3: Create Server Action for Changing Password
Next, create a server action for handling form submission and call the changePasswordRequest()
function above.
Inside the nextjs-frontend/src/app/actions/auth.ts
file, add the following code:
1// Path : nextjs-frontend/src/app/actions/auth.ts
2
3export async function changePasswordAction(
4 initialState: FormState,
5 formData: FormData
6): Promise<FormState> {
7 // Convert formData into an object to extract data
8 const password = formData.get("password");
9 const newPassword = formData.get("newPassword");
10 const confirmPassword = formData.get("confirmPassword");
11
12 const errors: Credentials = {};
13
14 if (!password) errors.password = "Current Password is required";
15 if (!confirmPassword) errors.confirmPassword = "Confirm password is required";
16 if (!newPassword) errors.newPassword = "New password is required";
17 if (confirmPassword !== newPassword) {
18 errors.confirmPassword = "Passwords do not match";
19 }
20
21 if (Object.keys(errors).length > 0) {
22 return {
23 errors,
24 values: { password, confirmPassword, newPassword } as Credentials,
25 message: "Error submitting form",
26 success: false,
27 };
28 }
29
30 // Call backend API
31 const res: any = await changePasswordRequest({
32 password,
33 newPassword,
34 confirmPassword,
35 } as Credentials);
36
37 if (res?.statusText !== "OK") {
38 return {
39 errors: {} as Credentials,
40 values: { password, confirmPassword, newPassword } as Credentials,
41 message: res?.statusText || res,
42 success: false,
43 };
44 }
45
46 return {
47 errors: {} as Credentials,
48 values: {} as Credentials,
49 message: "Reset password successful!",
50 success: true,
51 };
52}
The server action above does the following:
- Handles the password change form submission by validating the user's
current password
,new password
, andconfirm password
fields. - If any field is missing or if the new passwords don't match, it returns validation errors along with the previous form values.
- If validation passes, it calls
changePasswordRequest()
function to send the data to Strapi’s/auth/change-password
endpoint. If the API request fails, it returns an error message; otherwise, it returns a success message indicating the password was reset successfully.
Next, set up the change password page.
Step 4: Set up the Change Password Page and Form
Inside the nextjs-frontend/src/app/auth/change-password/page.tsx
file, add the following code:
1// Path: nextjs-frontend/src/app/auth/change-password/page.tsx
2
3"use client";
4
5import React, { useActionState, useEffect } from "react";
6import { redirect, useSearchParams } from "next/navigation";
7import { changePasswordAction } from "@/app/actions/auth";
8import { FormState } from "@/app/lib/definitions";
9import { toast } from "react-toastify";
10
11export default function ResetPassword() {
12
13 const initialState: FormState = {
14 errors: {},
15 values: {},
16 message: "",
17 success: false,
18 };
19
20 const [state, formAction, IsPending] = useActionState(
21 changePasswordAction,
22 initialState
23 );
24
25 useEffect(() => {
26 if (state.success) {
27 toast.success(state.message, { position: "top-center" });
28 redirect("/profile");
29 }
30 }, [state.success]);
31
32 return (
33 <div className="flex min-h-screen items-center justify-center bg-gray-100 px-4">
34 <div className="w-full max-w-md p-6 space-y-6 bg-white rounded-lg shadow-md text-center">
35 <h2 className="text-2xl font-semibold">Change Password</h2>
36
37 <p className="text-gray-600 text-sm">
38 Enter your new password below to update your credentials.
39 </p>
40
41 <form action={formAction} className="space-y-4 text-left">
42 <p className="text-red-500 text-center text-sm">
43 {!state?.success && state?.message}
44 </p>
45
46 {/* Current Password */}
47 <div>
48 <label className="block text-gray-700 mb-1">Current Password</label>
49 <input
50 type="password"
51 name="password"
52 defaultValue={state.values?.password}
53 placeholder="Enter current password"
54 className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
55 />
56 {state?.errors.password && (
57 <p className="text-red-500 text-sm">{state?.errors.password}</p>
58 )}
59 </div>
60
61 {/* New Password */}
62 <div>
63 <label className="block text-gray-700 mb-1">New Password</label>
64 <input
65 type="password"
66 name="newPassword"
67 defaultValue={state.values?.newPassword}
68 placeholder="Enter new password"
69 className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
70 />
71 {state?.errors.newPassword && (
72 <p className="text-red-500 text-sm">
73 {state?.errors.newPassword}
74 </p>
75 )}
76 </div>
77
78 {/* Confirm Password */}
79 <div>
80 <label className="block text-gray-700 mb-1">Confirm Password</label>
81 <input
82 type="password"
83 name="confirmPassword"
84 defaultValue={state.values?.confirmPassword}
85 placeholder="Confirm new password"
86 className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
87 />
88 {state?.errors.confirmPassword && (
89 <p className="text-red-500 text-sm">
90 {state?.errors.confirmPassword}
91 </p>
92 )}
93 </div>
94
95 {/* Reset Password Button */}
96 <button
97 type="submit"
98 disabled={IsPending}
99 className="w-full py-2 px-4 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition"
100 >
101 Change Password
102 </button>
103 </form>
104 </div>
105 </div>
106 );
107}
Here is what we did on the reset password page above:
- It uses
useActionState
to connect the form to thechangePasswordAction
server action, handling field validation, error messages, and form values. - Upon a successful password change, a success toast is displayed using
react-toastify
, and the user is redirected to the profile page. - Inputs for the current password, new password, and confirmed password are rendered with their respective validation error messages shown if any issues occur. While the form is submitted, the button is disabled using the
isPending
state to prevent duplicate submissions.
See the demo below:
A user logs in, clicks the "Change Password" button, and updates their 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
Wrap Up and Conclusion
Congratulations on completing this two-part tutorial series! You've done an excellent job diving deep into authentication, which is one of the most important aspects of modern web applications.
By implementing Strapi email/password authentication with Next.js 15, you’ve gained hands-on experience with secure user registration, email confirmation, session management, and password handling. You’ve also learned to combine server actions, stateless sessions, protected routes, and data access layers to build a full-stack authentication system.
Now that you’ve mastered the fundamentals, consider extending this project with advanced features:
- Add Role-Based Access Control (RBAC) using Strapi roles and permissions
- Integrate social login providers (Google, GitHub, etc.) via Strapi
- Allow users to upload avatars using Strapi’s Media Library
- Implement JWT refresh tokens or token expiration workflows
See Strapi in action with an interactive demo
Theodore is a Technical Writer and a full-stack software developer. He loves writing technical articles, building solutions, and sharing his expertise.