Authentication in Shopify Online Stores with JSON Web Tokens

Written by
Written by

Shopify is a popular and useful platform for creating online stores. It’s quite extensible, but it has its own limitations. Despite those limitations, it is possible to use Shopify apps and themes to provide deep customization for Shopify users even with the more basic Shopify plans, with little more than an API server hosted on the platform of your choice.

We want to be able to offer customized experiences to customers through an external app, using any logic we desire. To do so, we need to be able to have an external service that can tell who a customer is in order to specify what they should see. There are many uses for this. We could have an app that accepts reviews, but only if the customer has purchased the product being reviewed. We could handle business rules that, for example, check an external system before giving a customer free shipping. We could have a forum that is only accessible to customers who have purchased so many items. The possibilities are endless.

Throughout this, I’ll explain how to do this from a high level, with specific code examples for a Node.js application server, but the principle can be used with anything that you can use to host an HTTP API server.

The Shopify platform

Before we can get into how we integrate with Shopify, we have to start by discussing how Shopify works. Shopify is a service that lets merchants build stores, offering tools for managing sales online and on-site. In order to accommodate the business models of all sorts of stores, Shopify offers many options for extending the functionality on their platform.

The most basic one, which almost all merchants take advantage of, is the ability to theme your online store. Shopify themes consist of assets and templates. Assets are what one typically sees in websites: scripts, images, and stylesheets. The real power lies in their Liquid template markup language. Liquid is a template language created by Shopify, in the family of Jinja 2 and Twig, where template parameters can be inserted between pairs of curly braces, and variables can be piped through filters. For example, on a collection page, to output a bulleted list of products in the collection with formatted prices, you could use:

 <ul>
    {% for product in collection %}
        <li>{{ product.name | escape }}: {{ product.price | money }}</li>
    {% endfor %}
</ul>

Liquid is somewhat less rich than many other templating languages since it was made to run any merchant’s templates on Shopify’s servers. Many other templating languages can run arbitrary code in their host environment, but Liquid is unable to do so, allowing Shopify to offer templating without needing to trust that templates won’t try to break their security or overload their systems. Additionally, extending the Liquid templating language with additional filters and tags isn’t an option without a Shopify Plus.

Themes, like any website, can include JavaScript. To allow certain simple AJAX interactions, Shopify offers an “Ajax API” that can be used from within online stores. One of the uses of this API is for manipulating the customer’s cart, allowing, for example, a button that adds more of an item to a cart without needing to reload a page.

You can still get a lot done with Liquid. The Liquid template is supplied with parameters for the context of the store, allowing the template to access store data and information related to the current user and page.

Merchants can go even further by using Shopify apps. Shopify, like many platforms, has its own app store. For many common use cases, there’s already an app built for that. If there isn’t an app, then merchants can build their own personal apps. Shopify has three kinds of apps: private apps, are built into a store and have limited capabilities, custom apps, which can be installed in a single store at a time but have full capabilities; and public apps, which need to go through the Shopify approval process and can be installed from the Shopify app store. For this, we need to build a custom app, since private apps do not have the capabilities needed.

The only thing that private apps can do is access the Shopify admin API and receive webhooks. The admin API enables CRUD on most of the entities in a store: customers, products, orders, etc. The API can be used through either GraphQL or with REST. Some operations are faster with GraphQL, while others are faster with REST. The only one we’ll need for authentication is the customer API, but specific use cases will require more.

One of the lesser-known app interfaces is the app proxy. This is a necessary part of how we’re going to do authentication and why we need a custom app: private apps can’t use app proxies. With an app proxy, you can tell Shopify to proxy content from certain URLs on your app server into the same domain as the store. The important capability of this is that, before passing your app’s response to a customer who hits your app proxy, if you respond with a Liquid template, then Shopify will render that template with the template parameters available to any page in the theme.

