Customer sign-in

Learn how to authenticate Customers by using global and Store-specific sign-in methods and how to securely resolve login errors and user sessions.

Copy for LLM
View as Markdown

After completing this page, you should be able to:

  • Differentiate between global and Store-specific sign-in methods to authenticate Customers correctly.

  • Implement the Customer sign-in functionality that securely handles the InvalidCredentials error.

After a Customer has created an account and verified their email ID, the next step is to enable them to sign in. This section covers the two primary methods for Customer sign-in by using the Composable Commerce API: global sign-in and Store-specific sign-in. You'll also learn how to handle authentication errors securely and manage user sessions after a successful login.

Standard sign-in (global Customers)

For Customers who aren't exclusively tied to a specific Store (global Customers), the standard sign-in endpoint is the primary method for authentication.

  • Endpoint: POST /{projectKey}/login
  • Request:
    • email: the Customer's registered email address.
    • password: the Customer's password.
    • anonymousCartId (optional): the ID of an anonymous Cart. If provided, the Cart can be merged or assigned to the Customer post login, creating a seamless shopping experience.
    • anonymousCartSignInMode (optional): controls how an anonymousCartId is managed.
      • MergeWithExistingCustomerCart (default): merges the anonymous Cart with the Customer's existing active Cart. If no active Cart exists, then the anonymous Cart becomes the Customer's active Cart.
      • UseAsNewActiveCustomerCart: the anonymous Cart becomes the Customer's new active Cart, deactivating any pre-existing Cart.
    • anonymousId (optional): the ID of an anonymous session, which is used to link other anonymous resources to the Customer.
  • Outcome: On successful authentication, the API returns the following items:
    • customer: the entire Customer object.
    • cart (optional): the active Cart object, which can be a merged Cart, a newly assigned Cart, or a pre-existing Cart.

Example - Global Customer sign-in

The following example demonstrates a global Customer login, including the optional merging of an anonymous Cart:

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

// Function to handle customer login
async function customerLogin(
  email: string,
  password: string,
  anonymousCartId?: string
): Promise<CustomerSignInResult> {
  // Explicitly define the return type
  try {
    const loginResponse = await apiRoot
      .login()
      .post({
        body: {
          email: email,
          password: password,
          anonymousCartId: anonymousCartId,
          // anonymousCartSignInMode: 'MergeWithExistingCustomerCart' is default
        },
      })
      .execute();

    console.log("Login successful!");
    console.log("Customer details:", loginResponse.body.customer);
    if (loginResponse.body.cart) {
      console.log("Customer cart details:", loginResponse.body.cart);
    }
    return loginResponse.body;
  } catch (error: any) {
    // Type 'any' for caught error for now, as stricter typing for HTTP errors can be complex
    console.error("Login failed:", error);
    if (error.statusCode === 400 && error.body && error.body.errors) {
      error.body.errors.forEach((err: { code: string; message: string }) => {
        // Type for individual error objects
        if (err.code === "InvalidCredentials") {
          console.error("Reason: Invalid email or password provided.");
        }
      });
    }
    throw error;
  }
}

// Example login for Alice
(async () => {
  try {
    // Simulate Alice having an anonymous cart before login
    const anonymousCartId = "anonymous-cart-id-123"; // Replace with a real anonymous cart ID if testing

    console.log("Attempting global login for Alice...");
    const loginResult = await customerLogin(
      "alice.smith@example.com",
      "StrongPassword123!",
      anonymousCartId
    );
    // After successful login, your BFF should generate a secure session for Alice.
    console.log(
      `Alice logged in successfully. Customer ID: ${loginResult.customer.id}`
    );
  } catch (err) {
    console.error("Global login attempt failed.");
  }
})();

Store-specific sign-in

