DEV Community

Christian Nwamba
Christian Nwamba

Posted on • Updated on

How to Protect Next.js 13 App Routes with Authentication

Securing Next.js pages is tricky — considering all of the layers of rendering that Next.js offers. You need to understand where dynamic rendering starts and stops and where client only code takes over.

Regardless of your strategy, the common and recommended way to secure Next.js pages is to secure them on the server. That means we have to employ cookies instead of local Storage. That means we can also validate JWTs in the same file that our client code lives in.

With all of these possibilities it becomes hard to decide where things go. This article will guide you on how to handle sign in, sign up, password reset, and email confirmation.

Prerequisites

These days it’s becoming unreasonable to build Auth from scratch unless you have 30+ hours to kill. To help us focus on protecting Next.js pages instead of setting up Auth right, we’ll use AWS Amplify Auth. Amplify makes it easy to setup a full featured auth service in less than 10 clicks. That said you’ll need an AWS account and some knowledge of React to follow along.

Create an Amplify Project

To create an Amplify project, navigate to your AWS console, search for AWS Amplify, and select it to open the Amplify Console.

If this is your first app, scroll to the bottom of the page in the Amplify Studio section, select Get started. Name your app, and select Confirm Deployment.

If you’ve created an Amplify App in the past, follow the steps below:

  • Select New app in the upper right-hand corner, and select Build an app from the dropdown menu.

  • Give the app a name, and click the Confirm Deployment to deploy.

Once the deployment is completed, click the Launch Studio button to launch the Amplify studio.

AWS Amplify Studio is a visual development environment tailored for building fullstack web and mobile apps. One of its standout features is the ease with which you can set up backend resources for various tasks, such as authentication and managing customer data.

It also has a Content Management System (CMS), which is incredibly useful for viewing and managing user data.

You should now see the Home menu for your application. To learn more, see the Amplify Studio introduction.

Set up Authentication

Adding authentication to your app enables users to create accounts, sign in, and ensures that only authorized users can access your app. Writing the logic for an application's login flow can be challenging and time-consuming. However, Amplify simplifies this process by providing a complete authentication solution with Amazon Cognito that can be easily added to your app.

Follow the steps below to set up authentication for your app:

  1. Select the Authentication option in the setup menu on the screen's left side. You don't need to choose a login method in the Configure login section, as Email is already selected as the default option.
  2. Scroll down to the Configure Signup section, and you should see the Add attribute dropdown. Click the dropdown icon and select the Name attribute. You should see the Password protection settings in this section. Click on this to select from a variety of security options for user passwords.
  3. Click the Deploy button, acknowledge the warning, and select Confirm Deployment.

You should see the progress of this deployment process — a visual representation of how your setup is being deployed in the AWS environment.

This process should take a minute or two, but when it is done, you should see a confirmation message saying that you’ve successfully deployed authentication.

Set up a Next Project

To keep the focus of this guide on building the app, I will skip the steps in setting up the markup and the styles for the pages.

To quickly get started with a Next.js project that includes the necessary dependencies and UI components for various pages, navigate to your preferred directory and run the following command to clone this starter project:

npx degit christiannwamba/next-app-auth#starter next-app-auth
Enter fullscreen mode Exit fullscreen mode

Run the following command to navigate into the next-app-auth directory, install the dependencies, and start up your development server:

cd next-app-auth
npm install
npm start
Enter fullscreen mode Exit fullscreen mode

This lets you see the output generated by the build. You can see the running app by navigating to http://localhost:3000.

As you can see, we already have the custom Auth UI components set up for various pages in our Next.js application. These components are the basis for our authentication interface, providing templates for pages such as sign-in, sign-up, and password recovery.

As we continue, we will explain the contents of these starter files and their functionality in detail. We will also update their logic as needed to ensure they are tailored to the specific requirements of our application.

Navigate to your Amplify Studio application, and copy the pull command displayed. This command is important as it allows you to pull the newly created Amplify project from the cloud to your local environment (Next.js application in our case).

Dependency: Make sure you have AmplifyCLI configured before executing the pull command.

From your projects directory, paste the copied command into the terminal and then execute it. A typical instance of this command would appear as follows:

