koa-shopify-auth
koa-shopify-auth copied to clipboard
Not Possible To Capture shopOrigin After Callback To the App
Issue summary
I'm working on an app that requires login with twitter feature. So at first step, I should redirect merchants to the twitter authorization page (something like https://api.twitter.com/oauth/authorize?oauth_token={twitter_oauth_token}) which I'm able to do so. However, right after merchant authorize my app and callback url (e.g https://DOMAIN/oauth/callback/twitter?oauth_token={twitter-token}&oauth_verifier={twitter-verifier}) fired by twitter, the app is being redirect to the "https://domain/auth" url and it fails. I checked the browser's network tab and realized that it has failed because the app didn't know the shop origin that needs to be checked during auth since callback from twitter has no clue about it.
In past I was able to succeed this operation by relying on cookies but currently I'm using cookieless solution due to the browsers restrictions. I assume there is something to do with "ACTIVE_SHOPIFY_SHOPS" but I'm really confused and I couldn't find out a proper solution. Could you please assist me ?
package.json
{
"name": "shopify-app-node",
"version": "1.0.0",
"description": "Shopify's node app for CLI tool",
"scripts": {
"test": "jest",
"dev": "cross-env NODE_ENV=development nodemon ./server/index.js --watch ./server/index.js",
"build": "NEXT_TELEMETRY_DISABLED=1 next build",
"start": "cross-env NODE_ENV=production node ./server/index.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Shopify/shopify-app-node.git"
},
"author": "Shopify Inc.",
"license": "MIT",
"bugs": {
"url": "https://github.com/shopify/shopify-app-node/issues"
},
"dependencies": {
"@babel/core": "7.12.10",
"@babel/polyfill": "^7.6.0",
"@babel/preset-env": "^7.12.11",
"@babel/register": "^7.12.10",
"@shopify/app-bridge-react": "^1.15.0",
"@shopify/app-bridge-utils": "^1.28.0",
"@shopify/koa-shopify-auth": "^4.0.3",
"@shopify/polaris": "^5.12.0",
"@zeit/next-css": "^1.0.1",
"apollo-boost": "^0.4.9",
"axios": "^0.21.1",
"cross-env": "^7.0.3",
"dotenv": "^8.2.0",
"graphql": "^14.5.8",
"isomorphic-fetch": "^3.0.0",
"koa": "^2.13.1",
"koa-router": "^10.0.0",
"koa-session": "^6.1.0",
"next": "^10.0.4",
"next-env": "^1.1.0",
"node-fetch": "^2.6.1",
"react": "^16.10.1",
"react-apollo": "^3.1.3",
"react-dom": "^16.10.1",
"react-redux": "^7.2.2",
"redis": "^3.0.2",
"redux": "^4.0.5",
"redux-devtools-extension": "^2.13.9",
"redux-thunk": "^2.3.0",
"webpack": "^4.44.1"
},
"devDependencies": {
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-stage-3": "^7.0.0",
"babel-jest": "26.6.3",
"babel-register": "^6.26.0",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.5",
"husky": "^4.3.6",
"jest": "26.6.3",
"lint-staged": "^10.5.3",
"nodemon": "^2.0.0",
"prettier": "2.2.1",
"react-addons-test-utils": "15.6.2",
"react-test-renderer": "16.14.0"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,css,json,md}": [
"prettier --write"
]
}
}
server.js
import "@babel/polyfill";
import dotenv from "dotenv";
import "isomorphic-fetch";
import createShopifyAuth, { verifyRequest } from "@shopify/koa-shopify-auth";
import Shopify, { ApiVersion, SessionStorage } from "@shopify/shopify-api";
import Koa from "koa";
import next from "next";
import Router from "koa-router";
import * as redis from './utility/redis';
dotenv.config();
const port = parseInt(process.env.PORT, 10) || 3000;
const dev = process.env.NODE_ENV !== "production";
const app = next({
dev,
});
const handle = app.getRequestHandler();
const mySessionStorage = new Shopify.Session.CustomSessionStorage(
redis.storeSessionCallback,
redis.loadSessionCallback,
redis.deleteSessionCallback,
);
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: mySessionStorage,
});
// 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({
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.post("/graphql", verifyRequest(), 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("(.*)", 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}`);
});
});
Expected behavior
I should somehow reach the shop origin in case callback fired from remote url (twitter, facebook etc.) in order to complete authentication flow with social media accounts.
Actual behavior
- The app is being redirect to the "{domain}/auth".
- Shopify app bridge checks current window and redirect to the "{domain}/auth/inline?shop="
- Due to missing shop, it's being redirect to the following location; "https:///admin/oauth/authorize?client_id={client_id}&scope=read_products&redirect_uri={domain}/auth/callback&state={state}&grant_options[]=per-user"
Steps to reproduce the problem
You should have a valid twitter developer client to be able to reproduce this problem.
- Define callback url within twitter dev portal (e.g {domain}/oauth/callback/twitter)
- Install the app to the store and redirect app user to the twitter authorization page along necessary twitter request tokens.
- Complete authorization within twitter's page and wait for the callback url trigger.
Checklist
- [x] Please delete the labels section before submitting your issue
- [x] I have described this issue in a way that is actionable (if possible)
Yes I have the same problem unfortunately, I will message you when I find a solution.
After OAuth shopOrigin is defined, but if users navigate between pages shopOrigin becomes undefined. My code is exactly the same as the one in the shopify app node tutorial.
class MyApp extends App { render() { const { Component, pageProps, shopOrigin } = this.props; console.log(shopOrigin); const config = { apiKey: API_KEY, shopOrigin, forceRedirect: true }; return ( <React.Fragment> <Head> <title>Sample App</title> <meta charSet="utf-8" /> </Head> <Provider config={config}> <ClientRouter /> <AppProvider i18n={translations} features={{ newDesignLanguage: true }}> <MyProvider> <AxiosProvider Component={Component} shopOrigin={shopOrigin}/> {/* <Component {...pageProps}/> */} </MyProvider> </AppProvider> </Provider> </React.Fragment> ); } }
My question is also how can I keep track of the shopOrigin without using cookies.
I'm in similar boat. Getting Cannot complete OAuth process. Could not find an OAuth cookie for shop url
when I'm trying to navigate inside app.
I'm in similar boat. Getting
Cannot complete OAuth process. Could not find an OAuth cookie for shop url
when I'm trying to navigate inside app.
I'm having this same issue. I was successful when I created a route for the path I wanted to navigate to, but that's not really a solution.
Hey folks! To answer the original question, the /auth
path needs to contain the shop parameter, like /auth?shop=<my-shop-here>
, so it can know which shop is logging in.
Considering OAuth always runs on the browser top level, you should be able to store the shop using cookies / localStorage then - they're only problematic when the app is actually embedded in the Shopify Admin. That way, when the user lands back from twitter, you can read the shop from the cookie, so your flow might look something like:
- Land on app page (top level) - store the shop from the URL in a cookie / localStorage
- Perform the twitter OAuth
- When coming back from that, you should still be on the top level, so you can load the cookie from step 1 and, using that shop, redirect to
/auth?shop=<shop>
. That will ensure the Shopify process goes smoothly - Coming back, you can load App Bridge and the server will be ready to load your session
You might also want to consider doing the Shopify OAuth first which will set up your session, and then running the twitter OAuth when coming back, rather than loading App Bridge (which will load the embedded version of the app).
About the navigation issues, there's already a separate issue for that, we will respond there.
Hope this helps!
I retrieved the shop param by decoding the JWS session token and retrieving dest.
Hi @paulomarg , Thank you, your answer is really clarifies my issues. I'm able to do callbacks successfully right now as I followed your second suggestion
You might also want to consider doing the Shopify OAuth first which will set up your session, and then running the twitter OAuth when coming back, rather than loading App Bridge (which will load the embedded version of the app).
which makes more sense to me as well.
Now I'm facing with navigation issue. I'll address it within another open item #76.
@paulomarg I had to reopen this item because I'm not able to access browser top level cookie to be able to read shopOrigin except chrome incognito mode. Safari & mozilla private windows don't let me access browser top level cookie so it leads to not being able to initiate my app properly. My first app review has been done in chrome incognito mode. If chrome also not allowed to access cookies in future, it's going to be crucial problem for app review process.
Hey @barisozdogan, if it works on Chrome but fails on Safari and Firefox, it sounds like you might be missing a call to requestStorageAccess
. You can see more at https://developer.mozilla.org/en-US/docs/Web/API/Document/requestStorageAccess.
Basically, those two browsers are more restrictive than Chrome when it comes to allowing access, and there are a few rules to calling that function - it needs to be a Promise with a then
call, and it needs to happen after user interaction (like, say, clicking a button saying I allow cookies on this website
).
Considering most of the code for this library is on the server side and it works on one browser, I don't think it's an issue with our package.
Note that this repo is no longer maintained and this issue will not be reviewed. Prefer the official JavaScript API library. If you still want to use Koa, see simple-koa-shopify-auth for a potential community solution.