# Installation

# Prerequisites

Before you start, make sure to have commercetools and Auth0 accounts and working Vue Storefront 2 project.

# Manual installation

There are several steps required to install this integration manually. The whole process can be split into three parts:

  • Configuration of the commercetools project.
  • Configuration of the Auth0 tenant.
  • Installation in Vue Storefront 2 project.

# Configure commercetools project

# Create API client for Auth0 Actions

In commercetools Merchant Center, open the Settings > Developer settings page and create a new API client with the following permissions selected:

  • Manage > Customers,
  • Manage > Orders,
  • View > Orders.

Click the "Create API client" button and save the credentials for later.

# Enable introspection

Before enabling introspection, we need to generate a long and random string. It will be included in the requests and used to verify if commercetools servers initiated the introspection.

You can use any randomly generated string. One way is to use the following command on systems with openssl installed:

openssl rand -hex 32

Save this string for later.

Go to commercetools ImpEx (opens new window) and log in using your commercetools credentials. Once logged in, open the GraphQL IDE page and select a proper project from the dropdown.

With proper project selected execute the following query:

query project {
  project {
    version
  }
}

Copy the version number and use it to execute the next query:

mutation UpdateExternalOAuth {
  project: updateProject(version: <VERSION>, actions: [ # Change <VERSION> to the number from previous query
    {
      setExternalOAuth: {
        externalOAuth: {
          url: "<URL>/api/auth0/introspect", # Change <URL> to the public-facing, always available environment, that will be used to introspect for all environments.
          authorizationHeader: "<HEADER>" # Use the random string generated before. Keep it for later.
        }
      }
    }
  ]) {
    externalOAuth {
      url
      authorizationHeader
    }
  }
}

# Configure Auth0 tenant

# Create new tenant

Create a new tenant in Auth0. Select a region close to the one used in commercetools and your Vue Storefront servers. Close location is essential because, on every first request made using a given access token, commercetools will send a token introspection (opens new window) request to the Vue Storefront application. If it doesn't receive a response in a concise time window (500 milliseconds by default), the request will fail.

# Create new application

Go to the Applications > Applications page and create a new Application with the type Regular Web Application. Once created, go to the Settings tab and update the following fields:

  • in the Allowed Callback URLs field add URLs to all environments (including localhost) followed by /api/auth0/callback/. Example:

  • in the Allowed Logout URLs field add URLs to all environments (including localhost) followed by /api/auth0/postLogout. Example:

  • in the Allowed Web Origins field add URLs to all environments (including localhost). Example:

Save the changes.

Callback URL should end with a slash

Note that slash (/) at the end of each callback URL defined in Allowed Callback URLs is required due to the bug reported on Auth0 forums (opens new window).

Your configuration might look like this:

# Allowed Callback URLs
http://localhost:3000/api/auth0/callback/, https://example.com/api/auth0/callback/

# Allowed Logout URLs
http://localhost:3000/api/auth0/postLogout, https://example.com/api/auth0/postLogout

# Allowed Web Origins
http://localhost:3000, https://example.com

# Create new API

Go to the Applications > APIs page and create a new API with an identifier https://commercetools.com/. Once created, go to the "Permissions" tab and add all commercetools scopes you would like the customers to have when they log in. Refer to commercetools Scopes documentation (opens new window) for a list of available scopes.

For example, you can define the following scopes:

Permission Description
create_anonymous_token:<PROJECT_NAME> Create anynoymous token
manage_my_profile:<PROJECT_NAME> Manage user profile
view_categories:<PROJECT_NAME> View product categories
manage_my_payments:<PROJECT_NAME> Manage user payments
manage_my_orders:<PROJECT_NAME> Manage user orders
manage_my_shopping_lists:<PROJECT_NAME> Manage user shopping lists
view_published_products:<PROJECT_NAME> View published products
view_stores:<PROJECT_NAME> View stores

# Create Action to register users in commercetools

Open the Actions > Custom Actions and click on the "Create" button. Add an Action with the name "Register in CT on the first login" and "Login / Post Login" trigger.

Add new secrets by clicking the key icon on the left side. Most of them was generated in Create API client for Auth0 Actions section:

  • PROJECT_KEY
  • CLIENT_ID
  • CLIENT_SECRET
  • API_HOST
  • AUTH_HOST - https://auth.sphere.io.

Then, add modules by clicking the package icon on the left side:

  • Name: @commercetools/sdk-client, version: 2.1.2
  • Name: @commercetools/sdk-middleware-auth, version: 6.1.4
  • Name: @commercetools/sdk-middleware-http, version: 6.0.11
  • Name: node-fetch, version: 2.6.1

The next step is to replace the code in the editor with the following:

