nprutan / koa-shopify-graphql-proxy-cookieless

MIT License
1 stars 0 forks source link

Fails with a CORS error #1

Closed pramodparanthaman closed 4 years ago

pramodparanthaman commented 4 years ago

Hi Nate,

Thanks for providing 2 very useful modules.

Do you have a working demo app that uses both koa-shopify-auth-cookieless and koa-shopify-graphql-proxy-cookieless? I got the app to install and load using the new Shopify session tokens but I'm really struggling with koa-shopify-graphql-proxy-cookieless, it seems to fail with a CORS error every time I do a graphql query.

POST /graphql 303 See Other

Access to fetch at 'https://XXXX.myshopify.com/admin/auth/login' (redirected from 'https://XX.ngrok.io/graphql') from origin 'https://XX.ngrok.io' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

nprutan commented 4 years ago

Hi, @pramodparanthaman thanks for raising the issue. Can you show me the code that is failing?

Here is a snippet from a working app:

router.post("/graphql", async (ctx, next) => {
    const bearer = ctx.request.header.authorization;
    const secret = process.env.SHOPIFY_API_SECRET;
    const valid = isVerified(bearer, secret);
    if (valid) {
      const token = bearer.split(" ")[1];
      const decoded = jwt.decode(token);
      const shop = new URL(decoded.dest).host;
      const settings = await getAppSettings(shop);
      const proxy = graphQLProxy({
        shop: shop,
        password: settings.data.getUser.token || "accessToken",
        version: ApiVersion.July20,
      });
      await proxy(ctx, next);
    }
  });

  router.get("/", async (ctx, next) => {
    const shop = getQueryKey(ctx, "shop");
    const settings = await getAppSettings(shop);
    const token = settings.data.getUser && settings.data.getUser.token;
    ctx.state = { shopify: { shop: shop, accessToken: token } };
    await verifyToken(ctx, next);
  });

Please note that I'm using the following package for isVerified: https://www.npmjs.com/package/shopify-jwt-auth-verify And also the follwing package for the jwt namespace above: https://www.npmjs.com/package/jsonwebtoken

I'm also storing credentials in AWS AppSync, so that's where the settings are being pulled on these lines:

const settings = await getAppSettings(shop);

How are you storing your access token? You mentioned that you are successfully using session tokens, but if you're not storing a valid access token, then the verifyToken method in the koa-shopify-auth-cookieless package will redirect you to /auth.

If you can show me a snippet of how you're trying to accomplish this, it might be helpful.

Nate

pramodparanthaman commented 4 years ago

I have a live Node/React embedded app on Shopify hosted on Google App Engine that I'm migrating to the new JWT authentication. I plan to use Cloud datastore to persist App Tokens, for now I'm hardcoding the token during development. Almost all my code is identical to the Shopify Node React demo: https://github.com/Shopify/shopify-demo-app-node-react

Here's my server.js code:

require('isomorphic-fetch');
const dotenv = require('dotenv');
const Koa = require('koa');
const next = require('next');
const Router = require('koa-router');
const { createShopifyAuth, verifyToken, getQueryKey, redirectQueryString } = require("koa-shopify-auth-cookieless");
const { graphQLProxy, ApiVersion } = require("koa-shopify-graphql-proxy-cookieless");
const isVerified = require("shopify-jwt-auth-verify")['default']
const jwt = require('jsonwebtoken');

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 { SHOPIFY_API_SECRET_KEY, SHOPIFY_API_KEY, HOST, TEST } = process.env;

