Shopify data security
It is important to take the necessary steps to ensure that your Shopify app is secure and that sensitive data is protected. This guide will provide you with the necessary information to help you secure your Shopify app.
Built-in security measures
All Gadget apps come with built-in security measures to help protect your application data, including a role-based authorization system. This system allows you to limit access to your app's data based on the role of the user making the request.
Shopify apps built with Gadget will have two default roles:
shopify-app-users
: The role assigned to merchants interacting with your app in a store admin.unauthenticated
: All other users, including storefront shoppers.
It is important to make sure that each role has the appropriate and minimal permissions to access the data they need.
Gadget also has support for building multi-tenant, public Shopify apps, which ensures that merchants can only access data that belongs to their shop. By default, all Shopify models in Gadget are multi-tenant so that data is not inadvertently exposed to other merchants.
Storefront API access
Shoppers in Shopify storefronts are unauthenticated
users. The Storefront API provides unauthenticated access to Shopify data, with Shopify managing the authentication. This streamlined approach simplifies the process of making API requests with Shopify's Storefront API.
However, this presents a risk for data exposure if security practices and measures aren't taken into account when calling your Gadget API from the storefront.
Several measures have been built into Gadget to help enforce data security:
Browser-based API client creation restriction
The system prevents the creation of API clients using API keys within web browsers. This precautionary step ensures that API keys are not inadvertently exposed. Consequently, any in-browser API clients will automatically receive the unauthenticated
access role in Gadget.
Limited access for unauthenticated role
The unauthenticated
access role is intentionally restricted from accessing most Shopify models. This is a vital safeguard against inadvertent exposure of personally identifying information (PII) and other sensitive data.
Protected access tokens
Access tokens stored on the shopifyShop
model are intentionally omitted from the response when accessed through your Gadget app's public API. This measure prioritizes security and privacy. Nonetheless, these tokens can still be accessed via the internal API, should the need arise for server-side operations.
Securing your data: best practices
Gadget's built-in security practices provide a sensible default that secures your application, but it's still your responsibility to ensure the security of each new API endpoint you add.
Audit API access
The data in your Gadget database is typically sensitive data -- including Shopify merchants' data and especially customer personal information. Generally speaking, you should avoid granting read access to merchant data where possible, and if required for powering your user interface, you should only grant access to authenticated roles you know have permission to read the data.
Giving end users of your app access to your API should always be done consciously and with lots of scrutiny. Avoid unauthenticated
access to sensitive model actions.
Changing access control permissions without considering the shape of returned data can give malicious users access to sensitive data. This is why we recommend that developers take the time to think through which model APIs that roles are given access to.
Use global actions for controlled data access
When constructing global actions in Gadget, exercise caution to avoid disclosing sensitive data to unauthenticated users and retrieve only the requisite data needed. If you are building a multi-tenant Shopify app, ensure that the data being accessed belongs to the shop that is making the request. Read more about enforcing multi-tenancy in global actions.
Utilize the integrated roles and permissions system for authorization, ensuring controlled data access and enhancing overall data security within your application.
Safeguard HTTP Routes
Authenticating access to HTTP routes must be done in your own route code -- Gadget doesn't protect HTTP routes by default. If you must serve sensitive data through routes, you can implement route protection using Gadget's authentication plugin.
Because HTTP routes are unprotected by default, Gadget recommends using global actions when possible, instead of low-level HTTP routes.
Appropriately grant access to model read actions with non-sensitive data
If a data model contains non-sensitive information, it's acceptable to grant unauthenticated users access to that model's API. Ensure that minimum permissions are granted, preventing shoppers from creating or updating records while allowing them to read data.
Use the Storefront API to fetch Shopify data
Where possible, use the Shopify Storefront API for retrieving Shopify data. Shopify's Storefront API is performant and has out-of-the-box security settings that work well. Making requests to the Shopify Storefront API is done outside Gadget within a theme or theme extension, so Gadget's Shopify connection does not directly interface with it. This approach is suitable if you exclusively require data accessible through the Storefront API.
You can also use metafields and metaobjects to expose data from your backend to a storefront securely.
Adding multi-tenancy to custom models
Shopify models managed by Gadget have multi-tenancy support built in. This means that merchants will only be able to read and write data that belongs to their shop and helps to enforce row-level security (RLS) for your Shopify app. This is a crucial security measure that ensures that data is not inadvertently exposed to other merchants.
When creating custom models for public Shopify apps, it is important to ensure that they are also multi-tenant.
Follow these 3 steps to add multi-tenancy to your custom models:
- Add a belongs to relationship to the
shopifyShop
model from your custom model. The inverse relationship should be a has many relationship from theshopifyShop
model to your custom model.
For example, if you have a quiz
model and want to relate it to the shopifyShop
model, the relationship should be shopifyShop
has many quiz
.
- Import the
preventCrossShopDataAccess()
function fromgadget-server/shopify
into all your model actions and call it in therun
function before saving the record. This function ensures that the data being accessed belongs to the shop that is making the request.
preventCrossShopDataAccess
must be called before the save
function in your model actions!
For example, adding multi-tenancy to a quiz
model action:
1import { applyParams, save, ActionOptions } from "gadget-server";2import { preventCrossShopDataAccess } from "gadget-server/shopify";34export const run: ActionRun = async ({ params, record }) => {5 applyParams(params, record);6 // Prevent merchants from modifying each others data7 // must be called before save()8 await preventCrossShopDataAccess(params, record);9 await save(record);10};1112// ... rest of action file
1import { applyParams, save, ActionOptions } from "gadget-server";2import { preventCrossShopDataAccess } from "gadget-server/shopify";34export const run: ActionRun = async ({ params, record }) => {5 applyParams(params, record);6 // Prevent merchants from modifying each others data7 // must be called before save()8 await preventCrossShopDataAccess(params, record);9 await save(record);10};1112// ... rest of action file
Each time preventCrossShopDataAccess
runs, it will make sure the given record has the correct shopId
for the shop processing this action. For new records, that means it will assign the shopId
, and for existing records, it will verify that it matches.
- Add a tenancy filter to your model in
accessControl/permissions
. Tenancy filters in Gadget are written in Gelly, Gadget's data access language.
Most tenancy filters will check to make sure the shopId
of the current session
is equal to the shopId
stored on the current model, and will look like this:
A sample filter on a quiz modelgellyfilter ($session: Session) on Quiz [where shopId == $session.shopId]
The shopId
field is generated when you add the belongs to relationship to the shopifyShop
model. The name of this
field is the name of the relationship followed by Id
.
For example, if the relationship is named shop
, the field will be named shopId
.
If it is named cowboy
, the field will be named cowboyId
.
We don't advise naming your relationships to the shopifyShop
model cowboy
.
After following these steps, your models are set up for multi-tenancy.
Refer to use global actions for controlled data access for more information on how to control data access in HTTP routes and global actions.
Multi-tenancy in global actions
When building multi-tenant applications such as Public Shopify apps, ensure that the data being accessed belongs to the shop that is making the request. This typically means that you should be filtering data based on the shopId
of the shop making the request.
For example:
1export const run: ActionRun = async ({2 params,3 logger,4 api,5 scope,6 connections,7}) => {8 // get the current shop id9 const shopId = connections.shopify.currentShopId;10 // get the data for the current shop11 const settings = await api.customSettings.findMany({12 filter: {13 shopId: {14 equals: shopId,15 },16 },17 });1819 // do additional processing, if required2021 return settings;22};
1export const run: ActionRun = async ({2 params,3 logger,4 api,5 scope,6 connections,7}) => {8 // get the current shop id9 const shopId = connections.shopify.currentShopId;10 // get the data for the current shop11 const settings = await api.customSettings.findMany({12 filter: {13 shopId: {14 equals: shopId,15 },16 },17 });1819 // do additional processing, if required2021 return settings;22};
It is important to test your multi-tenancy implementation thoroughly to ensure that data is not inadvertently exposed to other merchants.