How can I fix the failure in validating authenticated session in Shopify CLI?

Topic summary

A developer encountered authentication failures when calling Shopify GraphQL APIs from a React UI built with Shopify CLI boilerplate. The issue centered on shopify.validateAuthenticatedSession() failing despite Redis session storage being properly configured.

Root Cause:
The @shopify/shopify-app-express v2.0.0 package lacks a built-in verifyToken() method to validate JWT tokens sent from the client.

Solution Approach:

  • Create custom middleware to decode and verify JWT tokens from the Authorization header
  • Extract the session ID (sid) from the decoded JWT payload
  • Use the session ID to retrieve the access token from Redis storage via shopify.config.sessionStorage.loadSession()
  • Store the access token in res.locals.shopify.session.accessToken for API calls

Key Implementation Details:

  • For MongoDB implementations, the session ID format is offline_ + shop domain (extracted from the dest field in JWT)
  • The JWT contains fields like iss, dest, aud, sub, sid but not dest_access_token
  • Redis stores session data as serialized arrays containing id, shop, state, isOnline, accessToken, and scope

Outstanding Question:
How to handle authentication for new shop installations when no session exists initially.

Summarized with AI on October 29. AI used: claude-sonnet-4-5-20250929.

Using shopify cli I have created boilerplate code to build app using React.

on React UI we are calling API to get data -

const {
data,
refetch: refetchProductCount,
isLoading: isLoadingCount,
isRefetching: isRefetchingCount,
} = useAppQuery({
url: “/api/products/count”,
reactQueryOptions: {
onSuccess: () => {
console.log(“success…”);
},
},
});

but on server side it is failing in shopify.validateAuthenticatedSession().
I have already done setup for redis session storage and it is working,
want to know what is missing here and how I can get data from graphql query inside React UI?

How did you set up your server-side code, especially the part where you’re using shopify.validateAuthenticatedSession()?

import { join } from “path”;
import { readFileSync } from “fs”;
import express from “express”;
import serveStatic from “serve-static”

import shopify from “./shopify.js”;
import GDPRWebhookHandlers from “./gdpr.js”;

const PORT = parseInt(process.env.BACKEND_PORT || process.env.PORT, 10);

const STATIC_PATH =
process.env.NODE_ENV === “production”
? ${process.cwd()}/frontend/dist
: ${process.cwd()}/frontend/;

const app = express();

