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.
This tutorial is divided into two parts.
The complete code for this project can be found in this repo: strapi-email-and-password-authentication
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:
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.
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
1# Path: ./.env
2
3# ... other environment variables
4
5STRAPI_ENDPOINT="http://localhost:1337"
6SESSION_SECRET=YOUR_SESSION_SECRET
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:
httpOnly
cookie named "session" with an expiration time.,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:
encrypt
function creates a secure token from a SessionPayload
with a 7-day expirationdecrypt
verifies and decodes the token. createSession
stores the signed JWT in an httpOnly
cookie to protect it from client-side access. deleteSession
removes the cookie to log the user out. 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:
protectedRoutes
(/profile
, /auth/change-password
) which should be available to users that are logged-in. And the publicRoutes
( /auth/login
, /auth/signup
, /
) which is the login, signup and home pages respectively.session
cookie to retrieve the user's JWT. The JWT will be present if the user is logged in./auth/login
. /auth/login
), they're redirected to /profile
. 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:
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:
verifySession
function that securely checks if a user is authenticated on the server. cookies()
to retrieve the session
cookie and decrypt()
from the session we implemented previously to decode its contents. { isAuth: false }
. { isAuth: true, session }
. 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. 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.
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:
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:
verifySession()
to retrieve the authenticated user's session. user.username
and provides options to change passwords or log out. LogOutButton
component.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:
Navbar
server Component checks if the user is authenticated by calling verifySession()
. 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?
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.
You can do this in several ways, but here is how we want to do it.
deleteSession()
when called.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.
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.
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
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.
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
.
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};
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}
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.
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
code
which was added to the reset password link.password
which is the new password.confirmPassword
should be the same as password
.The first step is to create a request function that will sends a request to the endpoint above to reset a user's password.
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};
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}
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:
useSearchParams
from Next.js is used to extract the reset code
from the URL, which is required for Strapi’s password reset flow. resetPasswordAction
we created using useActionState
, which handles validation, server response, and form state updates (errors
, values
, messages
). POST
request is made to the server with the new password, confirmation, and reset code. react-toastify
, and the user is redirected to the login page with redirect
. defaultValue
. 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.
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:
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:
POST
request to Strapi’s /auth/change-password
endpoint with the user's current and new passwords. verifySession()
function which we created earlier. jwt
using verifySession()
and includes it in the Authorization header. 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:
current password
, new password
, and confirm password
fields. 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.
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:
useActionState
to connect the form to the changePasswordAction
server action, handling field validation, error messages, and form values. react-toastify
, and the user is redirected to the profile page. isPending
state to prevent duplicate submissions.See the demo below:
A user logs in, clicks the "Change Password" button, and updates their password.
The complete code for this project can be found in this repo: strapi-email-and-password-authentication
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:
Theodore is a Technical Writer and a full-stack software developer. He loves writing technical articles, building solutions, and sharing his expertise.