# Customer sign-in After a Customer has created an account and verified their email address, the next step is to enable them to sign in. The following sections cover 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. For the canonical reference on Customer authentication and how global and Store-specific Customers differ, see [Customer authentication](/api/customers-overview.md#customer-authentication-sign-in). ## 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**: [Authenticate (sign in) Customer](/search.md?urn=ctp:api:endpoint:/{projectKey}/login:POST) (`POST /{projectKey}/login`). - **Request**: the payload is a [CustomerSignin](/search.md?urn=ctp:api:type:CustomerSignin) object. In practice, you always send `email` and `password`, and you optionally send `anonymousCartId` or `anonymousId` when you want to assign guest resources during sign-in. If you pass `anonymousCartId`, you can also set `anonymousCartSignInMode`. For merge behavior details, see [Cart merge during sign-in and sign-up](/api/customers-overview.md#cart-merge-during-sign-in-and-sign-up) and [Merge a Cart](/api/carts-orders-overview.md#merge-a-cart). - **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: ```ts 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 { // 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. Store-specific sign-in is essential for Customers who are exclusively associated with one Store. - **Endpoint**: [Authenticate (sign in) Customer in Store](/search.md?urn=ctp:api:endpoint:/{projectKey}/in-store/key={storeKey}/login:POST) (`POST /{projectKey}/in-store/key={storeKey}/login`). - **Request**: the request body is identical to the global sign-in method. - **Behavior**: - The Store-specific sign-in 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. ```ts 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 { // 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 { 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. For the error type definition, see [InvalidCredentialsError](/search.md?urn=ctp:api:type:InvalidCredentialsError). - **Error Message Structure**: ```json { "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 or email or password that's incorrect. Generic error messages prevent attackers from gaining information about account existence and reduce the risk of user enumeration attacks. ## Manage user sessions after sign-in commercetools 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. For more information on Customer permissions and data protection, see [Customer permissions](/api/customers-overview.md#customer-permissions). 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 - commercetools 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. commercetools doesn't handle session management. Token management and best practices are discussed in more detail later in the module. You've now grasped the core concepts of the Customer sign-in flow in commercetools, 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. ## Related pages - [Area overview page with navigation](/learning-implement-carts-and-shopping-lists.md) - [Previous page: Customer sign-up and email verification](/learning-implement-carts-and-shopping-lists/manage-signups-and-signins/customer-signup-and-email-verification.md) - [Next page: Password reset flow](/learning-implement-carts-and-shopping-lists/manage-signups-and-signins/password-reset-flow.md) - [Search documentation and API specs](/search.md)