The most expected use for an app in Shopify is the creation of an app interface that’s embedded into the admin UI for a store. We will need to implement a tiny portion of one to ensure that we can authenticate our app to Shopify, but most of our functionality will be outside of the admin UI.

JSON Web Tokens

Another important ingredient is JSON Web Tokens, or JWT’s. They are defined in RFC 7519 (https://datatracker.ietf.org/doc/html/rfc7519). They are commonly used for authentication, by creating an unforgeable token using cryptography in a standardized format. For our purposes, JWTs have an issuer, a subject, and an audience. The issuer is the application that creates the token. The subject is the user authenticated by the token. The audience is a set of applications that check that the token is valid.

To summarize, the issuer has a key. Using their key, the issuer creates a signature that cannot be reproduced by anyone else, which acts as proof that the data in the envelope were signed by the issuer. The issuer can also encrypt the data in the envelope to limit who can view the data inside. The data is a simple piece of JSON that identifies the subject. Then the audience members can use their key to validate that the data is signed by the issuer.

There are a few cryptographic algorithms that can be used to create the signature. The most common algorithm is HMAC, which is a symmetric algorithm. This means that if you have the key and the data, then you can create a signature. Additionally, the only way to validate the signature is to regenerate the signature from the data and the key. Since you need to keep the key secret, this is not well suited for if your signature needs to be validated by a large set of audience members. However, it works well if your issuer is the same as your audience.

There are also asymmetric signatures, using algorithms like RSA or ECDSA. These have keys that are split into public and private keys. The issuer publishes the public key but keeps the private key secret. Then the issuer creates the signature with the private key. The public key and the private key are mathematically related, allowing audience members to validate the signature using just the public key. This is useful if you want anyone to be able to prove the authenticity of the JWT’s signature.

For our case, our issuer and our audience will be the same application, so we can use HMAC.

Setting up your app with Shopify

Now that we have all that, we have the tools we need to solve our problem.

First, you need to set up a Shopify Partner account and create a custom app in order to create the credentials for the OAuth2 flow needed to run (https://shopify.dev/tutorials/make-your-first-shopify-api-request).

Once you have a custom app setup, you need to authenticate the app. Shopify has a tutorial on how to do so (https://shopify.dev/tutorials/authenticate-with-oauth). A quick way to do this is with Shopify’s Node.js library @shopify/koa-shopify-auth (https://github.com/Shopify/koa-shopify-auth). That library has enough documentation to get you up and running with a Node.js server based on Koa. As that tutorial notes, regardless of how your server works, your server will need the hostname that you set up for the app, the client ID, and the client secret. You will also need to find some means of hosting this server.

Depending on what your app needs to do, you will need to set Shopify’s OAuth2 scopes for their API (https://shopify.dev/docs/admin-api/access-scopes). At the minimum, you’re going to need read_customers in order to validate the existence of users. For the example of making sure that only customers who bought an item can submit reviews, you would also need read_orders and read_products.

You will also need to make sure that the access mode is set correctly. If you’re manually implementing the OAuth2 flow yourself, then you can just omit the grant_options[] option when calling the authorization endpoint. When using Shopify’s Koa library, you will need to set accessMode to offline.

When doing authentication, it’s recommended that your server has some kind of database or store to put its OAuth2 access token. It doesn’t need to be complex: a single file in the same directory as your server or a persistent key-value store will suffice. If you’re using Shopify’s Koa library, it has a good example using Redis. There’s a decent chance that your app will require some kind of data store. For example, for the product rating app, you need a place to store reviews.

That landing page will need to be an authenticated admin app page. Shopify relies on having a route that will redirect when authentication is needed. However, unlike in their Koa example, you’re also going to need several routes that aren’t going to be authenticated against Shopify, at least, not as Shopify expects you to do authentication.

The customer authorization API

We’re going to make our Shopify app use the app proxy functionality to perform authorization. Once your app is all set up, you need to use the Shopify Partner portal to set a path on your server as proxy routes. You can set up proxies with their directions (https://shopify.dev/tutorials/display-dynamic-store-data-with-app-proxies). You really should check the HMAC, per their instructions, to make sure that your requests are really going through the Shopify app proxy. In your server, you will need to have a prefix for all of your routes that are to be used in the app proxy, authenticating the Shopify HMAC on all of those routes.

We’re going to be handling the authorization with a JWT. Since JWTs require a key, that needs to be set in your server’s configuration. You can generate a sufficient JWT like so in Node.js, using the jsonwebtoken library (https://www.npmjs.com/package/jsonwebtoken):

const jwt = require('jsonwebtoken');

const renderTokenPageTemplate = (customerId) => {
  const token = jwt.sign({}, process.env.USER_VALIDATION_KEY, {
    expiresIn: 60,
    subject: customerId,
  });
  return `
    {%- layout none -%}
    {%- if customer.id == ${customerId} -%}
        {"token":"${token}"}
    {%- else -%}
        {"error":"Invalid request", "passed": "${customerId}", "expected": "{{ customer.id | replace '"', '\"' }}"}
    {%- endif -%}
  `.trim();
};

This is the core of your authorization process. We assume that somehow you’re getting a customer ID, either in a query parameter or as part of the route. That’s used to sign a JWT that uses the customer ID as the subject and expires in a minute. This short expiration period is used to ensure that it’s unlikely for the token to be leaked. You might want to use a longer expiration period if you need the customer to be authorized for a longer period of time.

Once you have the signed token containing the customer’s Shopify user ID, you need to use a Liquid template to authenticate that the customer making the request is the same as the customer you are authenticating. Your server has no access to the customer’s attributes from the Shopify store, so you need to rely on the app proxy template renderer to handle that. This is part of the reason you need to be sure that your requests come from Shopify: if anyone can get the raw template from your server, then they can create tokens for any customer ID they want.

If the template renderer renders the token, then you know that the customer making the request is exactly the customer whose ID was passed. Therefore, anyone who has a JWT created this way should be the subject described in the token.

You’re also going to need some routes that can authenticate with this token. To do so in Node.js, you can have a simple function:

const verifyCustomerToken = (customerId, token) => {
  try {
    jwt.verify(token, process.env.USER_VALIDATION_KEY, {
      subject: customerId,
    });
    return true;
  } catch (e) {
    return false;
  }
}

With these two building blocks, you can create an app that securely allows your customers to do all sorts of things.

Authentication in your theme

Now that you have an endpoint, you need to use it inside your theme. For example, if you’re trying to add ratings for your product pages, you can add this to your templates/product.liquid template in your theme with a little bit of HTML, CSS, and JavaScript to make a few buttons that run this JavaScript.

If we assume that you set up your app proxy at /apps/my-app, put the token endpoint in your app proxy path under /token, and used the query parameter customerId to give it the customer ID, then this JavaScript function can fetch the token:

const getToken = async (customerId) => {
  const tokenResponse = await fetch('/apps/my-app/token?customerId=' + encodeURIComponent(customerId));
  const tokenBody = await tokenResponse.text();
  const token = JSON.parse(tokenBody);
  return token.token;
};

Then, you can use JavaScript to feed that token as you would like to your API, being sure that the user with the token is the customer you authenticated.

For ratings, you could immediately call an endpoint with a rating and a product ID, check their order history in your server with the Shopify Admin API, and save the rating if they have an order with that product.

Conclusion

Even working within the limitations of Shopify, you can create customized user experiences with third-party services. We created an app, an app server, and a theme to let you create a customized experience for customers in your Shopify store. Shopify offers many different interfaces that you can develop software against. Armed with the knowledge of these and a little bit of engineering, you can distinguish yourself from your competitors and build a truly unique store.

Visit our careers page if you’re interested in becoming part of the FSL team, or contact us in case you need help with a new project, a current project, or staff augmentation.

Frequently Asked Questions