For multi-Store solutions, you can authenticate Customers within the context of a specific Store. This is essential for Customers who are exclusively associated with one Store.

  • Endpoint: POST /{projectKey}/in-store/key={storeKey}/login
  • Request: the request body is identical to the global sign-in method.
  • Behavior:
    • This endpoint authenticates Customers who are associated with the specified storeKey. A Customer assigned exclusively to one Store must use that Store's endpoint to log in.
    • Global Customers (those not assigned to any specific Store) can also authenticate via any Store-specific endpoint.
    • Security: a Customer who is assigned only to Store A cannot log in via the endpoint for Store B. This enforces business rules and Customer segregation in multi-brand setups.
  • Use cases:
    • Multi-brand/multi-site setups: distinct websites or mobile apps for different brands, each using its specific Store key for login.
    • Regional Stores: geographically-distinct Stores with separate Customer bases or Product assortments.

Example - Store-specific Customer sign-in

This example shows a Customer, David, who is exclusively registered to the electronics-high-tech-store. It demonstrates that they can log in only via that Store's endpoint.
import { apiRoot } from "../../client"; // Assuming apiRoot is initialized and exported here
import { CustomerSignInResult } from "@commercetools/platform-sdk"; // Import the type for the response body

// Function to handle store-specific customer login
async function storeSpecificCustomerLogin(
  storeKey: string,
  email: string,
  password: string,
  anonymousCartId?: string
): Promise<CustomerSignInResult> {
  // Explicitly define the return type
  try {
    const loginResponse = await apiRoot
      .inStoreKeyWithStoreKeyValue({ storeKey: storeKey })
      .login()
      .post({
        body: {
          email: email,
          password: password,
          anonymousCartId: anonymousCartId,
        },
      })
      .execute();

    console.log(`Login successful for store "${storeKey}"!`);
    console.log("Customer details:", loginResponse.body.customer);
    if (loginResponse.body.cart) {
      console.log("Customer cart details:", loginResponse.body.cart);
    }
    return loginResponse.body;
  } catch (error: any) {
    // Type 'any' for caught error for now, as stricter typing for HTTP errors can be complex
    console.error(
      `Store-specific login failed for store "${storeKey}":`,
      error
    );
    if (error.statusCode === 400 && error.body && error.body.errors) {
      error.body.errors.forEach((err: { code: string; message: string }) => {
        // Type for individual error objects
        if (err.code === "InvalidCredentials") {
          console.error(
            "Reason: Invalid email/password or customer not associated with this store."
          );
        }
      });
    }
    throw error;
  }
}

// NOTE: The `customerLogin` function is needed for the example below,
// assuming it's defined and imported from another file or defined within the same context
// For demonstration, let's include a placeholder for it here if not imported:
/*
async function customerLogin(email: string, password: string, anonymousCartId?: string): Promise<CustomerSignInResult> {
  try {
    const loginResponse = await apiRoot.login().post({ body: { email, password, anonymousCartId } }).execute();
    console.log(`  Global Login SUCCESS for ${email}.`);
    return loginResponse.body;
  } catch (error: any) {
    console.log(`  Global Login FAILED for ${email}: ${error.statusCode} - ${error.message}`);
    throw error;
  }
}
*/

// Example login for David, who is a 'electronics-high-tech-store' customer
(async () => {
  const ELECTRONICS_HIGH_TECH_STORE_KEY = "electronics-high-tech-store"; // Assuming this store key exists in your project
  const ZENITH_LIVING_STORE_KEY = "zenith-living-store"; // Assuming this store key exists

  console.log(
    `\nAttempting store-specific login for David at ${ELECTRONICS_HIGH_TECH_STORE_KEY}...`
  );
  try {
    const loginResult = await storeSpecificCustomerLogin(
      ELECTRONICS_HIGH_TECH_STORE_KEY,
      "david.shopper@example.com", // Assuming David's email
      "DavidSecurePass!" // Assuming David's password
    );
    console.log(
      `David logged in successfully to ${ELECTRONICS_HIGH_TECH_STORE_KEY}. Customer ID: ${loginResult.customer.id}`
    );
  } catch (err) {
    // Note to learner: Implement proper error handling
    console.error(
      `Store-specific login attempt failed for ${ELECTRONICS_HIGH_TECH_STORE_KEY}.`
    );
  }

  // What happens if David tries to log in via the global endpoint?
  console.log(
    `\nAttempting GLOBAL login for David (should fail if he's store-specific only)...`
  );
  try {
    // This `customerLogin` function needs to be available in this scope,
    // either imported or defined. Assuming it's the one from the previous snippet.
    await customerLogin("david.shopper@example.com", "DavidSecurePass!");
  } catch (err: any) {
    // Note to learner: Implement proper error handling
    console.error(
      `As expected, global login failed for David (if he's store-specific only). Error: ${err.message}`
    );
  }

  // What happens if David tries to log in to another store, say 'zenith-living-store'?
  console.log(
    `\nAttempting login for David at 'zenith-living-store' (should fail if he's not assigned)...`
  );
  try {
    await storeSpecificCustomerLogin(
      ZENITH_LIVING_STORE_KEY,
      "david.shopper@example.com",
      "DavidSecurePass!"
    );
  } catch (err: any) {
    // Note to learner: Implement proper error handling
    console.error(
      `As expected, login failed for David at 'zenith-living-store'. Error: ${err.message}`
    );
  }
})();