/**
* Handler that will be called during the execution of a PostLogin flow.
*
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
exports.onExecutePostLogin = async (event, api) => {
  if (event.user.app_metadata.commercetools_user_id) {
    return;
  }

  const crypto = require("crypto");
  const fetch = require('node-fetch');
  const { createClient } = require('@commercetools/sdk-client');
  const { createHttpMiddleware } = require('@commercetools/sdk-middleware-http');
  const { createAuthMiddlewareForClientCredentialsFlow } = require('@commercetools/sdk-middleware-auth');

  const projectKey = event.secrets.PROJECT_KEY;

  const authMiddleware = createAuthMiddlewareForClientCredentialsFlow({
    host: event.secrets.AUTH_HOST,
    projectKey,
    credentials: {
      clientId: event.secrets.CLIENT_ID,
      clientSecret: event.secrets.CLIENT_SECRET,
    },
    scopes: [`manage_customers:${projectKey}`],
    fetch
  });

  const httpMiddleware = createHttpMiddleware({
    host: event.secrets.API_HOST,
    fetch
  });

  const client = createClient({
    middlewares: [authMiddleware, httpMiddleware],
  });

  const response = await client.execute({
    uri: `/${projectKey}/customers`,
    method: 'POST',
    body: {
      email:  event.user.email,
      password: crypto.randomBytes(64).toString('base64'),
      externalId: event.user.user_id
    }
  });

  api.user.setAppMetadata('commercetools_user_id', response.body.customer.id);
};


/**
* Handler that will be invoked when this action is resuming after an external redirect. If your
* onExecutePostLogin function does not perform a redirect, this function can be safely ignored.
*
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
// exports.onContinuePostLogin = async (event, api) => {
// };

Click the "Save draft" and "Deploy" buttons.

# Create Action to merge guest and user carts after login

Open the Actions > Custom Actions page and click on the "Create" button and add an Action with the name "Merge carts" and "Login / Post Login" trigger.

Add new secrets by clicking the key icon on the left side. Most of them was generated in Create API client for Auth0 Actions section:

  • PROJECT_KEY
  • CLIENT_ID
  • CLIENT_SECRET
  • API_HOST
  • AUTH_HOST - https://auth.sphere.io.

Then, add modules by clicking the package icon on the left side:

  • Name: @commercetools/api-request-builder, version: 5.6.3.
  • Name: @commercetools/sdk-client, version: 2.1.2.
  • Name: @commercetools/sdk-middleware-auth, version: 6.1.4.
  • Name: @commercetools/sdk-middleware-http, version: 6.0.11.
  • Name: node-fetch, version: 2.6.1.

The next step is to replace the code in the editor with the following:

const fetch = require('node-fetch');
const { createClient } = require('@commercetools/sdk-client');
const { createHttpMiddleware } = require('@commercetools/sdk-middleware-http');
const { createAuthMiddlewareForClientCredentialsFlow } = require('@commercetools/sdk-middleware-auth');
const { createRequestBuilder } = require('@commercetools/api-request-builder');

async function getGuestCart(client, projectKey, anonymousId) {
  const uri = createRequestBuilder({ projectKey })
    .carts
    .where(`anonymousId = "${anonymousId}"`)
    .perPage(1)
    .page(1)
    .build();

  const response = await client.execute({
    uri,
    method: 'GET',
  });

  return response.body.results[0];
}

async function getUserCart(client, projectKey, commercetoolsUserId, currencyCode) {
  const uri = createRequestBuilder({ projectKey })
    .carts
    .where(`customerId = "${commercetoolsUserId}"`, `totalPrice.currencyCode = "${currencyCode}"`)
    .parse({
      sort: [
        { by: 'lastModifiedAt', direction: 'desc' }
      ]
    })
    .perPage(1)
    .page(1)
    .build();

  const response = await client.execute({
    uri,
    method: 'GET',
  });

  return response.body.results[0];
}

async function mergeCarts(client, projectKey, guestCart, userCart) {
  const actions = guestCart.lineItems.map(lineItem => ({
    action: 'addLineItem',
    quantity: lineItem.quantity,
    sku: lineItem.variant.sku
  }));

  const response = await client.execute({
    uri: `/${projectKey}/carts/${userCart.id}`,
    method: 'POST',
    body: {
      version: userCart.version,
      actions
    }
  });

  return response.body;
}

async function assignCartToUser(client, projectKey, guestCart, commercetoolsUserId) {
  const response = await client.execute({
    uri: `/${projectKey}/carts/${guestCart.id}`,
    method: 'POST',
    body: {
      version: guestCart.version,
      actions: [
        {
          action: 'setCustomerId',
          customerId: commercetoolsUserId
        }
      ]
    }
  });

  return response.body;
}

/**
* Handler that will be called during the execution of a PostLogin flow.
*
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
exports.onExecutePostLogin = async (event, api) => {
  const anonymousId = event.request.query.anonymousId;

  if (!anonymousId) {
    return;
  }

  const projectKey = event.secrets.PROJECT_KEY;

  const authMiddleware = createAuthMiddlewareForClientCredentialsFlow({
    host: event.secrets.AUTH_HOST,
    projectKey,
    credentials: {
      clientId: event.secrets.CLIENT_ID,
      clientSecret: event.secrets.CLIENT_SECRET,
    },
    scopes: [
      `view_orders:${projectKey}`,
      `manage_orders:${projectKey}`
    ],
    fetch
  });

  const httpMiddleware = createHttpMiddleware({
    host: event.secrets.API_HOST,
    fetch
  });

  const client = createClient({
    middlewares: [authMiddleware, httpMiddleware],
  });

  const guestCart = await getGuestCart(
    client,
    projectKey,
    anonymousId
  );

  if (!guestCart.lineItems.length) {
    // Guest cart is empty
    return;
  }

  const userCart = await getUserCart(
    client,
    projectKey,
    event.user.app_metadata.commercetools_user_id,
    guestCart.totalPrice.currencyCode
  )

  if (!userCart) {
      // Assign guestCart to user if he has no cart whose currency matches the anonymous cart 
      // or has no cart at all
      return await assignCartToUser(
        client,
        projectKey,
        guestCart,
        event.user.app_metadata.commercetools_user_id
      );
  }

  // Merge carts if user has a cart already
  await mergeCarts(client, projectKey, guestCart, userCart)
};


/**
* Handler that will be invoked when this action is resuming after an external redirect. If your
* onExecutePostLogin function does not perform a redirect, this function can be safely ignored.
*
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
// exports.onContinuePostLogin = async (event, api) => {
// };

Click the "Save draft" and "Deploy" buttons.

# Create Action to add user-specific claims

Open the Actions > Custom Actions page and click on the "Create" button and add an Action with the name "Add user-specific claims" and "Login / Post Login" trigger.

Replace the code in the editor with the following:

exports.onExecutePostLogin = async (event, api) => {
  api.accessToken.setCustomClaim('https://commercetools.com/customer_id', event.user.app_metadata.commercetools_user_id);
};

Click the "Save draft" and "Deploy" buttons.

# Update Login flow

Open Actions > Flows > Login page. Drag in newly created Actions from the right side to create the following flow:

              Start
                ↓
Register in CT on the first login
                ↓
           Merge carts
                ↓
     Add user-specific claims
                ↓
             Complete

Click the "Apply" button.

# Install in Vue Storefront 2 project

# Install the package

Install the integration package from our private registry. Please refer to How to use Vue Storefront Enterprise (opens new window) document if you are not already logged in:

yarn add @vsf-enterprise/ct-auth0

# Update imports

Auth0 integration package ships with a single useAuth0 composable. The next step is to replace the login, register, and logout methods imported from useUser with their equivalents imported from useAuth0.


  import { useUser } from '@vue-storefront/commercetools';
+ import { useAuth0 } from '@vsf-enterprise/ct-auth0';

  export default {
    setup () {
-     const { login, register, logout, user, load, setUser } = useUser();
+     const { user, load, setUser } = useUser();
+     const { login, register, logout } = useAuth0;
    }
  }

# Update Nuxt configuration

Open the nuxt.config.js and:

  • Add ['@vsf-enterprise/ct-auth0/nuxt', {}] to buildModules array.
  • Add @vsf-enterprise/ct-auth0 to dev and prod in useRawSource in @vue-storefront/nuxt module.

# Update "My Account" page

In pages/MyAccount.vue, remove is-authenticated from the middleware and replace it with the isAuthenticated middleware:

// pages/MyAccount.vue
import { isAuthenticated } from '@vsf-enterprise/ct-auth0';

middleware: [
  isAuthenticated
]

Then, remove the "Password change" tab from "My account > My profile".

# Update Server Middleware configuration

Open the middleware.config.js file and add configuration for auth0. Below is the minimal configuration required to make this package working. Refer to the Configuration interface for more details.

// middleware.config.js
const { tokenExtension } = require('@vsf-enterprise/ct-auth0/extensions');

const appURL = process.env.NODE_ENV === "production"
  ? '' // Test or staging environment URL. Should NOT end with "/"
  : 'http://localhost:3000';

module.exports = {
  integrations: {
    ct: {
      location: '@vue-storefront/commercetools-api/server',
      extensions: existing => existing.concat(
        // other extensions
        tokenExtension
      ),
    },
    auth0: {
      location: '@vsf-enterprise/ct-auth0/server',
      configuration: {
        api: {
          appURL,
          authorizationHeader: '', // String generated in the "Enable introspection" section
        }
        oidc: {
          baseURL: `${ appURL }/api`,
          issuerBaseURL: '<URL>', // "Domain" from "Applications > Applications>"
          clientID: '<ID>', // "Client ID" from "Applications > Applications>"
          clientSecret: '<SECRET>', // "Client Secret" from "Applications > Applications>"
          secret: '', // Randomly generated string 
          authorizationParams: {
            audience: 'https://commercetools.com/',
            scope: [
              'openid',
              // List of customer scopes defined in the "Create new API" section
            ].join(' ')
          }
        }
      }
    }
  }
};

# Deploy the changes

Deploy your changes to the environment configured in the Enable introspection section. Otherwise, the commercetools server won't be able to introspect the access tokens included in Vue Storefront requests and throw errors.