koa-shopify-auth
koa-shopify-auth copied to clipboard
v4: This app can’t load due to an issue with browser cookies
After upgrading to 4.0.2, my app fails to authenticate and load as an embedded app (using ngrok).
After a series of redirects it fails:

This was working normally under v3.2.0 so maybe you can point me in the right direction knowing what has been changed since then. Wondering if other users are facing the issue so we could update the docs if I'm missing something obvious.
My server.js
code:
const blitz = require("@blitzjs/server");
const Koa = require("koa");
const { default: shopifyAuth, verifyRequest } = require("@shopify/koa-shopify-auth");
const { default: Shopify, ApiVersion } = require("@shopify/shopify-api");
const session = require("koa-session");
const { PrismaClient } = require("@prisma/client");
const Queue = require("bull");
const dev = process.env.NODE_ENV !== "production";
const port = parseInt(process.env.PORT, 10) || 3000;
const { SHOPIFY_API_SECRET_KEY, SHOPIFY_API_KEY, HOST_NAME } = process.env;
const REDIS_URL = process.env.REDIS_URL || "redis://127.0.0.1:6379";
const app = blitz({ dev });
const prisma = new PrismaClient();
const handle = app.getRequestHandler();
const workQueue = new Queue("work", REDIS_URL);
// Initialize the library for Shopify API
Shopify.Context.initialize({
API_KEY: SHOPIFY_API_KEY,
API_SECRET_KEY: SHOPIFY_API_SECRET_KEY,
SCOPES: ["write_script_tags", "read_themes", "write_themes"],
HOST_NAME,
API_VERSION: ApiVersion.January21,
IS_EMBEDDED_APP: true,
SESSION_STORAGE: new Shopify.Session.MemorySessionStorage(),
});
app.prepare().then(() => {
const server = new Koa();
server.use(session({ sameSite: "none", secure: true }, server));
server.keys = [Shopify.Context.API_SECRET_KEY];
server.use(
shopifyAuth({
// Tried both online and offline
accessMode: "online",
async afterAuth(ctx) {
const { accessToken, shop: shopifyDomain } = ctx.state.shopify;
// Migrate to cookieless sessions
// ctx.cookies.set("shopOrigin", shopifyDomain, {
// httpOnly: false,
// secure: true,
// sameSite: "none",
// });
await workQueue.add();
await prisma.shop.upsert({
where: {
shopifyDomain,
},
update: {
shopifyToken: accessToken,
},
create: {
shopifyDomain,
shopifyToken: accessToken,
},
});
ctx.redirect(`/?shop=${shopifyDomain}`);
},
})
);
/**
* Everything after this point will require authentication.
*/
server.use(verifyRequest({ accessMode: "online" }));
server.use(async (ctx) => {
await handle(ctx.req, ctx.res);
ctx.respond = false;
ctx.res.statusCode = 200;
});
server.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`);
});
});
Hey @janklimo, the problem here is that you are calling verifyRequest
for the /
endpoint (and for static pages). With the new session token authentication, your app should always load a page skeleton without expecting a session, so it can build the App Bridge client and set up the JWT session.
It's important to note that you should not include any sensitive information in that initial request since it needs to be unauthenticated.
The React + Node tutorial has been updated to follow this new pattern (in particular, steps 5-7).
Let me know if that helps unblock you, and we can mention that in the migration guide as well! In any case, we should also remove koa-session
from the example app since it's no longer needed, and make sure that it follows the new pattern.
Hey @paulomarg, I am trying to implement this new pattern. I am getting inconsistent error messages when I redirect to /auth: "Cannot complete OAuth process. No session found for the specified shop url:". These errors are not triggered each time and I can not understand why.
The redirect is handled by a KOA middleware, I am using Mongodb Atlas to store the shop data:
const verifySession = async (ctx, next) => {
const shop = ctx.query.shop;
// DB is Mongodb Atlas
// Detect if shop is added to db, has passed trought OAuth
let result = await DB.find({
id: 'offline_' + shop
}).exec();
if (!result.length) {
// Go to OAuth
ctx.redirect(`/auth?shop=${shop}`);
return;
}
await next();
};
router.get('/', verifySession, handleRequest)
Regarding your comment, what does actually means: so it can build the App Bridge client and set up the JWT session. Thank you
Cannot complete OAuth process. No session found for the specified shop url:". These errors are not triggered each time and I can not understand why.
I am experiencing the same intermittent error message.
@mmccall10 @mirceapiturca can you guys share your full server.js
file? How's the session persisted? I'm not seeing those, what @paulomarg recommended worked for me.
accessToken
I'm getting back is a short one. I was expecting long bearer token which I could decode. Is it expected? I'm using offline
param that I pass same to verifyRequest
as mentioned here.
Session output:
Session {
id: 'offline_123.myshopify.com',
shop: '123.myshopify.com',
state: '5344458270303541',
isOnline: false,
accessToken: 'shpat_d8378c1996752bf8jdhc75b250926080',
scope: 'write_script_tags,write_themes'
}
I will share server.js
once I'm done with some cleanup. Currently its large.
@mirceapiturca it's hard to say why you're getting that intermittent error, but that message happens when your app tries to load a session that has not been created yet. Your code with MongoDB looks right to me, so it could be that your loadSession
callback for the Shopify.Context.SESSION_STORAGE
class isn't returning the session.
@pratiknikam the accessToken
you get back is used by your app to make requests to the Admin API (for example, using new Shopify.Clients.Rest(session.shop, session.accessToken)
).
Regarding your comment, what does actually means: so it can build the App Bridge client and set up the JWT session.
Embedded apps using session tokens need to use the App Bridge app
in their frontend. The flow is essentially this:
- App Bridge automatically creates the JWT session for you when you use the
app
in your frontend. - When you call
authenticatedFetch
it automatically sends the JWT Bearer token with your request. - Your server can then load the session from that JWT using e.g.
Shopify.Utils.loadCurrentSession(ctx.req, ctx.res)
- As I mentioned above, with that session you can make requests.
The tutorial page on Apollo requests shows an example of how to use App Bridge + JWT tokens to load data from Shopify. If you follow it along from the start, it goes over all of the steps needed to create an app that can fetch data using session tokens.
Hope this helps!
@janklimo @paulomarg I've actually managed to replicate this on a fresh install. The only change was adding accessMode: 'offline'
.
A bit context Using a fresh install MemorySessionStorage Access mode: offline Using Ngrock locally.
Installing the application works fine, no errors. Restarting the server, and accessing the app from the admin, redirects to auth but gives intermittent errors. Please see this screen recording: https://www.loom.com/share/0b06d220f46541b5b06b1d732e52bf8b
Here is the server:
import "@babel/polyfill";
import dotenv from "dotenv";
import "isomorphic-fetch";
import createShopifyAuth, { verifyRequest } from "@shopify/koa-shopify-auth";
import Shopify, { ApiVersion } from "@shopify/shopify-api";
import Koa from "koa";
import next from "next";
import Router from "koa-router";
dotenv.config();
const port = parseInt(process.env.PORT, 10) || 8081;
const dev = process.env.NODE_ENV !== "production";
const app = next({
dev,
});
const handle = app.getRequestHandler();
Shopify.Context.initialize({
API_KEY: process.env.SHOPIFY_API_KEY,
API_SECRET_KEY: process.env.SHOPIFY_API_SECRET,
SCOPES: process.env.SCOPES.split(","),
HOST_NAME: process.env.HOST.replace(/https:\/\//, ""),
API_VERSION: ApiVersion.October20,
IS_EMBEDDED_APP: true,
// This should be replaced with your preferred storage strategy
SESSION_STORAGE: new Shopify.Session.MemorySessionStorage(),
});
// Storing the currently active shops in memory will force them to re-login when your server restarts. You should
// persist this object in your app.
const ACTIVE_SHOPIFY_SHOPS = {};
app.prepare().then(async () => {
const server = new Koa();
const router = new Router();
server.keys = [Shopify.Context.API_SECRET_KEY];
server.use(
createShopifyAuth({
accessMode: 'offline',
async afterAuth(ctx) {
// Access token and shop available in ctx.state.shopify
const { shop, accessToken, scope } = ctx.state.shopify;
ACTIVE_SHOPIFY_SHOPS[shop] = scope;
const response = await Shopify.Webhooks.Registry.register({
shop,
accessToken,
path: "/webhooks",
topic: "APP_UNINSTALLED",
webhookHandler: async (topic, shop, body) =>
delete ACTIVE_SHOPIFY_SHOPS[shop],
});
if (!response.success) {
console.log(
`Failed to register APP_UNINSTALLED webhook: ${response.result}`
);
}
// Redirect to app with shop parameter upon auth
ctx.redirect(`/?shop=${shop}`);
},
})
);
const handleRequest = async (ctx) => {
await handle(ctx.req, ctx.res);
ctx.respond = false;
ctx.res.statusCode = 200;
};
router.get("/", async (ctx) => {
const shop = ctx.query.shop;
// This shop hasn't been seen yet, go through OAuth to create a session
if (ACTIVE_SHOPIFY_SHOPS[shop] === undefined) {
ctx.redirect(`/auth?shop=${shop}`);
} else {
await handleRequest(ctx);
}
});
router.post("/webhooks", async (ctx) => {
try {
await Shopify.Webhooks.Registry.process(ctx.req, ctx.res);
console.log(`Webhook processed, returned status code 200`);
} catch (error) {
console.log(`Failed to process webhook: ${error}`);
}
});
router.get("(/_next/static/.*)", handleRequest); // Static content is clear
router.get("/_next/webpack-hmr", handleRequest); // Webpack content is clear
router.get("(.*)", verifyRequest(), handleRequest); // Everything else must have sessions
server.use(router.allowedMethods());
server.use(router.routes());
server.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`);
});
});
Thank you
Hey @mirceapiturca, I think all you're lacking is to change the line
router.get("(.*)", verifyRequest(), handleRequest); // Everything else must have sessions
to
router.get("(.*)", verifyRequest({accessMode: 'offline'}), handleRequest); // Everything else must have sessions
Your app is trying to load an online session on requests, but it's performing OAuth for offline sessions.
@mirceapiturca said
Using Ngrock locally.
@paulomarg This might be a long shot but could this be in any way related to ngrok/tunneling? No idea how that would work but I saw the same thing happen in development but never in production (using custom SQL storage).
I am getting similar errors (not using ngrok, but live online app).
Nearly every time I reload the app it goes through Oauth, and sometimes I get this error: Cannot complete OAuth process. No session found for the specified shop url:
I understand this is probably because the session is not storing or loading properly. I've followed the Node tutorial here: https://shopify.dev/tutorials/build-a-shopify-app-with-node-and-react
I assumed the packages took care of the auth, but it doesn't work consistently even with SESSION_STORAGE: new Shopify.Session.MemorySessionStorage(),
The docs also say that is not a good method to use for production, and to use a custom method. But it does not say how to do that, other than a few incomplete lines of code without much explanation. I'm not sure why the tutorial would say "Here, do it like this for now. But change it to something else later, we just won't show you what."
Does anyone know how to implement "proper" session storage for a production app? For what it's worth, my app does not need Shopify to authenticate with the backend, since it has its own auth system. But it's still necessary to connect to Shopify.
Server.js file below:
require('isomorphic-fetch');
const dotenv = require('dotenv');
const Koa = require('koa');
const next = require('next');
const { default: createShopifyAuth } = require('@shopify/koa-shopify-auth');
const { verifyRequest } = require('@shopify/koa-shopify-auth');
const { default: Shopify, ApiVersion, SessionStorage } = require('@shopify/shopify-api');
const Router = require('koa-router');
const axios = require('axios').default;
dotenv.config();
Shopify.Context.initialize({
API_KEY: process.env.SHOPIFY_API_KEY,
API_SECRET_KEY: process.env.SHOPIFY_API_SECRET,
SCOPES: process.env.SHOPIFY_API_SCOPES.split(","),
HOST_NAME: process.env.SHOPIFY_APP_URL.replace(/https:\/\//, ""),
API_VERSION: '2021-01',
IS_EMBEDDED_APP: true,
SESSION_STORAGE: new Shopify.Session.MemorySessionStorage(),
});
const port = parseInt(process.env.PORT, 10) || 3000;
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
const ACTIVE_SHOPIFY_SHOPS = {};
app.prepare().then(() => {
const server = new Koa();
const router = new Router();
server.keys = [Shopify.Context.API_SECRET_KEY];
server.use(
createShopifyAuth({
accessMode: 'online',
async afterAuth(ctx) {
const { shop, accessToken, scope } = ctx.state.shopify;
ACTIVE_SHOPIFY_SHOPS[shop] = scope;
//Store accessToken in database
ctx.cookies.set('shopOrigin', shop, {
httpOnly: false,
secure: true,
sameSite: 'none'
});
ctx.redirect(`/?shop=${shop}`);
},
}),
);
router.post("/graphql", verifyRequest(), async (ctx, next) => {
await Shopify.Utils.graphqlProxy(ctx.req, ctx.res);
});
const handleRequest = async (ctx) => {
await handle(ctx.req, ctx.res);
ctx.respond = false;
ctx.res.statusCode = 200;
};
router.get("/", async (ctx) => {
const shop = ctx.query.shop;
if (ACTIVE_SHOPIFY_SHOPS[shop] === undefined) {
ctx.redirect(`/auth?shop=${shop}`);
} else {
await handleRequest(ctx);
}
});
router.get("(/_next/static/.*)", handleRequest);
router.get("/_next/webpack-hmr", handleRequest);
router.get("(.*)", verifyRequest(), handleRequest);
server.use(router.allowedMethods());
server.use(router.routes());
server.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`);
});
});
Hey @mirceapiturca, I think all you're lacking is to change the line
router.get("(.*)", verifyRequest(), handleRequest); // Everything else must have sessions
to
router.get("(.*)", verifyRequest({accessMode: 'offline'}), handleRequest); // Everything else must have sessions
Your app is trying to load an online session on requests, but it's performing OAuth for offline sessions.
Thank you for your support @paulomarg. Indeed I've missed adding the offline mode to verifyRequest
but I am still getting those errors. For some reason, that initial cookie that creates the session is not passed correctly on my setup.
Will update if I find a solution for this.
Thank you again
If it helps here is my setup:
Shopify APP CLI version: 1.6.0
Node version: v12.16.3
Storage: MemorySessionStorage
Localhost using Ngrock
Fresh install, only change made was adding accessMode: "offline"
in createShopifyAuth
and verifyRequest({accessMode: "offline"})
.
Thank you
Facing the same issue after upgrading to 4.0.2 as mentioned by @mirceapiturca . accessMode set as offline in verifyRequest.
In my case, the app never loads successfully. I consistently get this cookie error whenever I select this app from the available apps list.
During installation, I get the OAuth error - "Cannot complete OAuth process. No session found for the specified shop url: mystorename.myshopify.com"
@janklimo I was running the example app as offline. Sessions are being stored correctly and loaded correctly. I looked into Shopify node API and found this error is a bit misleading or at least there is overlap in the term "session". It is being thrown when a cookie is missing. I removed the calls to cookies and no longer experience the error.
https://github.com/Shopify/shopify-node-api/blob/fe5ed5475ef4cf23d7187c33c15eafeba26d9043/src/auth/oauth/oauth.ts#L111
That's a great find @mmccall10! I tested this myself ~15 times but unfortunately I couldn't reproduce the error. We'll improve that code to throw a different error on a missing cookie, though.
You did raise a great point about cookies - browsers have been adding restrictions on 3rd party cookies (cookies within iframes), so embedded apps should assume that cookies can't be used (except for OAuth which needs to happen at the top level, outside the iframe).
Note: As @janklimo pointed out when the issue was opened, the example app in https://github.com/Shopify/koa-shopify-auth wasn't working. We've since updated it to fit the new pattern. If anyone is using the previous iteration, you should check out the new one.
Unfortunately, at this point there is not much more we can do, since the package code seems to be working. If anyone finds problems in our code like the above or a specific set of actions that triggers the issue, we can investigate more.
@harishannam Hey, did You find the solution for this issue. Now I am facing the same. I am sending session token in headers from the client side, but it doesn't help
@vasiastep "The app could not be loaded" issue was solved by adding this - https://github.com/Shopify/shopify-app-node/blob/tutorial_fetch_data_with_apollo/server.js#L55
Still facing the OAuth error consistently. No idea how to get that fixed.
@paulomarg I have added accessMode: offline to the Fetch data with Apollo branch. Adding this accessMode makes the GraphQL calls to fail with an error Error: Cannot proxy query. No session found.
https://github.com/harishannam/shopify-app-node/commit/462c128a9e1de9fe309c1d66936c92cfd221266c
Same thing as soon as I get to verifyRequest
it throws to oauth
dance but I am missing store.myshopify.com
https://admin/oauth/authorize?client_id=..............
server.use(verifyRequest({ accessMode: 'offline' }));
// billing routes - deprecated - https://shopify.dev/concepts/about-apis/versioning/release-notes/2021-01
server.use(billingRouter.routes());
server.use(billingRouter.allowedMethods());
@paulomarg I am also experiencing an issue where my app does not load other pages. I was wondering, the line ACTIVE_SHOPIFY_SHOPS[shop] === undefined
does not verify any user session. So if some shop installs the app, then anyone can navigate to myapp.com?shop=<some shop>
. Is there something more to do there?
Other routes behind verifyRequest
aren't working for me, I define them like so, and I made sure that accessMode is offline everywhere:
router.get('anotherpage', verifyRequest({accessMode: 'offline'}), handleRequest)
Could being in a development store & development app have something to do with it? The redirect after installation doesn't put my app into an iframe, it just goes to the app's page.
Also, when I poke around at the values, I find that the following return null or undefined:
const session = await Shopify.Utils.loadCurrentSession(ctx.req, ctx.res, false)
ctx.request.headers.authorization
If my ctx.request.headers.authorization
is typically undefined, how do I set it? Even verifyRequest
eventually reaches:
https://github.com/Shopify/shopify-node-api/blob/617b7c55e7f3d6400931ee95de7a14435c870083/src/auth/oauth/oauth.ts#L240
I have the same issue , online mode works great but not in offline mode :/
Error: Cannot proxy query. No session found.
other people seems to have the same issue : https://community.shopify.com/c/Shopify-APIs-SDKs/Offline-Access-Token-will-cause-session-not-found/m-p/1106973#M64329
Hey everyone, let me see if I can answer some questions:
The GraphQL proxy is actually not supposed to be used with offline tokens, because users shouldn't be able to run queries directly using offline tokens - you could either use online tokens to use the proxy, or define your GraphQL queries in your app's backend.
@jin-ding-polymatiks you're right, ACTIVE_SHOPIFY_SHOPS
doesn't do any validation, it just checks whether the user needs to install the app. There's been a bit of discussion on loading pages in a different issue - that one was about navigation within the Admin, but it applies any time you need to load a server-rendered page.
The authorization
header will only be set if you use App Bridge's authenticatedFetch
(example from our tutorial), which means your app should do the following when a page is loaded:
- Use an unauthenticated endpoint (like
/
in the example app) to load a page skeleton - Build the App Bridge
app
in the skeleton page - Use that
app
to make calls usingauthenticatedFetch(app)
as per the example, which can be authenticated withverifyRequest
Hope this helps!
Hey everyone, let me see if I can answer some questions:
The GraphQL proxy is actually not supposed to be used with offline tokens, because users shouldn't be able to run queries directly using offline tokens - you could either use online tokens to use the proxy, or define your GraphQL queries in your app's backend.
@jin-ding-polymatiks you're right,
ACTIVE_SHOPIFY_SHOPS
doesn't do any validation, it just checks whether the user needs to install the app. There's been a bit of discussion on loading pages in a different issue - that one was about navigation within the Admin, but it applies any time you need to load a server-rendered page.The
authorization
header will only be set if you use App Bridge'sauthenticatedFetch
(example from our tutorial), which means your app should do the following when a page is loaded:
- Use an unauthenticated endpoint (like
/
in the example app) to load a page skeleton- Build the App Bridge
app
in the skeleton page- Use that
app
to make calls usingauthenticatedFetch(app)
as per the example, which can be authenticated withverifyRequest
Hope this helps!
Thanks for your answer :) So based on that. What will be the best way to get offline token on install , then get online token for every graphql request ?
cause authenticatedFetch will take the offline token if I choose offline token in Koa auth ?
Can you provide an example based on the official react app on GitHub ? Thanks
Hi @paulomarg. Thanks for answering these questions!
I'm facing the same issue as @harishannam and @psppro26. If offline tokens cannot be used to GraphQL queries, what is the right way to get the offline token at install and online token for each auth?
My use case is I need to get an offline token to make storefront API calls.
For both offline and online tokens, you can see here https://github.com/Shopify/koa-shopify-auth/issues/64#issuecomment-799409035
I am experiencing some issues properly loading the online token after auth though, but it has generated both tokens successfully.
Thanks @jt274, couldn't have said it better myself - the library can work with both offline and online tokens at the same time, they will be different sessions.
Just to clarify, you can use offline tokens to make GraphQL queries, you just shouldn't use the proxy for that - your app's backend can define the queries to run instead of taking them from the client side.
Usually, since offline tokens are used in background tasks, like webhook handling / periodic jobs, you can load them using loadOfflineSession(shop)
- note that this method doesn't run any authentication checks by itself, so you should make sure the shop comes from a secure location and not from user input.
Thanks @jt274! The solution in that commented thread seems to have worked for me.
Thanks @jt274! The solution in that commented thread seems to have worked for me.
Hey @rahulmadduluri, I can't seem to figure out how to make the solution that @jt274 commented to work. Would you mind sharing a more detailed example of the code for it?
Hey @janklimo, the problem here is that you are calling
verifyRequest
for the/
endpoint (and for static pages). With the new session token authentication, your app should always load a page skeleton without expecting a session, so it can build the App Bridge client and set up the JWT session.It's important to note that you should not include any sensitive information in that initial request since it needs to be unauthenticated.
The React + Node tutorial has been updated to follow this new pattern (in particular, steps 5-7).
Let me know if that helps unblock you, and we can mention that in the migration guide as well! In any case, we should also remove
koa-session
from the example app since it's no longer needed, and make sure that it follows the new pattern.
The tutorial is changed it seems!
@harishannam I am having the same strange behavior in my app. What was your solution? I tried everything posted in this thread but nothing works