app.prepare().then(() => {
  const server = new Koa();
  const router = new Router();
  server.keys = [SHOPIFY_API_SECRET_KEY];

  server.use(
    createShopifyAuth({
      apiKey: SHOPIFY_API_KEY,
      secret: SHOPIFY_API_SECRET_KEY,
      scopes: ['read_products', 'read_orders', 'write_inventory'],
      accessMode: 'offline',
      async afterAuth(ctx) {
        const { shop, accessToken } = ctx.state.shopify;
        // TODO: Save  shop and accessToken to datastore
        console.log('ctx.state.shopify: ', ctx.state.shopify);
        const redirectQuery = redirectQueryString(ctx);
        ctx.redirect(`/?${redirectQuery}`);
      },
    }),
  );

  router.post("/graphql", async (ctx, next) => {
    const bearer = ctx.request.header.authorization;
    const secret = SHOPIFY_API_SECRET_KEY;
    const valid = isVerified(bearer, secret);
    console.log('isVerified: ', valid); // Displays true
    if (valid) {
      const token = bearer.split(" ")[1];
      const decoded = jwt.decode(token);
      console.log('graphQL decoded: ', decoded); // Displays fully decoded JWT payload
      const shop = new URL(decoded.dest).host;
      // TODO: get accessToken from datastore using shop
      const accessToken ='shpat_XXXX';
      const proxy = graphQLProxy({
        shop: shop,
        password: accessToken,
        version: ApiVersion.July20,
      });
      await proxy(ctx, next);
    }
  });

  router.get("/", async (ctx, next) => {
    const shop = getQueryKey(ctx, "shop");
    // TODO: get accessToken from datastore using shop
    const accessToken ='shpat_XXXX';
    ctx.state = { shopify: { shop: shop, accessToken: accessToken } };
    await verifyToken(ctx, next);
  });

  router.get('*', async (ctx) => {
    const shop = getQueryKey(ctx, "shop");
    // TODO: get accessToken from datastore using shop
    ctx.state = { shopify: { shop: shop, accessToken: 'shpat_XXXX' } };
    const { accessToken } = ctx.state.shopify;
    switch(ctx.path) {
      default:
        await handle(ctx.req, ctx.res);
        ctx.respond = false;
        ctx.res.statusCode = 200;
        break;
    }
  });
  server.use(router.allowedMethods());
  server.use(router.routes());

  server.listen(port, () => {
    console.log(`> Ready on port ${port}`);
  });
});

I setup the GraphQL Apollo client code as shown in the Shopify documentation: https://shopify.dev/tutorials/authenticate-your-app-using-session-tokens

  client = new ApolloClient({
    link: new HttpLink({
      credentials: 'same-origin',
      fetch: authenticatedFetch(app),
    }),
    cache: new InMemoryCache()
  });
nprutan commented 4 years ago

Hi @pramodparanthaman, thanks for providing the code. I see a few things that should probably be changed:

  1. You should consider removing the switch statement in your wildcard route. Not sure if you're planning to add more branches to the statement, but for now, since it's just the default case it's not necessary. Instead of this:
    router.get('*', async (ctx) => {
    const shop = getQueryKey(ctx, "shop");
    // TODO: get accessToken from datastore using shop
    ctx.state = { shopify: { shop: shop, accessToken: 'shpat_XXXX' } };
    const { accessToken } = ctx.state.shopify;
    switch(ctx.path) {
      default:
        await handle(ctx.req, ctx.res);
        ctx.respond = false;
        ctx.res.statusCode = 200;
        break;
    }
    });

Try this:

router.get("*", async (ctx) => {
    await handle(ctx.req, ctx.res);
    ctx.respond = false;
    ctx.res.statusCode = 200;
  });
  1. I noticed that your ApolloClient may be using the wrong HttpLink method, or you are possibly importing the wrong package. Take a look at this working example:
    
    import { ApolloClient } from "apollo-client";
    import { createHttpLink } from "apollo-link-http";
    import { InMemoryCache } from "apollo-cache-inmemory";
    import { ApolloProvider } from "@apollo/react-hooks";
    import { AppProvider } from "@shopify/polaris";
    import "@shopify/polaris/dist/styles.css";
    import translations from "@shopify/polaris/locales/en.json";
    import createApp from "@shopify/app-bridge";
    import { authenticatedFetch } from "@shopify/app-bridge-utils";
    import { useRouter } from "next/router";

