Password reset flow

Learn how to implement a secure two-step password reset flow by using temporary tokens for the recovery of Customer accounts.

After completing this page, you should be able to:

  • Implement the complete two-step password reset flow, including token creation and password update by using the appropriate API endpoints.

  • Apply security best practices when implementing a password reset flow, such as managing token lifecycle and providing secure user feedback.

To manage forgotten passwords, implement a secure password reset flow. This process enables Customers to regain access to their accounts without compromising security. In this section, you'll learn how to implement a two-step password reset flow by using Composable Commerce, which is similar to email verification.

Request a password reset token

When a Customer indicates that they've forgotten their password, your application initiates the reset process by requesting a unique, temporary token from Composable Commerce. This token is then sent to the Customer's registered email address.

  • Endpoint: POST /{projectKey}/customers/password-token
    • To request a password reset token for a Customer in a Store, use the following endpoint: POST /{projectKey}/in-store/key={storeKey}/customers/password-token.
  • Request:
    • email: the email address of the Customer requesting the password reset. For security reasons, Composable Commerce doesn't indicate whether the email address exists in the system.
  • Response: returns a value that's the password reset token.
  • Action: after receiving the token, your backend sends it to the Customer's verified email address within a password reset link. For example, https://www.zenelectron.com/reset-password?token=TOKEN_VALUE. The Customer must click this link to proceed to the next step.
  • Considerations:
    • Token TTL: set a short time-to-live (TTL) for the token, such as 1-2 hours, to minimize security risks. The token must be invalidated immediately after use.

Example - Generate a password reset token

import { apiRoot } from '../../client'; // Assuming apiRoot is initialized and exported here
import { CustomerToken } from '@commercetools/platform-sdk'; // Import the type for the token response

// Function to request a password reset token
async function requestPasswordResetToken(email: string): Promise<string> {
  // Returns the token string
  try {
    const tokenResponse = await apiRoot
      .customers()
      .passwordToken()
      .post({
        body: {
          email: email,
          ttlMinutes: 60, // Token valid for 60 minutes (1 hour)
          invalidateOlderTokens: true, // Invalidate all password tokens previously issued for this Customer
        },
      })
      .execute();

    const customerToken: CustomerToken = tokenResponse.body;
    console.log(
      'Password reset token generated (but not sent to console for security):'
    );
    // For demonstration, we'll log, but in production, NEVER log the token directly.
    if (process.env.NODE_ENV !== 'production') {
      console.log('Token Value:', customerToken.value); // Access the value from the typed object
    }
    return customerToken.value;
  } catch (error: any) {
    // Type 'any' for caught error for now, as stricter typing for HTTP errors can be complex
    console.error('Error requesting password reset token:', error);
    // For security, always return a generic success message to the user,
    // regardless of whether the email exists or not.
    console.log(
      'If your email exists in our system, a password reset link has been sent.'
    );
    throw error;
  }
}

// Simulate Alice requesting a password reset
(async () => {
  const aliceEmail = 'alice.smith@example.com';
  console.log(`\n--- Initiating Password Reset Request for ${aliceEmail} ---`);
  try {
    const resetToken = await requestPasswordResetToken(aliceEmail);
    const resetLink = `https://www.zenelectron.com/reset-password?token=${resetToken}`;
    const emailData = {
      toName: 'Alice',
      toEmail: aliceEmail,
      subject: 'Password Reset for your Zen Electron Account',
      body: `
         Dear Alice,

         You have requested a password reset for your Zen Electron account.
         Please click on the following link to reset your password:
         ${resetLink}

         This link will expire in 1 hour.
         If you did not request a password reset, please ignore this email.
      `.trim(),
    };

    // Note to learner: Implement actual email sending logic inside sendPasswordResetEmail()
    sendPasswordResetEmail(emailData);
  } catch (err) {
    console.error('Failed to initiate password reset for Alice.');
  }
})();

