Separate your product catalog from commerce.

Tutorials

Single-sign on with Commerce Layer using Next.js and Auth0.

March 24, 2023 Matteo Alessani

In our previous article, we explained the benefits that an authenticated ecommerce experience can provide with a personalized set of features, like saved addresses and payment sources, order history, dedicated markets and price lists, and so on.

In this post, we have created a dive deep into how easy it is to integrate Commerce Layer together with Auth0, one of the most famous identity providers, using Next.js. During this tutorial, we will create a project where:

  • An unauthenticated user (aka an anonymous visitor to your site) can see a product with a price as a guest.
  • A signup or sign-in leads to an authenticated customer’s order history.

Prerequisites

  • A general understanding of how APIs and the command-line work.
  • Basic HTML, CSS, and JavaScript knowledge.
  • Commerce Layer enterprise account or planning to move to one.
  • An organization where you have created the required commerce data resources for your market (you can follow the onboarding tutorial or manual configuration to achieve this).

Before we bet started, let's look at a sequence diagram that explains what we hope to achieve. Take some time reviewing this because we work on building all of it with this proof of concept.


Commerce Layer's authentication sequence
  1. User clicks on the login button.
  2. The Next.js endpoint (/api/auth/login) redirects to the Auth0 Login Dialog.
  3. After successful authentication, Auth0 calls the /api/auth/callback endpoint with a code.
  4. The Next.js endpoint exchanges the code with Auth0.
  5. Auth0 replies with an access token and an ID token.
  6. Using a Commerce Layer integration application, the customer is retrieved or created on Commerce Layer using their email.
  7. The customer ID is received from Commerce Layer.
  8. The user metadata on Auth0 is updated with the customer ID provided by Commerce Layer using an Auth0 M2M application.
  9. An Auth0 session is created on the browser.
  10. The protected endpoint /api/token can now be called as the user is logged in with Auth0.
  11. The customer ID is retrieved from the user's metadata on Auth0 using an Auth0 M2M application and is used to get the customer token with the JWT bearer flow.
  12. The customer token is retrieved on the frontend, allowing the user to interact with Commerce Layer APIs as the customer.

Getting started with Auth0

Auth0 is an identity platform to manage access to your applications. If you’re new to identity and access management (IAM), learn some of the basics and plan a solution that best fits your technology requirements. If you’re familiar with IAM, then jump in and start building. You can start a new project by following this Auth0 tutorial, which uses the popular web framework Next.js.

Today's proof of concept uses Next.js to show how to implement SSO with Auth0 and Commerce Layer. (By the way, there are many other frameworks you can use, but we’ve chosen Next.js.) We will show how to do this without using a lambda function or any other parts in the middle. It will also show how to use Commerce Layer's sales channel token and customer token to interact with Commerce Layer APIs

Once you have properly configured your Auth0 account and set up a project that allows users to sign up, sign in, and logout, you can use Auth0 to synchronize customers with Commerce Layer. And lastly, by following the steps in the tutorial, you can enable users to log in and view their order history. Got it? Let’s dive in…

Add Commerce Layer dependencies

To easily interact with Commerce Layer APIs, you should first add the following Commerce Layer packages: @commercelayer/js-auth, @commercelayer/sdk, and @commercelayer/react-components.

We recommend using pnpm as the package manager for this example, but it should be straightforward to use npm or yarn instead.

pnpm add @commercelayer/js-auth @commercelayer/sdk @commercelayer/react-components

To connect your project to Commerce Layer, you need to use an integration application. This application, with admin role, allows you to perform any CRUD action on any resource.

To set up the integration application, add the following environment variable to your .env.local file:

CL_INTEGRATION_CLIENT_ID=<your-integration-client-id>
CL_INTEGRATION_SECRET=<your-integration-secret>
NEXT_PUBLIC_CL_ENDPOINT=<your-organization-slug>

Next, you will need to use the js-auth package to obtain an integration token. The following code snippet shows how to do this using the /utils/getIntegration.js file:

import { authenticate } from '@commercelayer/js-auth';
import { CommerceLayer } from '@commercelayer/sdk';

export default async function getClient() {
  const token = await getIntegration();
  const client = CommerceLayer({
    accessToken: token,
    organization: process.env.NEXT_PUBLIC_CL_ENDPOINT
  });
  return client;
}

async function getIntegration() {
  const token = await authenticate('client_credentials', {
    clientId: process.env.CL_INTEGRATION_CLIENT_ID,
    clientSecret: process.env.CL_INTEGRATION_SECRET
  });
  return token.accessToken;
}