export default function MyApp({ Component, pageProps }) { const router = useRouter(); const shopOrigin = router.query.shop || "shopOrigin"; const app = createApp({ apiKey: API_KEY, shopOrigin: shopOrigin, forceRedirect: true, }); const link = new createHttpLink({ credentials: "omit", fetch: authenticatedFetch(app), // app: App Bridge instance }); const client = new ApolloClient({ link: link, cache: new InMemoryCache(), });

return (

); }

3. Take a look at the package.json for a working project and make sure you are using similar versions:

"dependencies": { "@apollo/react-hooks": "^4.0.0", "@babel/core": "7.9.0", "@babel/polyfill": "^7.6.0", "@babel/preset-env": "^7.6.2", "@babel/register": "^7.6.2", "@shopify/admin-graphql-api-utilities": "^0.1.1", "@shopify/app-bridge": "^1.26.2", "@shopify/app-bridge-utils": "1.26.2", "@shopify/app-cli-node-generator-helper": "^1.2.1", "@shopify/dates": "^0.4.1", "@shopify/koa-shopify-webhooks": "^2.3.1", "@shopify/network": "^1.5.0", "@shopify/polaris": "^5.2.1", "@shopify/react-network": "^3.5.3", "@zeit/next-css": "^1.0.1", "apollo-cache-inmemory": "^1.6.6", "apollo-client": "^2.6.10", "apollo-link-http": "^1.5.17", "aws-amplify": "^3.2.0", "aws-appsync": "^4.0.0", "dotenv": "^8.2.0", "eslint": "^7.3.1", "file-saver": "^2.0.2", "graphql": "^15.3.0", "isomorphic-fetch": "^2.1.1", "jsonwebtoken": "^8.5.1", "koa": "^2.13.0", "koa-compose": ">=3.0.0 <4.0.0", "koa-router": "^8.0.6", "koa-shopify-auth-cookieless": "^1.0.26", "koa-shopify-graphql-proxy-cookieless": "^1.0.5", "next": "^9.5.3", "next-env": "^1.1.0", "nonce": "^1.0.4", "papaparse": "^5.2.0", "react": "^16.13.1", "react-csv": "^2.0.3", "react-dom": "^16.13.1", "react-papaparse": "^3.8.0", "safe-compare": "^1.1.2", "shopify-jwt-auth-verify": "^1.0.10", "store-js": "^2.0.4" },



Let me know if that helps!
nprutan commented 4 years ago

Hi @pramodparanthaman any luck with the redirects? Let me know if you have any further questions.

I’m considering creating a barebones repo with a working example as a reference.

Would this be helpful?

pramodparanthaman commented 4 years ago

That would be great! Thanks

nprutan commented 4 years ago

Ok, I'll go ahead and put that up as soon as I'm able.

In the meantime, I'm going to close this issue.

If you have any other troubles with this package, please reopen this issue or go ahead and open a new one.

pramodparanthaman commented 4 years ago

Finally got this working.

There were two issues.

  1. Cookies need to be deleted since some apps may have migrated from the old authentication to the new JWT sessions and users may still have the old cookie in their browser.
  2. Some Apollo package added the header "x-requested-with" to the GraphQL requests causing the CORS error.

I modified your code in shopify-graphql-proxy.js to fix both issues using proxyReqOptDecorator(proxyReqOpts). Please update and also add the latest Shopify API (October20: "2020-10").

Thanks!

    await proxy(shop, {
      https: true,
      parseReqBody: false,
      // Setting request header here, not response. That's why we don't use ctx.set()
      // proxy middleware will grab this request header
      headers: {
        "Content-Type": "application/json",
        "X-Shopify-Access-Token": accessToken,
      },
      proxyReqOptDecorator(proxyReqOpts) {
        delete proxyReqOpts.headers.cookie;
        delete proxyReqOpts.headers.Cookie;
        delete proxyReqOpts.headers['x-requested-with'];
        return proxyReqOpts;
      },
      proxyReqPathResolver() {
        return `https://${shop}${GRAPHQL_PATH_PREFIX}/${version}/graphql.json`;
      },
    })(
      ctx,

      /*
        We want this middleware to terminate, not fall through to the next in the chain,
        but sadly it doesn't support not passing a `next` function. To get around this we
        just pass our own dummy `next` that resolves immediately.
      */
      noop
    );
  };
}
nprutan commented 4 years ago

@pramodparanthaman, great thanks for the fix. I'll release a new package with the update as soon as I get a chance to test it.

nprutan commented 3 years ago

Hi @pramodparanthaman, FYI, the fix has now been incorporated in the latest package.

Also, I created a Shopify Cookieless Demo you can check out here:

https://github.com/nprutan/shopify-cookieless-auth-demo