Reset the password

After the Customer clicks the password reset link from their email, they are navigated to a page where they can enter a new password. Your application then sends this new password along with the token to Composable Commerce.

  • Endpoint: POST /{projectKey}/customers/password/reset
    • To reset the password for a Customer in a Store, use the following endpoint: POST /{projectKey}/in-store/key={storeKey}/customers/password/reset.
  • Request:
    • tokenValue: the password reset token from the email.
    • newPassword: the Customer's new password. Your backend must enforce strong password policies. For example, minimum length and character requirements.
  • Optional:
    • currentPassword: this field is available for password changes by logged-in users. However, for this scenario, the POST /{projectKey}/customers/password/change endpoint is more appropriate because it requires the id, version, currentPassword, and newPassword details.
  • Outcome: if the tokenValue is valid and not expired, then the Customer's password is updated. Otherwise, Composable Commerce returns an error message.
  • Security: always enforce strong password policies for the newPassword field. Your backend must validate password complexity before sending it to Composable Commerce. For added security, make sure that the Customer needs to log in with the new password to create an authenticated session.

Example - Reset a password

This example assumes your backend receives the tokenValue and newPassword details from the frontend form.
import { apiRoot } from '../../client'; // Assuming apiRoot is initialized and exported here
import { Customer } from '@commercetools/platform-sdk'; // Import the Customer type for the response

// Assuming `tokenValue` is received from the URL parameter and `newPassword` from a form
const receivedResetTokenValue = 'GENERATED_RESET_TOKEN_FROM_EMAIL'; // Replace with actual token
const aliceNewPassword = 'VerySecureNewPassword!2025'; // Ensure it meets strong policy

// Function to reset customer password
async function resetCustomerPassword(
  token: string,
  newPass: string
): Promise<Customer> {
  try {
    const customerResponse = await apiRoot
      .customers()
      .passwordReset()
      .post({
        body: {
          tokenValue: token,
          newPassword: newPass,
        },
      })
      .execute();

    const customer: Customer = customerResponse.body;
    console.log('Password successfully reset for customer:', customer.email);
    // After successful password reset, you might want to immediately log the user in
    // or redirect them to the login page.
    return customer;
  } catch (error: any) {
    // Type 'any' for caught error for now, as stricter typing for HTTP errors can be complex
    console.error('Error resetting password:', error);
    // Provide a generic error message to the user, e.g., "The password reset link is invalid or has expired."
    throw error;
  }
}

// Execute password reset
(async () => {
  try {
    console.log("\n--- Attempting to reset Alice's password ---");
    const updatedCustomer = await resetCustomerPassword(
      receivedResetTokenValue,
      aliceNewPassword
    );
    // After successful reset, you might auto-login Alice or prompt her to log in
    console.log(
      `Password reset for Alice successful! Her account is ready. Customer ID: ${updatedCustomer.id}`
    );
    // Note: Consider having the customer log in with the new password before creating an authenticated session.
    // This adds an extra layer of verification, ensuring the user actually knows the new password.
  } catch (err) {
    console.error("Failed to reset Alice's password.");
  }
})();

Entire password reset flow

The following diagram illustrates the complete password reset workflow:

Key takeaways

  • The password reset flow is a two-step process. First, request a token and then use that token to reset the password.
  • Use the POST /{projectKey}/customers/password-token endpoint to generate a secure, temporary token.
  • Always set a short time-to-live (TTL) on password reset tokens to enhance security.
  • Use the POST /{projectKey}/customers/password/reset endpoint with the token and new password to complete the reset.
  • Enforce strong password policies on your backend before sending the new password to the API.
  • For security reasons, don't provide feedback on whether an email address exists when a Customer requests a password reset.

You've now learned how to implement a secure password reset flow. Next, we'll explore how to handle the transition from an anonymous to an authenticated Customer session, ensuring a seamless experience.

Test your knowledge