Focuses on API authentication, access scopes, and permission management.
Hi, I'm an intern that is currently developing an embedded Shopify app for the company I'm working for and during development on local testing it works fine but once I pushed the final changes to the repository and the company deployed the app, the installation fails and gives an "Invalid OAuth callback" error when I try to install it on a test store with the deployed URL listed in the app details.
The app gets a list of products and their information
The app is deployed on an EC2 Instance, a Linux system with NPM if that is relevant to solve the problem.
The app's domain is https://shopify-app.stage.iqm.services/
If any other information is needed I can provide
My server.js file is shown below.
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();
var token = '';
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();
var url = '';
const router = new Router();
server.keys = [Shopify.Context.API_SECRET_KEY];
server.use(
createShopifyAuth({
async afterAuth(ctx) {
// Access token and shop available in ctx.state.shopify
const { shop, accessToken, scope } = ctx.state.shopify;
console.log("ACCESS TOKEN = " + accessToken);
setToken(accessToken);
const host = ctx.query.host;
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}&host=${host}`);
},
})
);
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;
url = 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.post(
"/graphql",
verifyRequest({ returnHeader: true }),
async (ctx, next) => {
await Shopify.Utils.graphqlProxy(ctx.req, ctx.res);
}
);
router.get("(/_next/static/.*)", handleRequest); // Static content is clear
router.get("/_next/webpack-hmr", handleRequest); // Webpack content is clear
router.get('/api/products', async (ctx) => {
try {
var token = getToken();
let productsURL = "https://" + url + "/admin/api/2021-04/products.json";
const results = await fetch(productsURL, {
headers: {
"X-Shopify-Access-Token": token,
},
})
.then(response => response.json())
.then(json => {
return [json, url];
});
ctx.body = {
status: 'success',
data: results
};
} catch (err) {
console.log("Fetch Failed");
console.log(err)
}
})
router.get('/shopname', async (ctx) => {
return url;
})
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}`);
});
});
function getToken() {
return token;
}
function setToken(newToken) {
token = newToken;
}
any answer? same case as well..
Any solutions? I'm facing the same issue
My issue was that 'next' was not properly set up in dev nor in production. In production environment I had to set up next.config.js so that api Key and Host url can be provided. I also had to feed my Provider config (_app.js) with api key,host and shop
(shop must me provided to AppBridge e.g "<MyProvider Component={Component} {...pageProps} shop={shop} />") parameters..
Also have the exact same issue. If anyone has a solution it would be great. My app works in development but not in production for some reason with the auth.
Okay figured out my issue. I was using the wrong API secret on the server, I accidentally copied the one for my development app instead.