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);

May 1, 2025 - 17:03
 0
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.

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".

Profile page

Thus, you need to protect pages, track user authentication state, and secure data access.

This is what we will do:

  1. Create session management to track user authentication state.
  2. Set up a middleware for public and protected routes.
  3. 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 a SessionPayload with a 7-day expiration
  • The decrypt verifies and decodes the token.
  • The createSession stores the signed JWT in an httpOnly 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 the publicRoutes ( /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:

Redirect to login.gif

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 usescookies() to retrieve the session cookie and decrypt() 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!

Redirect to profile.gif

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:

  1. 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.
  1. 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 calling verifySession().
  • 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.

When a user logs in

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.

login and logout a new user.gif

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.

Forgot Password Email Template

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.

Add reset password page

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.
        
        
{/* Email Input */}

{state.errors?.email}

{/* Submit Button */}
{/* Back to Sign In */}

Remembered your password?{" "} Sign In

); }