Building a Shopify POS UI Extension is a great way to add custom features to your Shopify POS. You use the Shopify CLI, generate your extension, customize the React component, and everything seems to work fine... until you need to make that one fetch call to your backend.
At this point, the following questions might come up:
- How can my backend know this request is legitimate?
- How does it know which shop this call is coming from without exposing that data in the request?
- How can we prevent someone malicious from pretending to be our extension and sending fake data?
If you've ever faced this, you know the answer is JWT authentication. Shopify solves this elegantly using Session Tokens.
The official documentation shows the way, but it can be a bit abstract.
In this article, I'll show you the practical, straightforward guide on how we implemented this end-to-end authentication: from the fetch in the POS Extension to the verification middleware in our Node.js backend, using the official library.
Let's dive in!
The General Flow
Before diving into the code, here’s a summary of the authentication flow in 3 steps:
- Frontend (POS Extension): Our extension uses the Session API (
useApi) to request a unique JWT from Shopify at the time of the request. - The Request: The frontend sends this JWT in the
Authorization: Bearer <token>header to our backend. - Backend (Your Server): Our middleware intercepts the call, uses the
@shopify/shopify-apilibrary to verify if the token is valid, and if so, identifies the shop and allows the request to continue.
Frontend - Getting the Token
Important: The token retrieval method may vary depending on the Shopify API version used. In this article, we are using version 2025-07.
Shopify makes this step very simple. Inside your extension (which likely uses React), you can use the useApi hook from the @shopify/ui-extensions-react/point-of-sale package.
// extensions/pos-ui-your-extension/src/YourComponent.jsx
import React, {useState, useEffect} from 'react';
import {
reactExtension,
useApi,
Screen,
Text,
} from '@shopify/ui-extensions-react/point-of-sale';
const SmartGridModal = () => {
const {currentSession, getSessionToken} =
useApi<'pos.home.modal.render'>().session;
const {shopId, userId, locationId, staffMemberId} = currentSession;
const [sessionToken, setSessionToken] = useState<string>();
useEffect(() => {
getSessionToken().then((newToken) => {
setSessionToken(newToken);
});
}, [])
return (
<Screen name="ScreenOne" title="Screen One Title">
<Text>
shopId: {shopId}, userId: {userId}, locationId: {locationId}, staffId:
{staffMemberId}
</Text>
<Text>sessionToken: {sessionToken}</Text>
</Screen>
);
};
export default reactExtension('pos.home.modal.render', () => (
<SmartGridModal />
));
Note: In cases where the call is triggered by an action (e.g., clicking a "Submit" button triggers a fetch to the backend with a body to be processed), I personally prefer to trigger
getSessionTokeninside the handler that will make the call. This ensures that the sessionToken is valid at the time of sending.const onClickHandle = () => { getSessionToken().then((newToken) => { const headers = { Authorization: `Bearer ${newToken}` }; fetch("[https://myapi.com](https://myapi.com)", { headers }).then((res) => console.log(res)); }); };
Backend - Verifying the Token
This is the crucial part. The backend will receive the token from the POS Extension and needs to do two things:
- Authenticate: Is it a valid token and was it really issued by Shopify?
- Authorize: Does this shop (which the token claims to represent) exist in our database?
We'll use the official Shopify library to do the heavy lifting.
API Configuration (Your shopifyApi.js)
First, we need to ensure our backend knows our API keys. You probably have a configuration file similar to this one (let's call it config/shopifyApi.js) that initializes the Shopify library:
import "@shopify/shopify-api/adapters/node";
import { shopifyApi, ApiVersion } from "@shopify/shopify-api";
import * as dotenv from "dotenv";
dotenv.config();
// We initialize the 'shopify' object, which will be used as the main Shopify instance throughout the API
const shopify = shopifyApi({
apiKey: process.env.SHOPIFY_API_KEY,
apiSecretKey: process.env.SHOPIFY_API_SECRET,
hostName: process.env.HOST,
scopes: process.env.SCOPES,
apiVersion: "2025-07", // Use your version
});
If you're not familiar with @shopify/shopify-api, access the complete documentation here.
Creating the Middleware
Now we will finally create the authentication and authorization middleware for the routes that will be used by the POS Extension. It will run before the protected routes.
// src/middlewares/auth.js
import { StoreRepository } from "../repositories/StoreRepository.js";
import { shopify } from "../config/shopifyApi.js"; // The config from Step 2a
export default async (req, res, next) => {
try {
// 1. Get the token from the request header
const token = req.headers["authorization"].split(" ")[1];
if (!token) {
throw new Error("Unauthorized");
}
// 2. THE MAGIC: Decode and Verify the Token
// The `decodeSessionToken` does everything:
// - Fetches Shopify's keys
// - Verifies the token's signature
// - Checks if it has expired
// - If everything is OK, returns the payload (the data)
const payload = await shopify.session.decodeSessionToken(token);
// 3. Get the shop's URL (e.g., "[https://my-store.myshopify.com](https://my-store.myshopify.com)")
const shopId = payload.dest;
// 4. Authorization: Check if this shop exists in OUR database
const store = await Store.findOne({ shopId });
if (!store) {
// The token is valid, but the shop is not installed in our app
throw new Error("Store not found");
}
// 5. Done! We attach the store to the request
// Now, all subsequent routes will have access to `req.store`
req.store = store;
return next(); // Continues the flow to the route
} catch (error) {
// If `decodeSessionToken` fails or the store is not found,
// we return 401 Unauthorized.
return res.status(401).json({ message: "Unauthorized" });
}
};
To summarize what happens in the code above:
-
shopify.session.decodeSessionToken(token)performs authentication (proves the user is who they claim to be, i.e., Shopify). -
Store.findOne()performs authorization (proves the user has permission to access the resource, in the example's case, checks if the shop is registered in the system).
Applying the Middleware to Routes
With the middleware ready, just add it to the routes that need protection.
// src/routes.js
import shopifyAuthMiddleware from "./middleware/shopifyAuth.js";
// ... other application routes
// This route is public, no token needed
routes.get("/api/public/health-check", (req, res) => {
res.json({ status: "ok" });
});
// This route is PROTECTED by our middleware
routes.get(
"/api/private/my-data",
shopifyAuthMiddleware, // <-- The middleware goes here!
async (req, res) => {
// If the code reached here, the token is valid and req.store exists!
const store = req.store;
res.json({
message: `Hello, shop ${store.name}!`,
});
}
);
Conclusion
It's done! With this architecture, we have closed the end-to-end authentication loop.
Let's recap what we did:
On the Frontend (POS Extension): We used the
getSessionTokenhook to get a valid session token (JWT) for each API call.On the Backend (Middleware): We used the
shopify.session.decodeSessionTokenmethod to validate the token's signature against Shopify's public keys.In the Route: We checked if the token's shop exists in our database (
Store.findOne) before allowing access.
Now, your backend has a robust and secure way to ensure that every request coming from your POS Extension is legitimate and authenticated. No more "invalid token" or mysterious calls!
So, how do you do it?
This was the way we found to implement authentication following Shopify's best practices.
How does your team handle extension authentication? Do you use a different approach? Did you encounter any problems along the way?
Leave your comment below! Let's share experiences.

Top comments (0)