Understand the InvalidCredentials error

The InvalidCredentials error is the most common response to a failed sign-in attempt. This error message is intentionally kept generic to enhance security.
  • Error Message Structure:
{
  "statusCode": 400,
  "message": "Account with the given credentials not found.",
  "errors": [
    {
      "code": "InvalidCredentials",
      "message": "Account with the given credentials not found."
    }
  ]
}
  • Ambiguity by design: the error doesn't specify whether the email is wrong, the password is wrong, or if the customer isn't associated with the specified Store. This ambiguity is a deliberate security measure to prevent account enumeration attacks, where an attacker can otherwise determine valid emails or Store associations.
  • Security rationale: if the API returned specific errors like "Email not found" or "Customer not in store," then an attacker can exploit this to harvest user data. The generic message provides no actionable information to a potential attacker.
  • Developer impact: with this security feature, you can't determine the exact cause of a login failure from the API error message alone. For troubleshooting, especially for Store-specific issues, your backend can query for the Customer by email to verify their existence and check their stores array.
  • User interface messaging: your UI should reflect this ambiguity. Instead of "Incorrect password," use a generic message like, "The email or password you entered is incorrect. Please try again." After several failed attempts, it's a best practice to suggest a password reset without confirming whether the email address is registered.

A best practice for login failure messages is to use generic and non-specific error messages that don't reveal whether it's the username/email or password that's incorrect. This prevents attackers from gaining information about account existence and reduces the risk of user enumeration attacks.

Manage user sessions after sign-in

Composable Commerce is a headless platform that doesn't manage user sessions. Your application's backend, such as a backend for frontend (BFF), is responsible for session management after a successful sign-in.

The key responsibilities of your application's backend include the following:

  • Generate a secure session token: use a standard one such as a JSON web token (JWT) or a secure, random session ID.
  • Store the token securely on the client: use HttpOnly and Secure cookies for web applications or the operating system's secure storage for mobile apps.
  • Validate the token on subsequent requests: your backend must validate the token for any request that requires authentication before interacting with the Composable Commerce API.
  • Manage session expiration and renewal: implement token refresh mechanisms or force re-authentication after a specific period.

Key takeaways

  • Composable Commerce provides two main sign-in endpoints: a global one (/{projectKey}/login) and a Store-specific one (/{projectKey}/in-store/key={storeKey}/login).
  • Store-specific Customers must use the endpoint for the Store to which they are assigned. Global Customers can use either the global endpoint or any Store-specific endpoint.
  • The InvalidCredentials error message is intentionally kept generic to prevent account enumeration attacks. Your UI and error handling must ensure this by providing non-specific feedback to the user.
  • Your application backend is responsible for creating, managing, and validating user sessions after a successful login. Composable Commerce doesn't handle session management.

We'll discuss token management and best practices in more detail later in the module.

You've now grasped the core concepts of the Customer sign-in flow in Composable Commerce, including the critical distinctions for multi-store setups like Zen Electron's and how to handle the InvalidCredentials error securely. Next, we'll cover the essential password reset process.

Test your knowledge