app.get(shopify.config.auth.path, shopify.auth.begin());
app.get(
shopify.config.auth.callbackPath,
shopify.auth.callback(),
shopify.redirectToShopifyOrAppRoot()
);
app.post(
shopify.config.webhooks.path,
shopify.processWebhooks({ webhookHandlers: GDPRWebhookHandlers })
);
app.use(“/api/*”, shopify.validateAuthenticatedSession());

app.use(express.json());

app.get(“/api/products/count”, async (req, res) => {
console.log(“inside /api/products/count”); // 1. trying to reach here to fetch data from graphql and pass it to UI.

// const countData = await shopify.api.rest.Product.count({
// session: res.locals.shopify.session, // 2. also not sure what this is
// });
res.status(200).send(10);
});

app.use(shopify.cspHeaders());
app.use(serveStatic(STATIC_PATH, { index: false }));

app.use(“/*”, shopify.ensureInstalledOnShop(), async (_req, res, _next) => {
return res
.status(200)
.set(“Content-Type”, “text/html”)
.send(readFileSync(join(STATIC_PATH, “index.html”)));
});

console.log("App Running on ", PORT);
app.listen(PORT);

I have created above code using shopify cli.
I have setup redis storage, I can see key’s inside redis so access token is getting stored.
Also from UI side wherever I call /api/products/count API, it does have valid JWT token in Authorization key of header.

First, make sure you’ve got the right environment variables set up for redis session storage. Your .env file should have these variables.

SHOPIFY_APP_SECRET=

Your API request from the UI side already has a valid JWT token in the `Authorization` header, so let's update the server-side code to use this token instead of session storage.

replace this

```markup
app.use("/api/*", shopify.validateAuthenticatedSession());

with this

app.use("/api/*", shopify.verifyToken());

lastly update your /api/products/count endpoint to use the GraphQL API for fetching the product count. Replace the commented-out code with

const { data } = await shopify.api.graphql.query({
     session: res.locals.shopify.session,
     query: `{
       productsCount: products(first: 0) {
         count
       }
     }`,
   });

   res.status(200).send(data.productsCount.count);

After you’ve made these changes, your app should be able to grab the data from the GraphQL API just fine and display it on your React UI.

Thank you for your replay with explanation.
I tried and realise that I don’t have shopify.verifyToken() method available.

@shopify/shopify-app-express”: “^2.0.0”,

And I am using shopifyApp method from that package like this -

const shopify = shopifyApp({

api: {

apiVersion: LATEST_API_VERSION,

restResources,

billing: undefined, // or replace with billingConfig above to enable example billing

},

auth: {

path: “/api/auth”,

callbackPath: “/api/auth/callback”,

},

webhooks: {

path: “/api/webhooks”,

},

// This should be replaced with your preferred storage strategy

sessionStorage: new RedisSessionStorage(

redis://${Config.REDIS_HOST_IP}:${Config.REDIS_HOST_PORT}

),

});

export default shopify;

You can create a utility function to decode and verify the JWT token.

// utils/decodeJWT.js
import jwt from "jsonwebtoken";

const decodeJWT = (token) => {
  try {
    const decoded = jwt.verify(token, process.env.SHOPIFY_APP_API_SECRET_KEY);
    return decoded;
  } catch (error) {
    return null;
  }
};

export default decodeJWT;

and then create a custom middleware to verify the JWT token

// middleware/verifyToken.js
import decodeJWT from "../utils/decodeJWT.js";

const verifyToken = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader) {
    return res.status(401).json({ message: "Unauthorized" });
  }

  const token = authHeader.split(" ")[1];
  const decoded = decodeJWT(token);

  if (!decoded) {
    return res.status(401).json({ message: "Unauthorized" });
  }

  res.locals.shopify = { session: { accessToken: decoded.dest_access_token } };
  next();
};

export default verifyToken;

import the verifyToken middleware in your server-side code

import verifyToken from "./middleware/verifyToken.js";

Replace this line

app.use("/api/*", shopify.validateAuthenticatedSession());

With this line:

app.use("/api/*", verifyToken);

This middleware should now verify the JWT token sent from the client-side and save the access token in res.locals.shopify.session.accessToken.

Make sure to restart your server after making these changes.

from above example, what is this part -

decoded.dest_access_token

I am able to decode the token and get data, but I don’t have any dest_access_token key in decoded part, I have checked with jwt.io as well, all I have is this -

{
"iss": "xxxx",
"dest": "xxxx",
"aud": "xxxxx",
"sub": "xxxx",
"exp": xxxx,
"nbf": xxxx,
"iat": xxxx,
"jti": "xxxx",
"sid": "xxxx"
}

The dest_access_token should be replaced with the appropriate field from the decoded JWT token.

In your decoded JWT token, you have a sid field, which might be the session ID. You can use this session ID to retrieve the access token from your Redis storage.

update the verifyToken middleware to store the session ID-

// middleware/verifyToken.js
import decodeJWT from "../utils/decodeJWT.js";

const verifyToken = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader) {
    return res.status(401).json({ message: "Unauthorized" });
  }

  const token = authHeader.split(" ")[1];
  const decoded = decodeJWT(token);

  if (!decoded) {
    return res.status(401).json({ message: "Unauthorized" });
  }

  res.locals.shopify = { session: { sessionId: decoded.sid } };
  next();
};

export default verifyToken;

And then, you need to create a middleware to retrieve the access token from Redis storage using the session ID. You can use the shopify.sessionStorage instance you created in your shopifyApp configuration.

// middleware/retrieveAccessToken.js
const retrieveAccessToken = async (req, res, next) => {
  const sessionId = res.locals.shopify.session.sessionId;
  const session = await shopify.sessionStorage.loadSession(sessionId);

  if (!session) {
    return res.status(401).json({ message: "Unauthorized" });
  }

  res.locals.shopify.session.accessToken = session.accessToken;
  next();
};

export default retrieveAccessToken;

Lastly update your server-side code to use both middlewares-

import verifyToken from "./middleware/verifyToken.js";
import retrieveAccessToken from "./middleware/retrieveAccessToken.js";

// ...

app.use("/api/*", verifyToken, retrieveAccessToken);

The access token should be stored in res.locals.shopify.session.accessToken after going through both middlewares.

I am using
@shopify/shopify-app-express”: “^2.0.0”,

so sessionStorage does not exists -
shopify.sessionStorage.loadSession(sessionId)

We can fetch data from Redis directly but sessionId does not match with anything. in Redis I can see below data -

"[[\"id\",\"xxxx\"],[\"shop\",\"xxxx\"],[\"state\",\"xxxx\"],[\"isOnline\",false],[\"accessToken\",\"xxxx\"],[\"scope\",\"xxxxx"]]"

I started getting “session token is not valid” after I deleted my app then installed it again. Tried a whole host of changes and after hours didn’t understand what the issue was. I implemented this and it’s fixed it. Still don’t understand what the cause was but thank you for this.

I am using shopify-app-express with mongodb. For me id is offline_shopDomain.

import decodeJWT from "../utils/decodeJWT.js";

const verifyToken = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader) {
    return res.status(401).json({ message: "Unauthorized" });
  }
  const token = authHeader.split(" ")[1];
  const decoded = decodeJWT(token);
  if (!decoded) {
    return res.status(401).json({ message: "Unauthorized" });
  }
  const shop = decoded.dest.replace("https://", "");
  res.locals.shopify = {
    session: {
      sessionId: "offline_" + shop,
      shop,
    },
  };
  next();
};

export default verifyToken;
import shopify from "../shopify.js";

const retrieveAccessToken = async (req, res, next) => {
  const sessionId = res.locals.shopify.session.sessionId;
  const session = await shopify.config.sessionStorage.loadSession(sessionId);
  if (!session) {
    return res.status(401).json({ message: "Unauthorized" });
  }
  res.locals.shopify.session.accessToken = session.accessToken;
  next();
};

export default retrieveAccessToken;

This worked for me.
Though I am still confused about new shop auth cause there will be no session to begin with. (will update)