This token will always be used in a Next.js API endpoint and will never be exposed on the client side. The same level of attention should be applied to the Auth0 token that we will use to update user metadata.

Create a Machine to Machine (M2M) application on Auth0

You have two way to interact with the Auth0 APIs:

  1. Using the access token of the logged-in user, or
  2. using a new Machine to Machine application.

The first approach may be easier, but it requires requesting more permissions from the user during authentication, which can lead to security issues... like a user updating their own metadata. You can review Auth0's documentation to learn more about the limitations.

For our POC, we will follow the Machine to Machine approach and interact with Auth0 Management APIs. To do this, you need to create a new application on Auth0.

Auth0 Machine to Machine application creation

When using a free plan, be mindful of the limited number of tokens that can be requested per month (1000). These tokens expire after one day, so it is advisable to store them in memory (for example, using Redis) and reuse them across all the APIs that require access to the Auth0 Management API.

After creating the application, you must set the following environment variables inside the .env.local file:

AUTH0_M2M_CLIENT_ID=<your-machine-to-machine-client-id>
AUTH0_M2M_CLIENT_SECRET=<your-machine-to-machine-client-secret>
AUTH0_ISSUER_DOMAIN=<your-auth0-domain-without-protocol>

When a user interacts with Auth0, a callback URL is invoked and a payload is received that can be used to add logic for interacting with the Commerce Layer API. Double check that a callback URL (http://localhost:3000/api/auth/callback for our Next.js project) and a logout URL (http://localhost:3000/) have been setup within the Auth0 application that was created at the beginning:

Confirm the callback and logout URLs exist

And change the value of the environment variable AUTH0_SCOPE on .env.local file:

AUTH0_SCOPE='openid profile email'

Sync a user that authenticates to Commerce Layer

To ensure that a user is authenticated on both Commerce Layer and Auth0, we need to fetch or create the user on Commerce Layer and update their metadata on Auth0 with the customer ID provided by Commerce Layer. This can be achieved by receiving a callback when a user signs up or logs in. To do this, we need to add the following code inside /pages/api/[...auth0].js. This code will enable us to receive the callback and update the necessary information accordingly.

import { handleAuth, handleCallback } from '@auth0/nextjs-auth0';

import getOrCreateCustomer from '../../../utils/getOrCreateCustomer';

export default handleAuth({
  async callback(req, res) {
    try {
      await handleCallback(req, res, { afterCallback });
    } catch (error) {
      res.status(error.status || 500).end();
    }
  }
});

const afterCallback = async (req, res, session, state) => {
  await getOrCreateCustomer(session.user);
  return session;
};

We will now add the logic to update the metadata of an Auth0 user in the file /utils/getOrCreateCustomer.js.

import { ManagementClient } from 'auth0';
import getClient from './getIntegration';

export default async function getOrCreateCustomer(user) {
  const auth0UserId = user.sub;
  const client = await getClient();
  const customer = await client.customers.list({ filters: { email_eq: user.email } });
  let customerId;

  if (customer.length > 0) {
    console.log('User already exists, syncing ID on Auth0');
    customerId = customer[0].id;
  } else {
    console.log(`Creating customer ${user.name}`);
    const newCustomer = await client.customers.create({ email: user.email, password: Math.random().toString(36).slice(-8) });
    customerId = newCustomer.id;
  }

  const currentUserManagementClient = new ManagementClient({
    domain: process.env.AUTH0_ISSUER_DOMAIN,
    clientId: process.env.AUTH0_M2M_CLIENT_ID,
    clientSecret: process.env.AUTH0_M2M_CLIENT_SECRET,
    scope: 'update:users'
  });

  try {
    await currentUserManagementClient.updateUserMetadata({ id: auth0UserId }, { customerId });
  } catch (error) {
    console.error('Error on updating the user metadata:', error);
    throw error;
  }
}

In the above code, we obtain a Commerce Layer integration token. We then attempt to locate the customer by email on Commerce Layer. If the customer is not present, we create a new one. We retrieve the customer ID, obtain an Auth0 client, and update the user's metadata with the customerId attribute. Before running the project we need to add the auth0 missing package that we will use to interact with Auth0 Management APIs:

pnpm add auth0

Manage Commerce Layer tokens with a provider

Next, we need to add a React component to handle the Commerce Layer sales channel tokens. If the user is not authenticated with Auth0, the user will navigate in guest mode and the provider should return a sales channel token with the following permissions. However, if the user is authenticated with Auth0, the provider should return a customer token owned by the signed-in user. From that point forward, the user will interact on behalf of that user.

First of all we need to add the following credentials to get the sales channel token on .env.local:

NEXT_PUBLIC_CL_SALES_CHANNEL_CLIENT_ID=<your-sales-channel-client-id>
NEXT_PUBLIC_CL_MARKET=<your-market-number>

The provider will store the token on the local storage and check if it is still valid, reusing it until it expires. To check if the user is logged in or not, we will use the useUser hook from the @auth0/nextjs-auth0/client package. If the user is logged in, we will check if there is a valid customer token on the local storage. Otherwise, we will request it from the /api/token endpoint (which we will discuss later) and store the token. On the other hand, if the user is not logged in, we will remove the customer token from the local storage (if it's present), check if we have a still valid sales channel token, and if not, we will request a new one using the @commercelayer/js-auth package.

Here the file /providers/CommerceLayerAuth.jsx :

import React, { createContext, useContext, useEffect, useState } from 'react';
import { authenticate } from '@commercelayer/js-auth';
import { useUser } from '@auth0/nextjs-auth0/client';

const CommerceLayerAuthContext = createContext();

const getAuth = (kind) => {
  if (kind === 'sales') {
    localStorage.removeItem(getStoreKey('customer'));
  }
  const storeKey = getStoreKey(kind);
  return JSON.parse(localStorage.getItem(storeKey) || 'null');
};

const storeAuth = (kind, authReturn) => {
  if (!authReturn) {
    return null;
  }

  const storeKey = getStoreKey(kind);

  const auth = {
    accessToken: authReturn.accessToken,
    expires: authReturn.expires?.getTime()
  };

  localStorage.setItem(storeKey, JSON.stringify(auth));

  return auth;
};

const getStoreKey = (customer) => `clayer_token:${customer}`;

const hasExpired = (time) => time === undefined || time < Date.now();
const isValid = (auth) => !hasExpired(auth?.expires);

export const CommerceLayerAuthProvider = ({ children }) => {
  const { user, isLoading } = useUser();
  const [auth, setAuth] = useState(null);
  const [isLoadingAuth, setIsLoadingAuth] = useState(true);

  useEffect(() => {
    async function fetchToken() {
      if (!isLoading) {
        if (user) {
          const auth = getAuth('customer');
          if (isValid(auth)) {
            console.log('Get customer token from local storage');
            setAuth(auth);
          } else {
            console.log('Get customer token from /api/token');

            const response = await fetch('api/token').then((response) =>
              response.json()
            );

            setAuth(
              storeAuth('customer', {
                ...response,
                expires: new Date(response.expires)
              })
            );
          }
        } else {
          const auth = getAuth('sales');
          if (isValid(auth)) {
            console.log('Get sales token from local storage');
            setAuth(auth);
          } else {
            console.log('Get sales token with js-auth');
            const salesChannelToken = await authenticate('client_credentials', {
              clientId: process.env.NEXT_PUBLIC_CL_SALES_CHANNEL_CLIENT_ID,
              scope: process.env.NEXT_PUBLIC_CL_MARKET
            });

            const token = salesChannelToken.accessToken;

            if (token) {
              setAuth(storeAuth('sales', salesChannelToken));
            }
          }
        }
        setIsLoadingAuth(false);
      } else {
        setIsLoadingAuth(true);
      }
    }
    fetchToken();
  }, [user, isLoading]);

  return (
    <CommerceLayerAuthContext.Provider
      value={{ auth, isLoading: isLoadingAuth }}
    >
      {children}
    </CommerceLayerAuthContext.Provider>
  );
};

export const useCommerceLayerAuth = () => useContext(CommerceLayerAuthContext);

The provider will save two tokens in the local storage: the clayer_token:sales key for the sales channel token, and clayer_token:customer for the customer token. This approach avoids the need to request or create a new token if it is still valid during a page refresh or for a returning user. If the user logs out, we will delete the customer token from the local storage and use the previous sales channel token, provided that it is still valid. Now you need to use the provider on the /pages/_app.js file:

import React from 'react';
import { UserProvider } from '@auth0/nextjs-auth0/client';

import Layout from '../components/Layout';

import '@fortawesome/fontawesome-svg-core/styles.css';
import initFontAwesome from '../utils/initFontAwesome';
import '../styles/globals.css';
import { CommerceLayerAuthProvider } from '../providers/CommerceLayerAuth';

initFontAwesome();

export default function App({ Component, pageProps }) {
  return (
    <UserProvider>
      <CommerceLayerAuthProvider>
        <Layout>
          <Component {...pageProps} />
        </Layout>
      </CommerceLayerAuthProvider>
    </UserProvider>
  );
}

Request the customer token

To obtain a customer token, we need to use the JWT bearer flow on the /api/token API endpoint. The token will be used to make API calls to Commerce Layer as a customer. The JWT bearer flow is only available to Enterprise customers.

To use this flow we have to create an assertion containing the customer ID.

pnpm run dev

To obtain the customer token, we need to create a protected Next.js endpoint (/pages/api/token.js). This endpoint uses the withApiAuthRequired function from the @auth0/nextjs-auth0 package to ensure that the user is authenticated with Auth0.

First, we retrieve the user from the session to obtain the user ID on Auth0. Then, we fetch the user from Auth0 to obtain the Commerce Layer customer ID from the user's metadata. We then get the customer token using the JWT bearer flow.

We also added two headers on the request to the Auth API endpoing to avoid being rate limited making a server side request for each customer logging in. Those headers are:

x-backend-auth: this key is mandatory for using the x-true-client-ip header to forward the client IP address, which helps in managing rate limits effectively by identifying unique client requests. x-true-client-ip: allows to forward the client IP address in server-side requests. Rate limits will be based on individual client IP addresses rather than the server's IP address alone.

x-backend-auth key is available only to enterprise customers.

import { getSession, withApiAuthRequired } from '@auth0/nextjs-auth0';
import { ManagementClient } from 'auth0';
import { authenticate, createAssertion } from '@commercelayer/js-auth';

export default withApiAuthRequired(async function token(req, res) {
  try {
    const session = await getSession(req, res);

    const managementClient = new ManagementClient({
      domain: process.env.AUTH0_ISSUER_DOMAIN,
      clientId: process.env.AUTH0_M2M_CLIENT_ID,
      clientSecret: process.env.AUTH0_M2M_CLIENT_SECRET,
      scope: 'read:users'
    });

    const { data: user } = await managementClient.users.get({
      id: session.user.sub
    });

    const token = await authenticate(
      'urn:ietf:params:oauth:grant-type:jwt-bearer',
      {
        clientId: process.env.CL_SALES_CHANNEL_CLIENT_ID,
        clientSecret: process.env.CL_SALES_CHANNEL_SECRET,
        scope: process.env.NEXT_PUBLIC_CL_MARKET,
        assertion: await createAssertion({
          payload: {
            'https://commercelayer.io/claims': {
              owner: {
                type: 'Customer',
                id: user.user_metadata.customerId
              }
            }
          }
        }),
        headers: {
          'x-backend-auth': process.env.CL_BACKEND_AUTH_KEY,
          'x-true-client-ip': req.socket?.remoteAddress
        }
      }
    );

    res.status(200).json({
      accessToken: token.accessToken,
      expires: token.expires
    });
  } catch (error) {
    res.status(error.status || 500).json({ error: error.message });
  }
});

At this point, you should use the useCommerceLayerAuth hook. It can provide a token based on whether the user is logged in or not. With this token, if the user is logged in, you can interact with Commerce Layer APIs to retrieve the customer's order history.

Build the order history page

To proceed, create a new page at /pages/orders.jsx. This page will only be visible to logged-in users.

import React from 'react';
import { withPageAuthRequired } from '@auth0/nextjs-auth0/client';
import {
  CommerceLayer,
  CustomerContainer,
  OrderListEmpty,
  OrderList,
  OrderListRow
} from '@commercelayer/react-components';
import Loading from '../components/Loading';
import ErrorMessage from '../components/ErrorMessage';
import { useCommerceLayerAuth } from '../providers/CommerceLayerAuth';

const columns = [
  {
    header: 'Order',
    accessorKey: 'number'
  },
  {
    header: 'Status',
    accessorKey: 'status'
  },
  {
    header: 'Date',
    accessorKey: 'updated_at'
  },
  {
    header: 'Amount',
    accessorKey: 'formatted_total_amount_with_taxes'
  }
];

function Orders() {
  const { auth, isLoading } = useCommerceLayerAuth();

  return (
    <>
      {!isLoading && (
        <div className="mb-5">
          <h1>Orders</h1>
          <div>
            <CommerceLayer
              accessToken={auth.accessToken}
              endpoint={`https://${process.env.NEXT_PUBLIC_CL_ENDPOINT}.commercelayer.io`}
            >
              <CustomerContainer>
                <OrderList className="table" columns={columns}>
                  <OrderListEmpty />
                  <OrderListRow field="number">
                    {({ cell, order, ...p }) => {
                      return (
                        <>
                          {cell?.map((cell) => {
                            return <p>Order # {cell.renderValue()}</p>;
                          })}
                        </>
                      );
                    }}
                  </OrderListRow>
                  <OrderListRow field="status" />
                  <OrderListRow field="updated_at" />
                  <OrderListRow field="formatted_total_amount_with_taxes" />
                </OrderList>
              </CustomerContainer>
            </CommerceLayer>
          </div>
        </div>
      )}
    </>
  );
}

export default withPageAuthRequired(Orders, {
  onRedirecting: () => <Loading />,
  onError: (error) => <ErrorMessage>{error.message}</ErrorMessage>
});

You can now change the link to the external page on the /components/NavBar.jsx component to the orders page from this:

<NavItem>
  <PageLink href="/external" className="nav-link" testId="navbar-external">
    External API
	</PageLink>
</NavItem>

To this:

<NavItem>
  <PageLink href="/orders" className="nav-link">
    Orders
  </PageLink>
</NavItem>

After logging in with your account, you should be able to access the orders page and view your orders.

Place an order and see it on your history page

If you don't have any orders for your new customer, you can add the following components to /components/Content.jsx. This will allow you to add a product to the cart, proceed to checkout, and see the newly placed order at http://localhost:3000/orders while still logged in.

import React from 'react';
import { Row, Col } from 'reactstrap';
import {
  OrderContainer,
  OrderStorage,
  CommerceLayer,
  PricesContainer,
  Price,
  SkusContainer,
  SkuField,
  Skus,
  AddToCartButton
} from '@commercelayer/react-components';

import { useCommerceLayerAuth } from '../providers/CommerceLayerAuth';

const skuCode = 'BABYONBU000000E63E7412MX';
const websiteUrl = 'http://localhost:3000';

const Content = () => {
  const { auth } = useCommerceLayerAuth();

  return (
    <CommerceLayer
      accessToken={auth?.accessToken}
      endpoint={`https://${process.env.NEXT_PUBLIC_CL_ENDPOINT}.commercelayer.io`}>
      <div className="next-steps my-5">
        <Row className="d-flex justify-content-between">
          <Col key={0} md={5} className="mb-4">
            <OrderStorage persistKey="my-order">
              <OrderContainer attributes={{ return_url: websiteUrl, cart_url: websiteUrl }}>
                <SkusContainer skus={[skuCode]}>
                  <div className="card" style={{ width: '18rem' }}>
                    <Skus>
                      <SkuField attribute="image_url" className="card-img-top" tagElement="img" />
                      <div className="card-body">
                        <SkuField attribute="name" className="card-title" tagElement="h5" />
                        <SkuField attribute="description" className="card-text" tagElement="p" />
                        <PricesContainer>
                          <Price skuCode={skuCode}>
                            {({ prices }) => {
                              if (prices.length > 0) {
                                const { formatted_amount, formatted_compare_at_amount } = prices[0];
                                return (
                                  <p>
                                    <span>{formatted_amount}</span>{' '}
                                    <span>
                                      <del>{formatted_compare_at_amount}</del>
                                    </span>
                                  </p>
                                );
                              }
                            }}
                          </Price>
                        </PricesContainer>
                        <AddToCartButton className="btn btn-primary" buyNowMode skuCode={skuCode} />
                      </div>
                    </Skus>
                  </div>
                </SkusContainer>
              </OrderContainer>
            </OrderStorage>
          </Col>
        </Row>
      </div>
    </CommerceLayer>
  );
};

export default Content;

Please modify the file /pages/index.jsx as follows:

import React from 'react';

import Content from '../components/Content';

export default function Index() {
  return <Content />;
}

On the homepage, you should see the product with the code TSHIRTMS000000FFFFFFLXXX (If you have not used our seeder to feed data on your organization, the SKU code may differ and I suggest you to use one of your SKUs). Add it to your cart, check out, and view the order in your user area.

The only missing piece is the application ID, which must be added to the .env.local file under the name CL_INTEGRATION_ID.

This approach, that can be adopted also for the sales channel token, eliminates the need to call Commerce Layer, which can speed up the callback endpoint. In addition, if your ecommerce experiences multiple concurrent signups/logins (which we hope it does!), this approach can help prevent your application from being rate-limited. For more information on rate limits, please refer to Commerce Layer's documentation here).

Conclusion

We hope you have learned more about ways to use our SSO feature, which is available to enterprise customers. You may also want to consider other identity provider platforms, such as Okta or Ory, that offer similar features and a seamless integration with Commerce Layer.

Stay tuned for more articles in this series!

You can explore this GitHub repository , which contains all the code used in this tutorial, for further learning and code customization. Thank you for reading this far. Cheers!

Want to learn more about Commerce Layer?