amplify pull --appId d2act21onzf92o --envName staging
Enter fullscreen mode Exit fullscreen mode

The appId specifies the unique ID of the Amplify App you want to pull and envName staging specifies the environment of the Amplify App that you want to pull. An Amplify App can have multiple environments (like 'development', 'staging', 'and production').

When you execute the command, you will be automatically redirected to your web browser to grant the CLI access. Once there, click 'Yes' to authenticate with Amplify Studio.

After successful login, return to the CLI. Here, you will be asked a series of questions to gather essential details about your project's configuration. Your answers will help Amplify understand your project's structure and needs.

Accept the default values highlighted in the image below:

The Amplify CLI will automatically carry out the following steps for you:

  1. Amplify will create a new project in the current local directory. This is indicated by the creation of an amplify folder in your project's root directory, which contains all of your project's backend infrastructure settings.
  2. It creates a file called aws-exports.js at the root of your Next.js application. It holds all the configuration for the services you create with Amplify, allowing the Amplify client to access necessary information about your backend services, such as API endpoints, authentication configuration, etc.

Install the Amplify Library

The Amplify library is like a toolbox containing various tools (methods, components, and APIs) that help us integrate our application with various cloud services, such as AWS Cognito, for authentication. It provides pre-built functions for use cases like signing in, signing up, checking user sessions, among others.

Run the following command to install the Amplify Library:

npm install aws-amplify
Enter fullscreen mode Exit fullscreen mode

The aws-amplify package is the library that enables you to connect to your backend resources.

Configure Amplify

For our application to communicate and interact with the services provided by AWS, we need to configure AWS Amplify for use throughout the project.

First, we must configure Amplify in the app/auth/Layout component to allow the library to communicate with the cloud services (like authentication) we've set up on our backend. This way, we can easily access the methods for signing in, signing up, and managing user sessions in any component that the Layout component wraps.

Update your app/auth/Layout.js file with the following:

"use client";
import React from "react";
import { Amplify } from "aws-amplify";
import awsExports from "@/aws-exports";

Amplify.configure({ ...awsExports, ssr: true });

export default function Layout({ children, title }) {
  return (
    <div className="flex min-h-full flex-1 flex-col justify-center px-6 py-12 lg:px-8">
      {children}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the code above, we import the Amplify library and configuration details from the aws-exports.js file.

We then configure AWS Amplify with the details we imported. This setup ensures that the Amplify library knows about your specific backend setup and can interact with it correctly.

To store the user access token (JWT) in the cookie instead of the localStorage, we set ssr to true. Storing the token in the cookie makes it easy for server rendered pages to access the token

As mentioned earlier, we need people to be authenticated to access the dashboard. To achieve this, let us go ahead and write the logic in our custom auth UI components.

Create a New User (Sign UP)

Update the code in your app/auth/sign-up/pages.js file to include the following:

Let’s start with importing the libraries we need for the Sign Up page:

"use client";
import React from "react";
// Import these
import { Auth } from "aws-amplify";
import { useRouter } from "next/navigation";
function SignUp() {
  // ...
}
export default SignUp;
Enter fullscreen mode Exit fullscreen mode

We first import Auth from aws-amplify, the AWS Amplify library's authentication module. We also import useRouter from next/navigation, which is a hook for navigation-related functionality.

Next, we initialize a state variable called user with properties name, email, and password, all set to empty strings.

"use client";
//...
function SignUp() {
  const [user, setUser] = React.useState({
    name: "",
    email: "",
    password: "",
  });

  //...
}
export default SignUp;
Enter fullscreen mode Exit fullscreen mode

We need to add a function that handles the sign up once the user submits the sign up form:

"use client";
//...
function SignUp() {
  const [user, setUser] = React.useState({
  //...

  //Add this
  const router = useRouter();
  async function signUp() {
    try {
      const auth = await Auth.signUp({
        username: user.email,
        password: user.password,
        attributes: {
          name: user.name,
        },
        autoSignIn: {
          enabled: true,
        },
      });
      console.log(auth);
      router.push(`/auth/confirm-email?email=${user.email}`);
    } catch (error) {
      console.log("error signing up:", error);
    }
  }

}
export default SignUp;
Enter fullscreen mode Exit fullscreen mode

We defined an asynchronous function called signUp, which is responsible for user registration. It calls the signUp method from the Auth module to create a new user with their email (username), password, and additional attributes (user's name). We set autoSignIn to true to automatically sign the user in after registration.

If the sign-up process is successful, the user is redirected to the email confirmation page. If an error occurs, it is logged to the console.

Now let’s hook up the function to the form:

// Markup is stripped to focus on logic

function SignUp() {
  const [user, setUser] = React.useState({
    //...
  });
  //Add this
  const router = useRouter();
  async function signUp() {
    //...
  }
  return (
    <>
      {/* //... */}
      <form
        className="space-y-6"
        onSubmit={(e) => {
          e.preventDefault();
          console.log(user);
          signUp();
        }}
      >
        <div>
          <label>Full Name</label>
          <div className="mt-2">
            <input
              onChange={(e) => {
                const val = e.target.value;
                if (val !== "") {
                  setUser({ ...user, name: val });
                }
              }}
            />
          </div>
        </div>
        <div>
          <label>Email address</label>
          <div className="mt-2">
            <input
              autoComplete="email"
              onChange={(e) => {
                const val = e.target.value;
                if (val !== "") {
                  setUser({ ...user, email: val });
                }
              }}
            />
          </div>
        </div>
        <div>
          <label>Password</label>
          <div className="mt-2">
            <input
              onChange={(e) => {
                const val = e.target.value;
                if (val !== "") {
                  setUser({ ...user, password: val });
                }
              }}
            />
          </div>
        </div>
        <div>
          <button type="submit">Sign up</button>
        </div>
      </form>
      <p>
        Already have an account? <a href="/auth/sign-in">Sign in</a>
      </p>
    </>
  );
}
export default SignUp;
Enter fullscreen mode Exit fullscreen mode

The return statement renders a form with input fields for capturing the user's details (name, email, and password). An onChange event handler is attached to each input field and is triggered every time the user types into the field. When triggered, the event updates the corresponding property (name, email, or password) on the user state object with the new state value using the setUser function. When the form is submitted, it calls the signUp function. We also added a link that redirects users to the sign-in page if they already have an account.

Now if you go to this URL — http://localhost:3000/auth/sign-up in your browser, and provide your details, you should be able to successfully create an account.

To see the details of the newly created account, go to your application's Amplify Studio dashboard, and navigate to the Manage section in the left sidebar. Select User management from the options available.

Confirm User’s Email

Once a user has successfully registered, AWS Amplify will automatically send a confirmation code to the user's email to verify their email address. The next step in our sign-up process is to implement the functionality that allows users to confirm their email by entering the code provided.

Update the code in your app/auth/confirm-email/pages.js file to include the following:

Let’s start with importing the libraries we need for the Confirm Email page:

"use client";
import React from "react";

//Import these
import { useSearchParams } from "next/navigation";
import { Auth } from "aws-amplify";
import { useRouter } from "next/navigation";
Enter fullscreen mode Exit fullscreen mode

The code above imports the necessary modules and hooks. Two specific hooks from Next.js are used: useSearchParams for accessing query parameters and useRouter for accessing router functions.

"use client";
import React from "react";

//...

function ConfirmEmail() {
  const [code, setCode] = React.useState("");

  // Add these
  const router = useRouter();
  const searchParams = useSearchParams();

  //...
}
export default ConfirmEmail;
Enter fullscreen mode Exit fullscreen mode

In the page component, we declare the state variable code to store the confirmation code the user will input. We also initialize the router and searchParams hooks.

Let’s add a function that handles the email confirmation once the user submits the form:

"use client";
import React from "react";

//...

function ConfirmEmail() {
  const [code, setCode] = React.useState("");

  const router = useRouter();
  const searchParams = useSearchParams();

  // Add this
  const router = useRouter();
  const searchParams = useSearchParams();

  async function confirm() {
    // Add these
    const email = searchParams.get("email");
    try {
      await Auth.confirmSignUp(email, code);
      router.push("/dashboard");
    } catch (error) {
      console.log("error confirming sign up", error);
    }
  }

  //...
}
export default ConfirmEmail;
Enter fullscreen mode Exit fullscreen mode

We defined an asynchronous function called confirm that validates the user's account with the confirmation code sent to their email. The function retrieves the user's email from the search parameters in the URL and attempts to confirm the sign-up process using the Auth.confirmSignUp method with the user's email and the code entered by the user.

If the confirmation is successful, the function redirects the user to the dashboard page using the router.push("/dashboard") method. If an error occurs, it gets logged to the console.

Let’s hook up the function to the form:

//...

function ConfirmEmail() {
  //...

  return (
    <>
      <div className="sm:mx-auto sm:w-full sm:max-w-sm">
        <h2 className="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
          Confirm Email
        </h2>
      </div>
      <div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
        <form
          className="space-y-6"
          onSubmit={(e) => {
            e.preventDefault();
            confirm();
          }}
        >
          <div>
            <label
              htmlFor="code"
              className="block text-sm font-medium leading-6 text-gray-900"
            >
              Code
            </label>
            <div className="mt-2">
              <input
                id="code"
                name="code"
                type="text"
                autoComplete="code"
                required
                onChange={(e) => {
                  setCode(e.target.value);
                }}
                className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
              />
            </div>
          </div>
          <div>
            <button
              type="submit"
              className="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
            >
              Confirm
            </button>
          </div>
        </form>
        <p className="mt-10 text-center text-sm text-gray-500">
          No Code?{" "}
          <a
            href="/auth/sign-up"
            className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500"
          >
            Resend Code
          </a>
        </p>
      </div>
    </>
  );
}
export default ConfirmEmail;
Enter fullscreen mode Exit fullscreen mode

In the return statement, we render a form with an input field for the user to enter their confirmation code. The confirm function is triggered when the form is submitted. We also provide a link for users to request the confirmation code if they did not receive it initially.

Head over to your browser and enter the confirmation code sent to your email after signing up, and you should be redirected to the dashboard page. Then, navigate to your application's Amplify Studio dashboard. Under User Management, you should see that the user status has changed to "Confirmed" if the confirmation process was successful, as shown below.

Enable User Sign Out

To allow users sign out, we can define a component that renders a sign-out button. When the user clicks the button, they will be logged out of their account.

At the root of your project, create a folder named components and inside the folder, create a file named SignOutButton.js and add the following to it:

"use client";
import { Auth } from "aws-amplify";
import { useRouter } from "next/navigation";
import React from "react";
function SignOutButton() {
  const router = useRouter();
  return (
    <button
      type="button"
      className="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
      onClick={async (e) => {
        try {
          await Auth.signOut();
          router.push("/auth/sign-in");
        } catch (error) {
          console.log("error signing out: ", error);
        }
      }}
    >
      Sign out
    </button>
  );
}
export default SignOutButton;
Enter fullscreen mode Exit fullscreen mode

The SignOutButton component defined above renders a button that, when clicked, attempts to sign the user out of their current authenticated session using the Auth.signOut method. If the sign-out process is successful, the user is redirected to the sign-in page (which has not yet been created). If there is an error during the sign-out process, the error is logged to the console.

To use this button component in our app, let’s update the auth/dashboard/page.js file with the following:

//Import this
import SignOutButton from "@/components/SignOutButton";
import Link from "next/link";
import React from "react";
function Dashboard() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-center p-24">
      <p>This is your dashboard</p>
      <Link
        href="/"
        className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500"
      >
        Go to Landing Page
      </Link>
      {/* Add this */}
      <SignOutButton />
    </main>
  );
}
export default Dashboard;
Enter fullscreen mode Exit fullscreen mode

This component returns JSX code defining what authenticated users will see when they visit their dashboard. At the top of the file, we're importing the SignOutButton component we previously defined. We also render the SignOutButton component to the dashboard, allowing users to sign out from their dashboard.

If you click the Sign out button, you should be redirected to the Sign in page. Now, let’s go ahead and implement the logic for signing in.

Authenticate Users

In this section, we will update the SignIn component to collect users' credentials, authenticate them, and navigate them to their dashboard after successful authentication.

To do this, update the auth/sign-in/page.js file to include the following:

Let’s start with importing the libraries we need for the Sign in page:

"use client";
import React from "react";

import { Auth } from "aws-amplify";
import { useRouter } from "next/navigation";
Enter fullscreen mode Exit fullscreen mode

Next, we define a local state variable, user, that holds the user's email and password. We also initialize a router object to handle navigation.

//...

function SignIn() {
  const [user, setUser] = React.useState({
    email: "",
    password: "",
  });

  const router = useRouter();

  //...
}
export default SignIn;
Enter fullscreen mode Exit fullscreen mode

Let’s add a function that handles the sign in once the user submits the form:

function SignIn() {
  const [user, setUser] = React.useState({
   //...
  });

  const router = useRouter();

  //Add this
  async function signIn() {
    try {
      const auth = await Auth.signIn({
         username: user.email,
        password: user.password,
      });
      console.log(auth);
      router.push(`/dashboard`);
    } catch (error) {
      console.log("error signing up:", error);
    }
  }
  //...
}
export default SignIn;
Enter fullscreen mode Exit fullscreen mode

We defined an async function called signIn that uses the signIn method from the Auth library to sign in the user with their email and password. If the sign-in process is successful, it redirects the user to their dashboard page; otherwise, it logs any error that occurs.

Next, we hook up the function to the form:

"use client";
import React from "react";

import { Auth } from "aws-amplify";
import { useRouter } from "next/navigation";

function SignIn() {
  //...

  return (
        <>
          //...
            <form
              className="space-y-6"
              onSubmit={(e) => {
                e.preventDefault();
                console.log(user);
                signIn();
              }}
            >
              <div>
                <label
                  htmlFor="email"
                  className="block text-sm font-medium leading-6 text-gray-900"
                >
                  Email address
                </label>
                <div className="mt-2">
                  <input
                    id="email"
                    name="email"
                    type="email"
                    autoComplete="email"
                    onChange={(e) => {
                      const val = e.target.value;
                      if (val !== "") {
                        setUser({ ...user, email: val });
                      }
                    }}
                    required
                    className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
                  />
                </div>
              </div>
              <div>
                <div className="flex items-center justify-between">
                  <label
                    htmlFor="password"
                    className="block text-sm font-medium leading-6 text-gray-900"
                  >
                    Password
                  </label>
                  <div className="text-sm">
                    <a
                      href="/auth/forgot-password"
                      className="font-semibold text-indigo-600 hover:text-indigo-500"
                    >
                      Forgot password?
                    </a>
                  </div>
                </div>
                <div className="mt-2">
                  <input
                    id="password"
                    name="password"
                    type="password"
                    autoComplete="current-password"
                    onChange={(e) => {
                      const val = e.target.value;
                      if (val !== "") {
                        setUser({ ...user, password: val });
                      }
                    }}
                    required
                    className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
              />
            </div>
          </div>
          <div>
           <button
                  type="submit"
                  className="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
            >
              Sign in
            </button>
          </div>
        </form>
     //...
}

export default SignIn;
Enter fullscreen mode Exit fullscreen mode

The return statement renders a standard sign-in form with fields for email and password and a submit button. The onChange handlers for the input fields update the user state whenever the user types into the fields. When the form is submitted, it calls the signIn function. We also provide a link for users who have not signed up, which navigates to the sign-up page.

We have successfully set up sign-in functionality for our app. Now, you can open your application in the browser and enter the login credentials you used during sign-up. If authentication is successful, you will be automatically redirected to your dashboard.

Protect the Dashboard Page

Next, we need to secure the dashboard page by restricting access to only authenticated users. Currently, the dashboard page can be accessed by anyone, whether they are authenticated or not. We need to implement the logic that intercepts unauthenticated users attempting to access the dashboard and redirects them to the sign-in page instead.

We need to configure Amplify again because this layout is server-rendered, and Amplify library and features will not be shipped to the client. Our Auth layout and pages are client rendered and also need the Auth library. Since this Amplify config we are doing in the root layout won't be accessible to client pages, we are configuring Amplify again in the auth layout component.

Update your app/layout.js file to import and load the configuration file:

import "./globals.css";
import { Inter } from "next/font/google";

// Add these
import { Amplify } from "aws-amplify";
import awsExports from "../aws-exports";
Amplify.configure({ ...awsExports, ssr: true });

const inter = Inter({ subsets: ["latin"] });

export const metadata = {
  title: "Your Next.js App",
  description:
    "A Next.js App that shows you to protect pages in Next.js app directory",
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the file above, we initialize and configure the AWS Amplify library. The { ...awsExports, ssr: true } syntax is used to spread the properties of the awsExports object into a new object, and we added the ssr property set to true because we want to configure the Amplify library to work correctly with Next.js's server-side rendering (SSR) feature. The ssr: true setting allows Amplify to correctly manage user sessions when your pages are rendered on the server.

Let's update our dashboard page to render the dashboard only for authenticated users and redirect unauthenticated users to the sign-in page. Replace the code in your auth/dashboard/page.js file with the following:

import SignOutButton from "@/components/SignOutButton";
import Link from "next/link";
import React from "react";

import { headers } from "next/headers";
import { withSSRContext } from "aws-amplify";
import { redirect } from "next/navigation";

async function Dashboard() {
  const req = {
    headers: {
      cookie: headers().get("cookie"),
    },
  };

  const { Auth } = withSSRContext({ req });

  try {
    const user = await Auth.currentAuthenticatedUser();
    return (
      <main className="flex min-h-screen flex-col items-center justify-center p-24">
        <p>This is your dashboard, {user.attributes.name}.</p>
        <Link
          href="/"
          className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500"
        >
          Go to Landing Page
        </Link>
        <SignOutButton />
      </main>
    );
  } catch (error) {
    console.log(error);
    redirect("/auth/sign-in");
  }
}

export default Dashboard;
Enter fullscreen mode Exit fullscreen mode

In this code file, we first import headers in order to get the headers from the HTTP request. We also import withSSRContext, a function from AWS Amplify, to interact with Amplify functionalities on the server side. The redirect function is used to redirect users to different pages as needed.

To validate whether a user is authenticated on the server side, we build a req object to use the cookie stored by AWS Amplify for user sessions. We pass this req object to the withSSRContext function to get Amplify's Auth object, which can check if a user is authenticated.

In the try block, we use the currentAuthenticatedUser function to fetch the current user's details. If the user is authenticated, the function will return the user's data and we render the user's dashboard page. If not, we log the error to the console and redirect the user to the sign-in page.

Now, if you try to access the dashboard directly after logging out of your application, you will be redirected to the sign-in page.

Add the 'Forgot Password' Functionality

Next, let us move on to implementing the feature that allows a user to input their email address and request a password reset. This involves sending a reset code to the provided email. If the code is successfully authenticated, the user will be redirected to the password change page.

Update the code in your auth/forgot-password/page.js file with the following:

"use client";
import React from "react";

//Import these
import { Auth } from "aws-amplify";
import { useRouter } from "next/navigation";

function ForgotPassword() {
  const [email, setEmail] = React.useState("");

  //Add these
  const router = useRouter();
  async function sendCode() {
    try {
      await Auth.forgotPassword(email);
      router.push(`/auth/change-password?email=${email}`);
    } catch (error) {
      console.log("error sending code", error);
    }
  }

  return (
        <>
          <div className="flex min-h-full flex-1 flex-col justify-center px-6 py-12 lg:px-8">
            <div className="sm:mx-auto sm:w-full sm:max-w-sm">
              <h2 className="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
                Forgot Password
              </h2>
            </div>
            <div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
          <form
            className="space-y-6"
            onSubmit={(e) => {
              e.preventDefault();
              sendCode();
            }}
          >
            //...omitted for brevity
          </form>
          //...
  );
}

export default ForgotPassword;
Enter fullscreen mode Exit fullscreen mode

In this file, we import the necessary libraries and modules. We then declare a state variable email with its initial value set to an empty string.

We define an async function called sendCode, which uses the Auth.forgotPassword() method to initiate the password recovery process. If the request succeeds, the user is redirected to the change-password (which we have yet to create) page with their email as a query parameter. If an error occurs, it gets logged to the console.

The return statement renders a form that includes an input field for the email address, which updates the email state each time the user types into it. When the form is submitted, it calls the sendCode function.

Now, if you go to the sign-in page and click on the "forgot password" link, you will be taken to the password reset page. After entering your email address, a unique code will be sent to your email, and you will be directed to the password change page.

Add the 'Change Password' Functionality

To complete the password reset flow, we need to implement the functionality for the change password page. Once the user enters the code sent to their email and submits a new password, they should be able to successfully log in with their updated credentials.

Update the code in your auth/change-password/page.js file with the following:

"use client";
import React from "react";

// Import these
import { useSearchParams } from "next/navigation";
import { Auth } from "aws-amplify";
import { useRouter } from "next/navigation";

function ChangePassword() {
  const [pass, setPass] = React.useState({
    code: "",
    password: "",
  });

  const router = useRouter();
  const searchParams = useSearchParams();

  //Add this
  async function changePassword() {
    const email = searchParams.get("email");
    try {
      await Auth.forgotPasswordSubmit(email, pass.code, pass.password);
      router.push("/auth/sign-in");
    } catch (error) {
      console.log("error changing password", error);
    }
  }

  return (
        <>
          <div className="flex min-h-full flex-1 flex-col justify-center px-6 py-12 lg:px-8">
            <div className="sm:mx-auto sm:w-full sm:max-w-sm">
              <h2 className="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
                Change Password
              </h2>
            </div>
            <div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
          <form
            className="space-y-6"
            onSubmit={(e) => {
              e.preventDefault();
              changePassword();
            }}
          >
            //...omitted for brevity
          </form>
            //...
  );
}
export default ChangePassword;
Enter fullscreen mode Exit fullscreen mode

At the beginning of the file, we import the necessary modules. We also import useSearchParams hook to access query parameters from the current URL.

Within the ChangePassword component, we declare state variables for the password change code and the new password. Next, we define a changePassword function. It fetches the user's email from the URL's search parameters and calls the Auth.forgotPasswordSubmit method, passing in the email, code, and new password. If the password change is successful, we redirect the user to the sign-in page.

In the return statement, we render a form where users can enter the code they received in their email and their new password. Form submission triggers the changePassword function.

Now, you can copy the code sent to your email and input it into the provided form along with your new desired password. If the form is submitted successfully, you should be redirected to the sign in page.

After successfully changing your password, when you proceed to sign into your account, you will be automatically redirected to your dashboard.

Clean Up

To ensure that you don’t have any unused resources in you AWS account, run the following command to delete all the resources that were created in this project if you don’t intend to keep them.

amplify delete
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this guide, we learned how to make our Next.js pages secure by using AWS Amplify to implement a robust custom authentication system. We covered important features like signing up, signing in, and dashboard access, as well as handling edge cases like password management. With these functionalities in place, users can experience a secure and seamless experience while using your app. I hope this guide has helped you in building a secure and reliable platform for your users. Thank you for reading!

Top comments (4)

Collapse
 
izh24e3849921 profile image
izh • Edited

this is a great tutorial - a comprehensive one that everything a-z. I really appreciate this article. Have a request, are you able to write a similar tutorial for prebuilt AWS Amplify components Authentication and WithAuthenticator (HOC) and using them to secure App Routes. I guess HOC doesn't provide option for customize experience but Authentication piece does to an extent ...

Collapse
 
codebeast profile image
Christian Nwamba

Thanks for the kind words. I will add this to my next topic list.

Collapse
 
izh24e3849921 profile image
izh

I have another topic suggestion! There is not much available on AWS Amplify and how it handles GraphQL enum fields (select field / dropdown) when it comes to rendering and updating data for them. AWS Components or Component Collections will not allow data binding to enum or select fields. It would be nice to get a tutorial on it. AWS has zero info on it in their examples or data patterns and I am not seeing public topics either.

Collapse
 
rifath22 profile image
rifath22

Amazing tutorial. Can you please share this code repository ?