Coder Social home page Coder Social logo

shopify / koa-shopify-auth Goto Github PK

View Code? Open in Web Editor NEW
80.0 80.0 64.0 330 KB

DEPRECATED Middleware to authenticate a Koa application with Shopify

License: MIT License

TypeScript 21.34% JavaScript 78.64% Shell 0.02%
javascript koa middleware shopify

koa-shopify-auth's Introduction

DEPRECATED @shopify/koa-shopify-auth

NOTE: this repo is no longer maintained. Prefer the official Node API.

If you're still wanting to use Koa, see simple-koa-shopify-auth for a potential community solution.

Build Status License: MIT npm version

Middleware to authenticate a Koa application with Shopify.

Sister module to @shopify/shopify-express, but simplified.

Features you might know from the express module like the webhook middleware and proxy will be presented as their own packages instead.

Warning: versions prior to 3.1.68 vulnerable to reflected XSS

Versions prior to 3.1.68 are vulnerable to a reflected XSS attack. Please update to the latest version to protect your app.

Installation

This package builds upon the Shopify Node Library, so your app will have access to all of the library's features as well as the Koa-specific middlewares this package provides.

$ yarn add @shopify/koa-shopify-auth

Usage

This package exposes shopifyAuth by default, and verifyRequest as a named export. To make it ready for use, you need to initialize the Shopify Library and then use that to initialize this package:

import shopifyAuth, {verifyRequest} from '@shopify/koa-shopify-auth';
import Shopify, {ApiVersion} from '@shopify/shopify-api';

// Initialize the library
Shopify.Context.initialize({
  API_KEY: 'Your API_KEY',
  API_SECRET_KEY: 'Your API_SECRET_KEY',
  SCOPES: ['Your scopes'],
  HOST_NAME: 'Your HOST_NAME (omit the https:// part)',
  API_VERSION: ApiVersion.October20,
  IS_EMBEDDED_APP: true,
  // More information at https://github.com/Shopify/shopify-node-api/blob/main/docs/issues.md#notes-on-session-handling
  SESSION_STORAGE: new Shopify.Session.MemorySessionStorage(),
});

shopifyAuth

Returns an authentication middleware taking up (by default) the routes /auth and /auth/callback.

app.use(
  shopifyAuth({
    // if specified, mounts the routes off of the given path
    // eg. /shopify/auth, /shopify/auth/callback
    // defaults to ''
    prefix: '/shopify',
    // set access mode, default is 'online'
    accessMode: 'offline',
    // callback for when auth is completed
    afterAuth(ctx) {
      const {shop, accessToken} = ctx.state.shopify;

      console.log('We did it!', accessToken);

      ctx.redirect('/');
    },
  }),
);

/auth

This route starts the oauth process. It expects a ?shop parameter and will error out if one is not present. To install it in a store just go to /auth?shop=myStoreSubdomain.

/auth/callback

You should never have to manually go here. This route is purely for shopify to send data back during the oauth process.

verifyRequest

Returns a middleware to verify requests before letting them further in the chain.

Note: if you're using a prefix for shopifyAuth, that prefix needs to be present in the paths for authRoute and fallbackRoute below.

app.use(
  verifyRequest({
    // path to redirect to if verification fails
    // defaults to '/auth'
    authRoute: '/foo/auth',
    // path to redirect to if verification fails and there is no shop on the query
    // defaults to '/auth'
    fallbackRoute: '/install',
    // which access mode is being used
    // defaults to 'online'
    accessMode: 'offline',
    // if false, redirect the user to OAuth. If true, send back a 403 with the following headers:
    //  - X-Shopify-API-Request-Failure-Reauthorize: '1'
    //  - X-Shopify-API-Request-Failure-Reauthorize-Url: '<auth_url_path>'
    // defaults to false
    returnHeader: true,
  }),
);

Migrating from cookie-based authentication to session tokens

Versions prior to v4 of this package used cookies to store session information for your app. However, internet browsers have been moving to block 3rd party cookies, which creates issues for embedded apps.

If you have an app using this package, you can migrate from cookie-based authentication to session tokens by performing a few steps:

  • Upgrade your @shopify/koa-shopify-auth dependency to v4+
  • Update your server as per the Usage instructions to properly initialize the @shopify/shopify-api library
  • If you are using accessMode: 'offline' in shopifyAuth, make sure to pass the same value in verifyRequest
  • Install @shopify/app-bridge-utils in your frontend app
  • In your frontend app, replace fetch calls with authenticatedFetch from App Bridge Utils

Note: the backend steps need to be performed to fully migrate your app to v4, even if your app is not embedded.

You can learn more about session tokens in our authentication tutorial. Go to the frontend changes section under Setup for instructions and examples on how to update your frontend code.

Example app

This example will enable you to quickly set up the backend for a working development app. Please read the Gotchas session below to make sure you are ready for production use.

import 'isomorphic-fetch';

import Koa from 'koa';
import Router from 'koa-router';
import shopifyAuth, {verifyRequest} from '@shopify/koa-shopify-auth';
import Shopify, {ApiVersion} from '@shopify/shopify-api';

// Loads the .env file into process.env. This is usually done using actual environment variables in production
import dotenv from 'dotenv';
dotenv.config();

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

// initializes the library
Shopify.Context.initialize({
  API_KEY: process.env.SHOPIFY_API_KEY,
  API_SECRET_KEY: process.env.SHOPIFY_API_SECRET,
  SCOPES: process.env.SHOPIFY_APP_SCOPES,
  HOST_NAME: process.env.SHOPIFY_APP_URL.replace(/^https:\/\//, ''),
  API_VERSION: ApiVersion.October20,
  IS_EMBEDDED_APP: true,
  // More information at https://github.com/Shopify/shopify-node-api/blob/main/docs/issues.md#notes-on-session-handling
  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 = {};

const app = new Koa();
const router = new Router();
app.keys = [Shopify.Context.API_SECRET_KEY];

// Sets up shopify auth
app.use(
  shopifyAuth({
    async afterAuth(ctx) {
      const {shop, accessToken} = ctx.state.shopify;
      ACTIVE_SHOPIFY_SHOPS[shop] = true;

      // Your app should handle the APP_UNINSTALLED webhook to make sure merchants go through OAuth if they reinstall it
      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['APP_UNINSTALLED'].success) {
        console.log(
          `Failed to register APP_UNINSTALLED webhook: ${response['APP_UNINSTALLED'].result}`,
        );
      }

      // Redirect to app with shop parameter upon auth
      ctx.redirect(`/?shop=${shop}`);
    },
  }),
);

router.get('/', async (ctx) => {
  const shop = ctx.query.shop;

  // If 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 {
    // Load app skeleton. Don't include sensitive information here!
    ctx.body = '๐ŸŽ‰';
  }
});

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}`);
  }
});

// Everything else must have sessions
router.get('(.*)', verifyRequest(), async (ctx) => {
  // Your application code goes here
});

app.use(router.allowedMethods());
app.use(router.routes());
app.listen(port, () => {
  console.log(`> Ready on http://localhost:${port}`);
});

Gotchas

Session

The provided MemorySessionStorage class may not be scalable for production use. You can implement your own strategy by creating a class that implements a few key methods. Learn more about how the Shopify Library handles sessions.

Testing locally

By default this app requires that you use a myshopify.com host in the shop parameter. You can modify this to test against a local/staging environment via the myShopifyDomain option to shopifyAuth (e.g. myshopify.io).

koa-shopify-auth's People

Contributors

amorriscode avatar andyw8 avatar atesgoral avatar devjones avatar eran-pinhas avatar geddski avatar jakxz avatar mkevinosullivan avatar mllemango avatar mmccall10 avatar osmszk avatar paulomarg avatar shopify-shipitnext[bot] avatar thecodepixi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

koa-shopify-auth's Issues

TypeError [ERR_INVALID_ARG_TYPE]

Issue summary

I've followed the nodejs and react tutorial to build an app, but I'm getting errors when I try to install my app to my test store via the partners dashboard.

Expected behavior

The app should be installed in the test store.

Actual behavior

I'm getting the following errors when I try to install my app to my test store via the partners dashboard:

  TypeError [ERR_INVALID_ARG_TYPE]: The "key" argument must be of type string or an instance of Buffer, TypedArray, DataView, or KeyObject. Received undefined
      at prepareSecretKey (internal/crypto/keys.js:322:11)
      at new Hmac (internal/crypto/hash.js:111:9)
      at Object.createHmac (crypto.js:147:10)
      at sign (โ€ฆ/node_modules/keygrip/index.js:23:8)
      at Keygrip.sign (โ€ฆ/node_modules/keygrip/index.js:30:38)
      at Cookies.set (โ€ฆ/node_modules/cookies/index.js:110:30)
      at oAuthStart (โ€ฆ/node_modules/@shopify/koa-shopify-auth/dist/src/auth/create-oauth-start.js:18:21)
      at โ€ฆ/node_modules/@shopify/koa-shopify-auth/dist/src/auth/index.js:55:46
      at step (โ€ฆ/node_modules/tslib/tslib.js:141:27)
      at Object.next (โ€ฆ/node_modules/tslib/tslib.js:122:57)
  TypeError [ERR_INVALID_ARG_TYPE]: The "key" argument must be of type string or an instance of Buffer, TypedArray, DataView, or KeyObject. Received undefined
      at prepareSecretKey (internal/crypto/keys.js:322:11)
      at new Hmac (internal/crypto/hash.js:111:9)
      at Object.createHmac (crypto.js:147:10)
      at sign (โ€ฆ/node_modules/keygrip/index.js:23:8)
      at Keygrip.sign (โ€ฆ/node_modules/keygrip/index.js:30:38)
      at Cookies.set (โ€ฆ/node_modules/cookies/index.js:110:30)
      at topLevelOAuthRedirect (โ€ฆ/node_modules/@shopify/koa-shopify-auth/dist/src/auth/create-top-level-oauth-redirect.js:10:21)
      at โ€ฆ/node_modules/@shopify/koa-shopify-auth/dist/src/auth/index.js:61:46
      at step (โ€ฆ/node_modules/tslib/tslib.js:141:27)
      at Object.next (โ€ฆ/node_modules/tslib/tslib.js:122:57)

Steps to reproduce the problem

  1. Setup a test app like in the tutorial
  2. Install it to a test store via the partners dashboard

Reduced test case

The best way to get your bug fixed is to provide a reduced test case.


Checklist

  • Please delete the labels section before submitting your issue
  • I have described this issue in a way that is actionable (if possible)

associated_user is inaccessible if accessMode is offline

Issue summary

associated user is inaccessible if accessMode is 'offline',

Write a short description of the issue here โ†“

#23 added associated user to the session, but its inaccessible if accessMode is set to 'offline'

Checklist

  • I have described this issue in a way that is actionable (if possible)

Online (session) tokens instead of permanent offline tokens

Overview
I have next code (using @shopify/koa-shopify-auth):

...
 server.use(session({ secure: true, sameSite: "none" }, server));
  server.use(
    shopifyAuth({
      apiKey: SHOPIFY_API_KEY,
      secret: SHOPIFY_API_SECRET_KEY,
      scopes: ["write_script_tags", "read_script_tags"],
      accessMode: "offline",
      async afterAuth(ctx) {
        const { shop, accessToken } = ctx.session;
        console.log("We did it!", accessToken);
        ctx.cookies.set("shopOrigin", shop, {
          httpOnly: false,
          secure: true,
          sameSite: "None"
        });
...

But for some reason the result is:
image
So with accessMode: "offline" I still get online tokens?
Please, correct me If I am doing something wrong. I need permanent offline tokens to avoid window redirect & refresh for users on every login.

[koa-shopify-auth] After approving subscription, embedded app 404s

I'm having a strange issue with the redirects when I host the app on my own domain. With ngrok, I follow the URL to install the shop and everything works fine, but when I move the app to my domain, it doesn't work.

Steps are:

  1. Authenticate app - this works fine
  2. Redirects to my domain and can see the app fine
  3. Redirect back to shopify and I get the following message
    2020-04-08-111740_912x864_scrot
  4. If I go back to apps and then click back into my app it loads the app as expected.

The error only seems to occur on the initial install.

I'm also getting a postMessage error in the console but I'm not sure that's related.

I'm using the sample here: https://shopify.dev/tutorials/build-a-shopify-app-with-node-and-react and basically copied the code in their repo.

Any help appreciated.

Can not get online access token

Overview

I am using @shopify/koa-shopify-auth and need to implement online access mode authorization flow, I set accessMode to online but still get offline access token which does not contain associated_user data described in Shopify docs

shopifyAuth({
  apiKey: SHOPIFY_API_KEY,
  secret: SHOPIFY_API_SECRET,
  scopes: ['write_products'],
  accessMode: 'online',
  afterAuth(ctx) {
    const {shop, accessToken} = ctx.session;
    
    console.log('We did it!', accessToken);

    return ctx.redirect('/');
  },
}),

screenshot

Please, correct me If I am doing something wrong.

Consuming repo

createShopify middleware is not getting called on firebase functions after deploying it

The createShopify middleware is not getting called on firebase functions after deploying it.
But it works well on local emulator.
Any idea how to make this middleware createShopifyAuth() called ?
Thank you,

const { default: createShopifyAuth } = require('@shopify/koa-shopify-auth');
const server = new Koa();
server.use(session({ secure: true, sameSite: 'none' }, server));
server.keys = [API_SECRET_KEY];
server.use(
createShopifyAuth({
apiKey: API_KEY,
secret: API_SECRET_KEY,
scopes: ['read_p'],
afterAuth(ctx) {
ctx.redirect('/');
},
})
)

Embedded app cannot complete OAuth process, and custom session storage documentation

Issue summary

App authentication broken after updating to the "new" way of authenticating embedded apps, according to the Shopify tutorial here: https://shopify.dev/tutorials/build-a-shopify-app-with-node-and-react

The previous tutorial produced a working app.

Error: Cannot complete OAuth process. No session found for the specified shop url:

Additionally, the tutorial utilizes MemorySessionStorage, and tells you not to use it. The following page provides a vague explanation of CustomSessionStorage, but does not give enough detail for a working example: https://github.com/Shopify/shopify-node-api/blob/main/docs/issues.md#notes-on-session-handling

The app in question produces the error with MemorySessionStorage.

Expected behavior

App should authenticate once, and store the session so no further authentication is required.

Tutorials should document a fully working CustomSessionStorage example, and explain how to properly access the shopOrigin parameter throughout the React app with cookies no longer active.

Actual behavior

App re-authenticates on almost every page refresh, or displays an error: Cannot complete OAuth process. No session found for the specified shop url:. This also may produce console errors for Graphql requests such as "invalid token < in JSON"

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.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}`);
  });
});

_app.js file below:

import React from 'react';
import App from 'next/app';
import Head from 'next/head';
import { AppProvider } from '@shopify/polaris';
import { Provider, Context } from "@shopify/app-bridge-react";
import { authenticatedFetch } from "@shopify/app-bridge-utils";
import '@shopify/polaris/dist/styles.css';
import translations from '@shopify/polaris/locales/en.json';
import ClientRouter from '../components/ClientRouter';
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from 'react-apollo';
import '../uptown.css';

class MyProvider extends React.Component {
  static contextType = Context;

  render() {
    const app = this.context;

    const client = new ApolloClient({
      fetch: authenticatedFetch(app),
      fetchOptions: {
        credentials: "include",
      },
    });

    return (
      <ApolloProvider client={client}>
        {this.props.children}
      </ApolloProvider>
    );
  }
}

class MyApp extends App {
  render() {
    const { Component, pageProps, shopOrigin } = this.props;
    const config = { apiKey: process.env.NEXT_PUBLIC_SHOPIFY_API_KEY, shopOrigin: shopOrigin, forceRedirect: true };
    return (
      <React.Fragment>
        <Head>
          <title>My App</title>
          <meta charSet="utf-8" />
        </Head>
        <Provider config={config}>
          <ClientRouter />
          <AppProvider i18n={translations}>
            <MyProvider>
              <Component {...pageProps} />
            </MyProvider>
          </AppProvider>
        </Provider>
      </React.Fragment>
    );
  }
}

MyApp.getInitialProps = async ({ ctx }) => {
  return {
    shopOrigin: ctx.query.shop,
  }
}

export default MyApp;

ACTIVE_SHOPIFY_SHOPS documentation and example

Overview/summary

There is little documentation in regard to the ACTIVE_SHOPIFY_SHOPS hash. Documentation mentions it is important, but does not give much detail.

Motivation

Current documentation in the tutorial (https://shopify.dev/tutorials/build-a-shopify-app-with-node-and-react/embed-your-app-in-shopify) reads:

Create the ACTIVE_SHOPIFY_SHOPS hash and track shops that complete OAuth. Your app needs this to decide whether a new shop needs to perform OAuth to install it:
Storing the active shops in memory will force merchants to go through OAuth again every time your server is restarted. We recommend you persist the shops to minimize the number of logins merchants need to perform.

The documentation does not make the following clear:

  1. Should this variable store active shops that only the current authorized user has installed this app on?
  2. Should this variable store a list of ALL shops across Shopify that have installed this app, regardless of the user?
  3. At what point should the current active shops be loaded? I would assume before auth is complete.
  4. How exactly is the ACTIVE_SHOPIFY_SHOPS related to the storing/recall of the user session?
  5. Is there any difference for online or offline session modes?

Redirect after invalid token and successful oAuth

Hi there,

We are using Shopify Koa for our oAuth Shopify process. Thank you all of you who work on it. Appreciated.

Here is an issue we have:
If your session expires during using the app and you click e.g the /producs page you get redirected to Shopify, then the /auth/callback is invoked and in my afterAuth() function we call ctx.redirect('/'). We want the user to be redirected to the original page requested (/products). However it seems that this information is lost during the oAuth. Is it possible to fix that?

P.S I hope you guys will be able to add the Shopify session tokens auth mechanism soon. We want to switch because of our Safari users and the cookies problems they have

Cheers!

Node ESM support: The requested module '@shopify/koa-shopify-auth' does not provide an export named 'verifyRequest'

Overview

I am trying to import the library to node with experimental modules enabled (node --expermental-modules server.js, but the ESM loader claims that the export doesn't exist.

I have tried various other ways of importing like {* as shopifyAuth} but those don't seem to work either. I think that the reason for this is issue is defined in https://nodejs.org/api/esm.html#esm_writing_dual_packages_while_avoiding_or_minimizing_hazards

Building a wrapper like the one mentioned in the repo should solve the problem:

// ./node_modules/pkg/wrapper.mjs
import cjsModule from './index.cjs';
export const name = cjsModule.name;

This approach is appropriate for any of the following use cases:

The package is currently written in CommonJS and the author would prefer not to refactor it into ES module syntax, but wishes to provide named exports for ES module consumers.

Motivation

Better support for @shopify/koa-shopify-auth and ESM modules support.

Code

TypeScript Version: 3.8.3
Node version: 14.0.0

import createShopifyAuth, { verifyRequest } from '@shopify/koa-shopify-auth';

tsconfig.json:

{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": false,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "outDir": "build"
  },
  "exclude": [
    "node_modules"
  ],
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    "server.ts"
  ]
}

command:

"start":  "tsc && node --experimental-modules ./build/server.js"

Expected behavior:

Everything works

Actual behavior:

The requested module '@shopify/koa-shopify-auth' does not provide an export named 'verifyRequest'

@koa-shopify-auth: createShopifyAuth prefix is killing two birds with one stone

I wouldn't write a story or detail anything. I have gone in circles with the whole existing stack provided by shopify only because I chose not to use top level domain for app.

Pretty much all the issues here are coming from impractical assumptions made on prefix option accepted by createShopifyAuth.

Currently, the oAuth paths constructed using prefix are then being compared with ctx.path for redirect logic, but ctx.path is almost always relative in deployments behind a proxy.

ctx.originUrl could be used, right now my head is a centrifuge, I will add more to this once I am over hacking around my deployments and get an indication that this middleware being used in CLI generated product codebase is not as orphan as it seems to be.

Oh and for TLDR, this is resulting in bad redirections, redirect loops, broken in-app view, cookie issues, callback url glitch and countless reported issues with wrong diagnostics.

Api Versions are out of date

Issue summary

I am using this as specified in the "Usage" section, by also including import Shopify, {ApiVersion} from '@shopify/shopify-api';
When I print out the available ApiVersions, I get an old list:

ApiVersions: {
  July19: '2019-07',
  October19: '2019-10',
  January20: '2020-01',
  April20: '2020-04',
  July20: '2020-07',
  October20: '2020-10',
  Unstable: 'unstable',
  Unversioned: 'unversioned'
}

Seems that these are cached values. How do I get the latest API versions to load?

I would post this in the shopify-api github, but that seems to be a Ruby repo, so I didn't think it was appropriate to ask there.


super confusing... there is a package called shopify_api and one called @shopify/shopify-api. The later is NOT ruby, so I'll post there.

SameSite=None not working on oauth authorize

Issue summary

Hi, I'm facing an issue with the third-party cookies on the oauth autorize flow. It seems that the oauth autorize route doesn't set SameSite=None;Secure parameters leading to the app not loading.

Expected behavior

The https://xxx.myshopify.com/admin/oauth/authorize url should set cookies with SameSite=None;Secure parameters.

Actual behavior

The https://xxx.myshopify.com/admin/oauth/authorize set cookies with SameSite=Lax (or without any) parameters. Causing app to reload three times before landing on apps list with error : This app canโ€™t load due to an issue with browser cookies. The others routes (https://example.com/?shop=xxx.myshopify.com, https://example.com/auth?shop=xxx.myshopify.com, https://example.com/auth/inline?shop=xxx.myshopify.com, https://example.com/auth/callback all have corrects SameSite=None;Secure parameters since configured in our code

screenshot

Steps to reproduce the problem

Like I said, cookies set from our end are corrects since we followed the docs with :

const cookieOptions = { httpOnly: false, secure: true, sameSite: 'none' }
const server = new Koa();
server.use(session(cookieOptions, server));
server.use(
        createShopifyAuth({
            apiKey: Shopify.Context.API_KEY,
            secret: Shopify.Context.API_SECRET_KEY,
            scopes: ['xxx'],
            afterAuth(ctx) {
                const { shop, scope, accessToken } = ctx.state.shopify;
                ACTIVE_SHOPIFY_SHOPS[shop] = scope;
                ctx.cookies.set("accessToken", accessToken, cookieOptions);
                ctx.cookies.set("shopOrigin", shop, cookieOptions);
                ctx.redirect(`/?shop=${shop}`);
            },
        }),

Thank you for your time and your help ๐Ÿ™‚

Handling generated records sessions

Issue summary

Every time when I load app new sessions get generated.

Expected behavior

Generated sessions need to be deleted either on deleteCallback or sort of periodically job clean.

Actual behavior

image

I am wondering how other folks handling this.

Embedded app OAuth not working properly with custom session storage

Issue summary

After trying to migrate from cookie-based authentication to session tokens, using my custom session storage, my OAuth process stopped working properly.

I used the solution posted in the shopify-node-api repository as starting point, my custom session storage doesn't use Redis, I store my sessions in a MySQL database where each record holds the session ID and its body.

I also used the Node and React tutorial to make the proper changes in the server.js and _app.js files.

When using the MemorySessionStorage the app main page loaded correctly, though when trying to navigate the other pages using the embedded navigation items, the app would go to the OAuth process again and then load the main page, never displaying the pages linked to the navigation items.

After implementing my custom session storage, the OAuth process stopped working, the app receives the session object and stores it in the database, but when trying to load it, that's when everything fails.

I also would like to know if I should persist the ACTIVE_SHOPIFY_SHOPS object, it's not clear to me whether the implementation of the CustomSessionStorage means I should not use that object anymore or if I should use it along with that implementation; so if I need to keep using it, how should I persist it and when should I set/update it?

Expected behavior

The app should be authenticated, store the session and then, whenever it is required, load the session and the app with no issues.

Actual behavior

App authenticates, stores the session but when trying to load it, the loaded session isn't valid.

Logging to the console the session object before adding it to the database, I get this:

Session {
  id: 'offline_[shop].myshopify.com',
  shop: '[shop].myshopify.com',
  state: '806541391131453',
  isOnline: false
}

The app then stores the ID along with the stringified JSON object.

As you can see, the Session doesn't include the accessToken nor the scope, so when trying to load it, I get this error in the server:

TypeError: Cannot read property 'map' of undefined
      at new AuthScopes (/home/ilugo/Downloads/shopify-app-node-master/node_modules/@shopify/shopify-api/dist/auth/scopes/index.js:14:35)
      at AuthScopes.equals (/home/ilugo/Downloads/shopify-app-node-master/node_modules/@shopify/shopify-api/dist/auth/scopes/index.js:38:21)
      at /home/ilugo/Downloads/shopify-app-node-master/node_modules/@shopify/koa-shopify-auth/dist/src/verify-request/verify-token.js:20:79
      at step (/home/ilugo/Downloads/shopify-app-node-master/node_modules/tslib/tslib.js:133:27)
      at Object.next (/home/ilugo/Downloads/shopify-app-node-master/node_modules/tslib/tslib.js:114:57)
      at fulfilled (/home/ilugo/Downloads/shopify-app-node-master/node_modules/tslib/tslib.js:104:62)
      at processTicksAndRejections (internal/process/task_queues.js:93:5)

In the front-end I get an Internal Server Error

In consequence, ctx.state.shopify in afterAuth doesn't include values for accessToken and scope; for a strange reason, the app tries to store the session once again, but this time the console logs the following:

Session {
  id: 'offline_[shop].myshopify.com',
  shop: '[shop].myshopify.com',
  state: '522271201811028',
  isOnline: false,
  accessToken: '[accessToken]',
  scope: 'read_all_orders,write_orders,write_customers,write_script_tags'
}

I don't understand why the OAuth process executes the storeCallback function if the Session object doesn't include all the data and then tries to execute it again with a valid object.

Adding the involved files below for a better understanding:

server.js

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";

const bodyParser = require('koa-bodyparser');

const stringInject = require('stringinject');

const dashboardGetHelper = require('../getHelpers/dashboardGetHelper');
const storefrontSetHelper = require('../setHelpers/storefrontSetHelper');
const dashboardActionHelper = require('../actionHelpers/dashboardActionHelper');
const serverGetHelper = require('../getHelpers/serverGetHelper');
const serverSetHelper = require('../setHelpers/serverSetHelper');
const serverActionHelper = require('../actionHelpers/serverActionHelper');
const storefrontGetHelper = require('../getHelpers/storefrontGetHelper');
const storefrontActionHelper = require('../actionHelpers/storefrontActionHelper');

// database helpers
const dbSetHelper = require('../database/dbSetHelper');
const dbGetHelper = require('../database/dbGetHelper');
import SessionHandler from './handlers/sessionHandler';

// log messages in their own file
const logMessages = require('../util/logMessages');

// logger
const logger = require('../util/logger');

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 sessionStorage = new SessionHandler();

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.January21,
  IS_EMBEDDED_APP: true,
  IS_PRIVATE_APP: false,
  // This should be replaced with your preferred storage strategy
  //SESSION_STORAGE: new Shopify.Session.MemorySessionStorage(),
  SESSION_STORAGE: new Shopify.Session.CustomSessionStorage(
    sessionStorage.storeCallback,
    sessionStorage.loadCallback,
    sessionStorage.deleteCallback
  ),
});

// 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.use(bodyParser());
  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;
        const installedStatus = await dbGetHelper.getInstalledStatus(shop);

        if (installedStatus !== undefined && installedStatus !== null && (installedStatus === 0 || installedStatus === 2)) {
          console.log(`we gonna install`);
          await dbSetHelper.saveAccessTokenForShop(shop, accessToken);
          await dbSetHelper.createBackfillDataRow(shop);
          await dbSetHelper.saveShopLanguage(shop, 'en');
          await serverSetHelper.createActiveMode(shop, accessToken);
          await serverSetHelper.createApiMode(shop, accessToken);
          await serverSetHelper.createBackfillMode(shop, accessToken);
          await serverSetHelper.createApiKeys(shop, accessToken);
          
          ACTIVE_SHOPIFY_SHOPS[shop] = scope;
          
          const responseUninstall = await Shopify.Webhooks.Registry.register({
            shop,
            accessToken,
            path: "/webhooks",
            topic: "APP_UNINSTALLED",
            webhookHandler: async (topic, shop, body) =>
              delete ACTIVE_SHOPIFY_SHOPS[shop],
          });

          if (!responseUninstall.success) {
            console.log(
              `Failed to register APP_UNINSTALLED webhook: ${responseUninstall.result}`
            );
          }

          const responseCreate = await Shopify.Webhooks.Registry.register({
            shop,
            accessToken,
            path: "/webhooks",
            topic: "ORDERS_CREATE",
            webhookHandler: async (topic, shop, body) =>
              delete ACTIVE_SHOPIFY_SHOPS[shop],
          });

          if (!responseCreate.success) {
            console.log(
              `Failed to register ORDERS_CREATE webhook: ${responseCreate.result}`
            );
          }

          const scriptTagStatus = await serverGetHelper.getScriptTags(shop, accessToken, `${process.env.HOST}/fingerprint.js`);

          // checks if there is already a script tag with the same src
          if (scriptTagStatus !== undefined && scriptTagStatus !== null && scriptTagStatus === 0) {
            const scriptTagBody = {
              'script_tag': {
                'event': 'onload',
                'src': `${process.env.HOST}/fingerprint.js`,
                'display_scope': 'online_store'
              }
            };

            fetch(`https://${shop}/admin/api/${process.env.API_VERSION}/script_tags.json`, {
              method: 'POST',
              credentials: 'include',
              body: JSON.stringify(scriptTagBody),
              headers: {
                'Content-Type': 'application/json',
                'X-Shopify-Access-Token': accessToken,
                'Accept': 'application/json'
              },
            });
          } else if (scriptTagStatus === undefined || scriptTagStatus === null) {
            logger.error(stringInject.default(logMessages.serverScriptTagStatusUndefined, [shop]));
          }

          // Redirect to app with shop parameter upon auth
          ctx.redirect(`/?shop=${shop}`);
        } else if (installedStatus !== undefined && installedStatus !== null && installedStatus === 1) {
          ACTIVE_SHOPIFY_SHOPS[shop] = scope;
          ctx.redirect(`/?shop=${shop}`);
        } else if (installedStatus === undefined || installedStatus === null) {
          logger.error(stringInject.default(logMessages.serverInstalledStatusUndefined, [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({ accessMode: 'offline' }), handleRequest); // Everything else must have sessions

  server.use(router.allowedMethods());
  server.use(router.routes());
  server.listen(port, () => {
    console.log(`> Ready on http://localhost:${port}`);
  });
});

_app.js

import ApolloClient from "apollo-boost";
import { ApolloProvider } from "react-apollo";
import App from "next/app";
import { AppProvider } from "@shopify/polaris";
import { Provider, useAppBridge } from "@shopify/app-bridge-react";
import { authenticatedFetch } from "@shopify/app-bridge-utils";
import "@shopify/polaris/dist/styles.css";
import translations from "@shopify/polaris/locales/en.json";

function MyProvider(props) {
  const app = useAppBridge();

  const client = new ApolloClient({
    fetch: authenticatedFetch(app),
    fetchOptions: {
      credentials: "include",
    },
  });

  const Component = props.Component;

  return (
    <ApolloProvider client={client}>
      <Component {...props} />
    </ApolloProvider>
  );
}

class MyApp extends App {
  render() {
    const { Component, pageProps, shopOrigin } = this.props;
    return (
      <AppProvider i18n={translations}>
        <Provider
          config={{
            apiKey: API_KEY,
            shopOrigin: shopOrigin,
            forceRedirect: true,
          }}
        >
          <MyProvider Component={Component} {...pageProps} />
        </Provider>
      </AppProvider>
    );
  }
}

MyApp.getInitialProps = async ({ ctx }) => {
  return {
    shopOrigin: ctx.query.shop,
  };
};

export default MyApp;

SessionHandler.js

import { Session } from '@shopify/shopify-api/dist/auth/session';
import { promisify } from 'util';
const dbSetHelper = require('../../database/dbSetHelper');
const dbGetHelper = require('../../database/dbGetHelper');

class SessionHandler {

 async storeCallback(session) {
  try {
    console.log('session to store');
    console.log(session);
    if(dbSetHelper.saveSessionForShop(session.id, JSON.stringify(session))) {
      return true;
    } else {
      return false;
    }
  } catch (err) {
    // throw errors, and handle them gracefully in your application
    throw new Error(err)
  }
 }

 async loadCallback(id) {
  try {
    var reply = await dbGetHelper.getSessionForShop(id);
    if (reply) {
      const parsedJson = JSON.parse(reply);
      var newSession = new Session(parsedJson['id']);
      newSession.shop = parsedJson['shop'];
      newSession.state = parsedJson['state'];
      newSession.isOnline = parsedJson['isOnline'];
      return newSession;
    } else {
      return undefined;
    }
  } catch (err) {
    // throw errors, and handle them gracefully in your application
    throw new Error(err)
  }
 }

 async deleteCallback(id) {
  try {
    if(dbSetHelper.deleteSessionForShop(id)) {
      return true;
    } else {
      return false;
    }
  } catch (err) {
    // throw errors, and handle them gracefully in your application
    throw new Error(err)
  }
 }
}

// Export the class
module.exports = SessionHandler

App prefix doesn't work

Is there an example of a project using next.js and an app-prefix (which is required to use next.js api routes)? As is, can't seem to get the app prefix stuff to work.

server.use(
    createShopifyAuth({
      apiKey: SHOPIFY_API_KEY,
      prefix: "/shopifyapp/",
      secret: SHOPIFY_API_SECRET,
      scopes: [SCOPES],
      accessMode: "offline",

      async afterAuth(ctx) {
        // set cookies and stuff
      },
    })
  );

  server.use(
    mount(
      "/shopifyapp",
      graphQLProxy({
        version: ApiVersion.October19,
      })
    )
  );

  server.use(verifyRequest({
  	// default would be '/auth'
  	authRoute: '/shopifyapp/auth',
  	fallbackRoute: '/shopifyapp/install',
  }));

  router.get("(.*)", async (ctx) => {
    await handle(ctx.req, ctx.res);
    ctx.respond = false;
    ctx.res.statusCode = 200;
  });

Then for my app url I do https://853e7.ngrok.io/shopifyapp within the partners dashboard

Here are my logs:
Screen Shot 2020-12-19 at 4 43 13 PM

[koa-shopify-auth] not reliably returning offline accessTokens

Overview

I have deployed this app to production, and while 95% of my customer installs result in an offline accessToken on ctx.session (with no expiry date), about 5% of installs generate an accessToken that expires in ~ 24hrs. Because my app required long-term API calls, this is a deal breaker and causing big problems for those 5% of customers.

Reproduction: I do not have a guaranteed way to reproduce the bug. If I ask the customer to clear their browser cache before installing, it seems to always result in an offline token, so maybe that's a hint.

Package: @shopify/koa-shopify-auth

Package.json

"dependencies": {
    "@shopify/app-bridge-react": "^1.26.2",
    "@shopify/koa-shopify-auth": "^3.1.65",
    "@shopify/koa-shopify-graphql-proxy": "^4.0.1",
    "@shopify/koa-shopify-webhooks": "^2.4.3",
    "@shopify/polaris": "^5.1.0",
    "@zeit/next-css": "^1.0.1",
    "apollo-boost": "^0.4.9",
    "dotenv": "^8.2.0",
    "graphql": "^15.1.0",
    "isomorphic-fetch": "^2.2.1",
    "js-cookie": "^2.2.1",
    "koa": "^2.13.0",
    "koa-logger": "^3.2.1",
    "koa-router": "^9.1.0",
    "koa-session": "^6.0.0",
    "mysql2": "^2.1.0",
    "next": "^9.4.4",
    "qrcode.react": "^1.0.0",
    "react": "^16.13.1",
    "react-apollo": "^3.1.5",
    "react-dom": "^16.13.1",
    "react-mobile-store-button": "^0.0.6",
    "sequelize": "^6.3.3",
    "store-js": "^2.0.4",
    "uuid": "^8.1.0"
  },

Server.js

require('isomorphic-fetch')

const { Account } = require('./db.js')
const { ApiVersion } = require('@shopify/koa-shopify-graphql-proxy')
const { default: createShopifyAuth } = require('@shopify/koa-shopify-auth')
const { default: graphQLProxy } = require('@shopify/koa-shopify-graphql-proxy')
const { v4: uuidv4 } = require('uuid')
const { verifyRequest } = require('@shopify/koa-shopify-auth')
const { receiveWebhook } = require('@shopify/koa-shopify-webhooks')
const dotenv = require('dotenv')
const Koa = require('koa')
const logger = require('koa-logger')
const next = require('next')
const Router = require('koa-router')
const session = require('koa-session')
const store = require('store-js')

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 } = process.env

app.prepare().then(() => {
  const server = new Koa()
  const router = new Router()

  server.use(logger())
  server.use(session({ secure: true, sameSite: 'none' }, server))
  server.keys = [SHOPIFY_API_SECRET_KEY]

  server.use(
    createShopifyAuth({
      apiKey: SHOPIFY_API_KEY,
      secret: SHOPIFY_API_SECRET_KEY,
      scopes: [
        'read_products',
        'read_inventory',
        'write_inventory',
        'read_locations',
      ],
      accessMode: 'offline',
      async afterAuth(ctx) {
        try {
          const { shop, accessToken, _expire } = ctx.session
          ctx.cookies.set('shopOrigin', shop, {
            httpOnly: false,
            secure: true,
            sameSite: 'none',
          })
          const secretCode = uuidv4()
          ctx.cookies.set('secretCode', secretCode, {
            httpOnly: false,
            secure: true,
            sameSite: 'none',
          })
          const account = await Account.upsert({
            shopifyToken: accessToken,
            shopifyShop: shop,
            shopifyTokenExpiresAt: _expire,
            cycleToken: secretCode,
          })
          store.set('secretCode', secretCode)
        } catch (err) {
          console.log(err)
        }

        ctx.redirect('/')
      },
    })
  )

  const webhook = receiveWebhook({ secret: SHOPIFY_API_SECRET_KEY })

  router.post('/webhooks/customers/redact', webhook, (ctx) => {
    console.log('received webhook: ', ctx.state.webhook)
  })
  router.post('/webhooks/customers/data_request', webhook, (ctx) => {
    console.log('received webhook: ', ctx.state.webhook)
  })
  router.post('/webhooks/shop/redact', webhook, (ctx) => {
    console.log('received webhook: ', ctx.state.webhook)
  })

  server.use(graphQLProxy({ version: ApiVersion.July20 }))

  router.get('/(.*)', verifyRequest(), async (ctx) => {
    await handle(ctx.req, ctx.res)
    ctx.respond = false
    ctx.res.statusCode = 200
  })

  server.use(router.allowedMethods())
  server.use(router.routes())

  server.listen(port, () => {
    console.log(`> Ready on http://localhost:${port}`)
  })
})

[Koa-shopify-auth] missing "/" in redirect url when passing prefix

Overview

If passing prefix /shopify

app.use(
  shopifyAuth({
    prefix: '/shopify',
    apiKey: SHOPIFY_API_KEY,
    secret: SHOPIFY_SECRET,
    scopes: ['write_orders, write_products'],
    accessMode: 'offline',
    afterAuth(ctx) {
      // do something...
    },
  }),
);

Expected redirect URL should be /shopify/auth/enable_cookies?shop=test.myshopify.com, but the result is /shopifyauth/enable_cookies?shop=test.myshopify.com

Should add / between prefix and auth of URL

doesNotHaveStorageAccessUrl: "${prefix}/auth/enable_cookies?shop=${encodeURIComponent(
  shop,
)}",

https://github.com/Shopify/quilt/blob/master/packages/koa-shopify-auth/src/auth/client/request-storage-access.ts#L10

How to run this in a local dev environment with docker-compose and nginx reverse proxy

Hi,

not really a general issue with the package, more a specific issue that arises in my local dockerized setup. I couldn't find any ressources on this, so I thought I might open up this thread to discuss this.

My app works perfectly in my production environment (docker / Kubernetes in Gcloud). However, because of the Shopify auth process, I didn't manage to run my app locally and being able to connect it to a shopify dev store for testing changes before committing them.

My local setup is as follows:

  • On my local machine I'm using docker-compose to run the app.
  • Also I'm running my backend locally with docker-compose with which the app is supposed to communicate.
  • Therefor I'm also running an nginx reverse proxy locally (also as docker-container, gets started via docker-compose in my backend).
  • To make my local app public to the outer world I use ngrok.
  • I entered the ngrok-urls in my app setting on my shopify app in my partners account

All of this works fine, however, I cannot install my local app inside a shopify dev-store. So far I identified the following problems:

  1. Traffic coming through my ngrok tunnel carries the ngrok-address as host-header and therefor my proxy does not properly distribute the traffic. I was able to solve this by chaning the host-header of the incoming traffic via ngrok.conf:
host_header: shopify-app.local
  1. The same problem is arising vice versa: The host-header of the response is set to shopify-app.local. I was also able to change this to the ngrok address by using a location-directive on the proxy for my shopify-app.local response traffic
add_header  Host  [NGROK_ADDRESS] always;
  1. Now the next (and hopefully final) issue arises from this package. In src/auth/oauth-query-string.ts the redirect_uri is built from the ctx.host property. The hostname of course is shopify-app.local (since I changed it in 1. via ngrok.conf) and not the ngrok-hostname which means, the redirect-uri is https://shopify-app.local/auth/callback.

Now it gets weird...
I get different errors, each time I try to install the app.

Sometimes I end up on https://shopify-app.local/ with the following App-Bridge error:

AppBridgeError: APP::ERROR::INVALID_CONFIG: shopOrigin must be provided

Which probably means (assuming from this thread) the shopOrigin is not available. The shopOrigin gets extracted from the session and gets set as cookie. After redirecting to the local redirect_uri both are not available in that namespace which I assume causes the error message.

Another time I get (correctly?) redirected to [NGROK-ADDRESS]/auth?shop=my-local-store.myshopify.com (store-address changed) and receive the following error:

Expected a valid shop query parameter

which I don't understand, since it's obviously there.

Also another time I end up on the shop backend page which lists all the installed apps and there I get an error:

The app couldnโ€™t be loaded

This app canโ€™t load due to an issue with browser cookies. Try enabling cookies in your browser, switching to another browser
, or contacting the developer to get support.

(I'm using chrome and cookies are enabled)

Also another time I end up on this page https://shopify-app.local/auth/callback?code=[CODE_REMOVED]&hmac=[HMAC_REMOVED]&shop=my-local-store.myshopify.com&state=161278933272500&timestamp=1612789333 with the error

Request origin could not be verified

Since I get various different errors, it's very hard for me to analyze the problems.

My auth-middleware looks like this:

server.use(
    createShopifyAuth({
      apiKey : SHOPIFY_API_KEY,
      secret : SHOPIFY_API_SECRET_KEY,
      scopes : appScopes,
      async afterAuth(ctx) {
        const {shop, accessToken} = ctx.session
        ctx.cookies.set('shopOrigin', shop, {
          httpOnly : false,
          secure   : process.env.NODE_ENV === 'production',
          sameSite : 'none'
        })

        //webhook registrations removed to improve readability

        await insertScriptTag({shop, accessToken})

        // ctx.redirect('/')
        ctx.redirect(process.env.APP_URL) //this is the ngrok-address
      },
    }),
  )

I would be more than happy if someone has any advice for me on how to get my local app to communicate with shopify properly or what else I can do to track down the issues.

Is it possible to release a new version ?

Hello Here and thanks for your work,

Would it be possible to release a new version from the master branch (to add these commits in the distrib:
v3.1.72...master )

Particularly, I think that the addition of the missing user data to KOA auth is a must have

Thanks a lot,
Greg

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:

Screen Shot 2021-03-04 at 14 28 20

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}`);
  });
});

403 Forbidden

I'm using this library koa-shopify-aut to get our OAuth2 on shopify,
but we loose permission access after ( 403 Forbidden ) after 1-2 days.

No idea why this is happening, if it is a bug and we are getting a temporary token instead of the permanent one, or we are doing something wrong.

I still able to get Shop info, but I loose access to requested permission such read_products

Importing @shopify/koa-shopfy-auth crashes the server with no error message

Issue summary

Importing

import createShopifyAuth, { verifyRequest } from '@shopify/koa-shopify-auth';

throws this in tsc, and the server crashes

node_modules/@shopify/koa-shopify-auth/dist/src/verify-request/types.d.ts(1,28): error TS2307: Cannot find module 'src/types' or its corresponding type declarations.

image

Expected behavior

Shouldn't crash :D

Actual behavior

Crahses

Simple Fix:

import createShopifyAuth from '@shopify/koa-shopify-auth';

const { verifyRequest } = createShopifyAuth; 

import ShopifyMaster, { ApiVersion } from '@shopify/shopify-api';

const Shopify = ShopifyMaster.default;

Steps to reproduce the problem

  1. git clone https://github.com/Twiggeh/koa-shopify-auth-report.git
  2. cd koa-shopify-auth-report/server && yarn install && yarn debug
    (Seperate Terminal)
  3. cd koa-shopify-auth-report/server && node ./dist/app.js

Checklist

  • Please delete the labels section before submitting your issue
  • I have described this issue in a way that is actionable (if possible)

Test bug report

Issue summary

Checking to see if it triggers against Slack channel - to be deleted immediately.

Checklist

  • Please delete the labels section before submitting your issue
  • I have described this issue in a way that is actionable (if possible)

[koa-shopify-auth] - Cookies and redirect not working on serverless functions (like firebase)

Overview

I'm trying to host the Koa app on Firebase Cloud Functions and I'm having two problems.

Cookie issue

Firebase functions only allows one cookie, named __session. Every other cookie gets stripped out. (reference)
koa-shopify-auth package relies upon cookies by settings them with the Koa api like ctx.cookies.set(TEST_COOKIE_NAME, '1'); etc. So this doesn't work because the cookie name is not __session.

Suggested Solution: Instead of setting/getting cookies directly from ctx.cookies, we can first try to use ctx.session. If there is no session, then fallback to setting cookies directly.

In my case, I've set up the koa-session middleware to use the cookie key __session, so the above solution will work just fine with something like: ctx.session[TOP_LEVEL_OAUTH_COOKIE_NAME] = '1';

Redirect issue

My cloud function's endpoint is https://us-central1-my-project-id.cloudfunctions.net/mykoaapp.
I've set up the URL rewrites so the above function that is my koa app, is available at (and also whitelisted in the Shopify partner dashboard):
https://my-project-id.firebaseapp.com/api
Assuming the prefix is working (currently it is broken Shopify/quilt#1148), the redirect is set to ${prefix}/auth?shop=${shop}

The problem is that the redirect is using a relative path from the root domain. In my case, I need the redirect set to https://my-project-id.firebaseapp.com/api/${prefix}/auth?shop=${shop}, otherwise, as per the current behavior, the user gets to the 404-page in my case, because the redirected location gets set to https://my-project-id.firebaseapp.com/${prefix}/auth?shop=${shop},

Suggested Solution:
OAuthStartOptions should take an additional option something like APP_ROOT_URL.
And whenever we redirect the user to any URL, we should prepend the APP_ROOT_URL if it exists.
Something like:

 `${APP_ROOT_URL || ''}${prefix}/auth?shop=${shop}`, 

...

Type

  • New feature
  • Changes to existing features

Motivation

What inspired this feature request? What problems were you facing,
or what else makes you think this should be in quilt?

I encountered these problems after following this tutorial. The tutorial part was fine, but when I moved to the deployment part, it very just painful as I wasn't aware of these serverless limitations earlier.

While I know that these are not the issues with this package itself and can be avoided if I deploy the app on a platform like Heroku.
But supporting serverless environments by adding a bit more flexibility to the package is worthwhile I believe because more and more apps are built using serverless functions these days.

...

Scope

  • Is this issue related to a specific package?

    • Tag it with the Package: koa-shopify-auth label.

Checklist

  • Please delete the labels section before submitting your issue
  • I have described this issue in a way that is actionable (if possible)

How to getSessionToken and use it in fetch API directly?

Issue summary

I am trying to get a session token using import { getSessionToken } from @shopify/app-bridge-utils so that I can get token and add it to the Authorization header in my fetch calls. I am getting this error

AppBridgeError: APP::ERROR::INVALID_CONFIG: shopOrigin must be provided
. This happened while I tried to upgrade to the latest version of the library which handles. I went through the upgrade guide and just wasn't sure about the last part that says to replace all fetch calls in frontend to authenticatedFetch. In my case, I am not using GraphQL hence all of my calls are wired up using native fetch calls that's speak to koa routes. Based on this source I am trying to get tokens and failing to get one. All of my fetch calls are redirecting to /auth I guess because of the missing Authorization token and verify request middleware fails.

My app works well on every install as expected is functions as an embedded app on initial load but all of the fetch calls on user interaction don't work because of the above issue.

_app.js

import React from 'react';
import App from 'next/app';
import Head from 'next/head';
import { AppProvider } from '@shopify/polaris';
import '@shopify/polaris/dist/styles.css';
import translations from '@shopify/polaris/locales/en.json';
import { ThemeProvider } from 'styled-components'
import { Provider } from '@shopify/app-bridge-react';

const theme = {};
class MyApp extends App {
    render() {
        const { Component, pageProps, shopOrigin } = this.props;
        const config = { apiKey: API_KEY, shopOrigin, forceRedirect: true };
        console.log(config);

        return (
            <React.Fragment>
                <Head>
                    <title>Custom App for Shopify</title>
                    <meta charSet="utf-8" />
                </Head>
                <Provider config={config}>
                    <AppProvider i18n={translations} features={{ newDesignLanguage: true }}>
                        <ThemeProvider theme={theme}>
                            <Component {...pageProps} />
                        </ThemeProvider>
                    </AppProvider>
                </Provider>
            </React.Fragment>
        );
    }
}

MyApp.getInitialProps = async ({ ctx }) => {
    return {
        shopOrigin: ctx.query.shop,
    }
}

export default MyApp;

ComponentNeedingToken.js

import React, { useEffect } from 'react'
import createApp from "@shopify/app-bridge";
import { getSessionToken } from "@shopify/app-bridge-utils";

export default function MyComponent() {

    useEffect(async () => {
        const app = createApp({
            apiKey: "abc",
        });
        const sessionToken = await getSessionToken(app);
        console.log(sessionToken);
    }, []);

    return (
        <p>Hey</p>
    )
}

Console print for app config to ensure shopOrigin is set by the server. This is printed both serverside and client-side as I am using Next.js

{
  apiKey: 'abc',
  shopOrigin: 'mydomain.myshopify.com',
  forceRedirect: true
}

server.js

import "@babel/polyfill";
require('isomorphic-fetch');

import Koa from 'koa';
import Router from '@koa/router';
import bodyParser from 'koa-bodyparser';
import next from 'next';
import shopifyAuth, { verifyRequest } from '@shopify/koa-shopify-auth';
import Shopify, { ApiVersion } from '@shopify/shopify-api';
import dotenv from 'dotenv';
import RedisStore from './util/redis-store';

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 } = process.env;

const sessionStorage = new RedisStore();

Shopify.Context.initialize({
    API_KEY: SHOPIFY_API_KEY,
    API_SECRET_KEY: SHOPIFY_API_SECRET_KEY,
    SCOPES: ['read_products', 'write_products', 'read_orders'],
    HOST_NAME: HOST,
    API_VERSION: ApiVersion.January21,
    IS_EMBEDDED_APP: true,
    // More information at https://github.com/Shopify/shopify-node-api/blob/main/docs/issues.md#notes-on-session-handling
    SESSION_STORAGE: new Shopify.Session.CustomSessionStorage(
        sessionStorage.storeCallback,
        sessionStorage.loadCallback,
        sessionStorage.deleteCallback,
    ),
});

app.prepare().then(() => {

    const server = new Koa();
    const router = new Router();

    // Disable bodyparser for webhook routes as process handler requires http request
    server.use(async (ctx, next) => {
        if (ctx.path.includes('/webhooks/')) ctx.disableBodyParser = true;
        await next();
    });

    server.use(bodyParser());
    server.keys = [Shopify.Context.API_SECRET_KEY];

    // 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 = {};

    server.use(
        shopifyAuth({
            accessMode: 'online',
            async afterAuth(ctx) {
                const { shop, scope } = ctx.state.shopify;
                ACTIVE_SHOPIFY_SHOPS[shop] = scope;
                // TODO: Handle uninstalls here
                ctx.redirect(`/?shop=${shop}`);
            }
        }));

    const handleRequest = async ctx => {
        await handle(ctx.req, ctx.res);
        ctx.respond = false;
        ctx.res.statusCode = 200;
    };

    router.post("/api/import/all", verifyRequest(), async ctx => {
        console.log("This never fires because requests cannot be verified");
    });

    router.get("(/_next/static/.*)", handleRequest);

    router.get("/_next/webpack-hmr", handleRequest);

    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("(.*)", verifyRequest(), handleRequest);

    server.use(router.allowedMethods());
    server.use(router.routes());

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

Cookieless session support

The feature of the session tokens replacing cookies is active for several months now

Community recommendations (KisukaKiza response) suggests this

For the server-side. You will need to remove the Koa Auth package and write your own oauth method. You can simply take the koa auth package and strip out the parts that do a bunch of cookie things before it's oauth process begins.

Is there a plan to add support to this package for session tokens?

Type

  • New feature
  • Changes to existing features

Motivation

Before starting implementation on my code, I would like to verify that it is indeed the official recommendation to use when applying this feature.

Area

  • Add any relevant Area: <area> labels to this issue

Checklist

  • Please delete the labels section before submitting your issue
  • I have described this feature request in a way that is actionable (if possible)

Periodic re-authentication breaks Admin Link

Issue summary

Using the guide from https://shopify.dev/tutorials/build-a-shopify-app-with-node-and-react , I setup a Custom app for my store. It works well in general, and was straightforward to get running. I have found an issue that I am hopeful someone can help me with.

I added an Admin Link to my app, which points to https://{my-app-uri}/doSomething . Most times, clicking the link works as expected and I am brought directly to the desired section of my app. However, periodically it seems, I have to 're-auth' the app (after quitting the browser, after a full relaunch of my server, sometimes after 24 hours). What happens then is that when I click the Admin Link, it starts to load https://{my-app-uri}/doSomething , but then the re-auth happens, and I am redirected to the index screen of my app instead of where I was hoping to go.

Unfortunately, I don't see any trace of what the original URI was when the re-auth occurs. I was hoping somewhere in the Koa context I could see a full 'referer' field or similar showing the original Admin Link so I could redirect there instead of to '/', but that does not seem to be sent through. The challenge is that the Admin Link includes some query parameters that are sent by Shopify, and since I am losing those on the re-auth I can't just redirect to where the Admin Link previously was loading.

Is there a way to save the original URI Admin Link with query params so I can redirect to it after re-auth?

Expected behavior

Clicking an Admin Link should load through to the desired section of my app, including the original query params.

Actual behavior

Clicking an Admin Link when a re-auth is required forwards to my app's home screen instead of the desired section with none of the original query params.

Steps to reproduce the problem

  1. Setup an Admin Link (from the Partners dashboard, open your app, then go to Extensions; mine are on the Draft Order Details and Order Details pages)
  2. Remove all cookies for your app's domain
  3. Use the Admin Link in your store
  4. You will be redirected to the index page of the app instead of the desired section

Re-auth app access if scopes changed

Overview

I'm using this package for my app, but as part of authentication it does not re-auth permissions if any app scope updated. Please provide this feature as it is very important.

Type

  • New feature

Enable cookies redirection loop - koa-shopify-auth

Overview

I've been experiencing a number of issues with the authentication process for my app since updating the shopify koa auth package to 3.1.36.

One of these issues is the following โ โ€”

On Safari, when I open a fresh browser window with no cookies / cache at all, sign into my Shopify store, and open up my app, I am sent to a redirection loop.

Here's what it looks like:

Enable cookies page (https://<app_domain>/auth/enable_cookies) -> click "Enable Cookies" -> user gets redirected to https://<app_domain>/admin/apps/<some_hash> -> user gets redirected to https://<app_domain>/auth/enable_cookies again

Consuming repo

What repo were you working in when this issue occurred?

https://github.com/Shopify/quilt/tree/master/packages/koa-shopify-auth

[koa-shopify-auth] `verifyToken` doesn't handle expired tokens well

Overview

In our embedded app (Next.js based on the tutorial but serverless), we do server-side rendering of Graphql queries. Similar to the tutorial, we have an API route /api/graphql that uses @shopify/koa-shopify-graphql-proxy. When that route is hit, verifyRequest from @shopify/koa-shopify-auth kicks in, and if the token is expired, it redirects to the auth endpoint.

So far so good, except that it tries to read the shop parameter from url's query parameters. I checked and it does sometimes exist on the request's referer header to my auth endpoint but not in the actual request url. Thus the auth request fails with Expected a valid shop query parameter. This is very inconvenient because then the GraphQL proxy route throws an "Unexpected token <" error during SSR (because the response is HTML not JSON).

This is the relevant section in your code: https://github.com/Shopify/quilt/blob/dd44131ffd037ca350958f39569dee89957359a4/packages/koa-shopify-auth/src/verify-request/utilities.ts#L9-L16

Two solutions I can think of:

  • Read the shop name from the session cookie
  • Check the referer header

Maybe it could even just try both if there's no shop in the query. What do you think? I can open a PR for this. Suggested solution:

const shop =
  ctx.query.shop ||
  ctx.session?.shop ||
  new URLSearchParams(ctx.req.headers.referer).get('shop');

const routeForRedirect = shop ? `${authRoute}?shop=${shop}` : fallbackRoute;

ctx.redirect(routeForRedirect);

URLSearchParams is available as a global in Node since v10.0 but can be imported from 'url' since v7.5 or v6.13 (backported).

Multi page server side rendered NextJS app fails to open other pages

Issue summary

I've managed my existing app to migrate to use sessions instead of cookies and initial index page loads fine. When I click on another menu items within embedded app, it re-initiates auth process and kicks ma back on index page.

image

Main Packages:

"@shopify/koa-shopify-auth": "^4.0.3"
"@shopify/app-bridge-utils": "^1.29.0"
"@koa/router": "^8.0.8"
"next": "^10.0.9"
"shopify-api-node": "^3.6.6"

Expected behavior

Should be able to visit other pages within app.

Actual behavior

When I click on other navigation items, does re-auth and kick me back to index page when I click on any navigation item

Code

server.js
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";
import { MongoClient } from "mongodb";

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,
    SESSION_STORAGE: new Shopify.Session.CustomSessionStorage(
        // not including implementation to save some space
        storeCallback,
        loadCallback,
        deleteCallback
    )
});

// 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 = {};

MongoClient.connect(MONGODB_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
}).then(async connection => {
    console.info(`Successfully connected to Mongo`);
    const mongodb = connection;

    app.prepare().then(async () => {
        const server = new Koa();
        const router = new Router();
        server.keys = [Shopify.Context.API_SECRET_KEY];
        server.use(
            createShopifyAuth({
                apiKey: SHOPIFY_API_KEY,
                secret: SHOPIFY_API_SECRET,
                scopes: [SCOPES],
                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;

                    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);
            }
        });

        server.use(require("./api"));

        router.get("(/_next/static/.*)", handleRequest); // Static content is clear
        router.get("/_next/webpack-hmr", handleRequest); // Webpack content is clear
        router.get("(.*)", verifyRequest({ accessMode: "offline" }), handleRequest); // Everything else must have sessions

        server.use(router.allowedMethods());
        server.use(router.routes());
        server.listen(port, () => {
            console.log(`> Ready on http://localhost:${port}`);
        });
    });
});
_app.js
import ApolloClient from "apollo-boost";
import { ApolloProvider } from "react-apollo";
import App from "next/app";
import { AppProvider } from "@shopify/polaris";
import { Provider, useAppBridge } from "@shopify/app-bridge-react";
import { authenticatedFetch } from "@shopify/app-bridge-utils";
import "@shopify/polaris/dist/styles.css";
import translations from "@shopify/polaris/locales/en.json";

function MyProvider(props) {
  const app = useAppBridge();

  const client = new ApolloClient({
    fetch: authenticatedFetch(app),
    fetchOptions: {
      credentials: "include",
    },
  });

  const Component = props.Component;

  return (
    <ApolloProvider client={client}>
      <Component {...props} />
    </ApolloProvider>
  );
}

class MyApp extends App {

  render() {
    const { Component, pageProps, shopOrigin } = this.props;
    return (
      <AppProvider i18n={translations}>
        <Provider
          config={{
            apiKey: API_KEY,
            shopOrigin: shopOrigin,
            forceRedirect: true,
          }}
        >
          <MyProvider Component={Component} {...pageProps} />
        </Provider>
      </AppProvider>
    );
  }
}

MyApp.getInitialProps = async ({ ctx }) => {
  return {
    shopOrigin: ctx.query.shop,
    API_KEY: process.env.SHOPIFY_API_KEY
  };
};

export default MyApp;

Checklist

  • Please delete the labels section before submitting your issue
  • I have described this issue in a way that is actionable (if possible)

verifyRequest() fails on a Page refresh after `redirect.dispatch`

Issue summary

Doing a page refresh after redirect.dispatch on edit-products page makes the verifyRequest() fail. It redirects to auth and returns back to home page.

Expected behavior

It should retain the auth and load the edit-products page instead of doing a new auth and returning back to index.

Actual behavior

What actually happens?

Tip: include an error message (in a <details></details> tag) if your issue is related to an error

Steps to reproduce the problem

  1. clone - https://github.com/Shopify/shopify-app-node/blob/tutorial_fetch_data_with_apollo/
  2. run yarn install, set env variables, run yarn run dev
  3. Open the app on Shopify and select a product
  4. Once you are on the edit-products path, do a page refresh.

Reduced test case

The best way to get your bug fixed is to provide a reduced test case.


Checklist

  • Please delete the labels section before submitting your issue
  • I have described this issue in a way that is actionable (if possible)

App using createShopifyAuth from @shopify/koa-shopify-auth cannot be installed when app is using prefix

prefix = "/" default in this file:

const requestStorageAccess = (shop: string, prefix = '/') => {

Is causing infinite redirect when prefix does not end with a trailing /, however in other code default prefix is "" and forces user to configure prefix without trailing /

Basically this makes it impossible to install from path like /a/b/app or alike

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

  1. The app is being redirect to the "{domain}/auth".
  2. Shopify app bridge checks current window and redirect to the "{domain}/auth/inline?shop="
  3. 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.

  1. Define callback url within twitter dev portal (e.g {domain}/oauth/callback/twitter)
  2. Install the app to the store and redirect app user to the twitter authorization page along necessary twitter request tokens.
  3. Complete authorization within twitter's page and wait for the callback url trigger.

Checklist

  • Please delete the labels section before submitting your issue
  • I have described this issue in a way that is actionable (if possible)

Redirect Loop in Safari

I'm currently facing a redirect loop when my Shopify app starts the authentication process in Safari. Due to Safari's new policy where they block 3rd party cookies you can only get around by accessing Storage Access. Koa-Auth is already addressing this and prompts the user to accept. While this solution worked for some time it is not anymore.

Is someone else also experiencing this behavior? Can we expect any bugfix for this in the near future?

Thanks,
Chris

Offline mode - Endless redirects

Issue summary

When using v4 and setting accessMode 'offline', after a successful authentication the application will not redirect properly to the application home page. Instead it redirects to the 'fallbackRoute' and starts performing endless redirects to itself.

Expected behavior

Prior to v4, accessMode 'offline' still loaded the application correctly after authentication. Should we be using online mode exclusively now and then requesting an offline token separately after a successful authentication?

Could you please add MIT License

Issue summary

The MIT License link in the Readme.md is linking to a non-existent file

Expected behavior

Should display the MIT license

Actual behavior

404 error

Steps to reproduce the problem

  1. In the Readme.md, click on the MIT License link

Reduced test case

This is the link it tries to display but does not exist
https://github.com/Shopify/koa-shopify-auth/blob/HEAD/LICENSE.md


Checklist

  • Please delete the labels section before submitting your issue
  • I have described this issue in a way that is actionable (if possible)

Create utility function to tell whether an AccessMode is online / offline

Overview

We currently check if AccessMode === 'online' in several places in the code. We could extract those checks into a shared method, in possibly a new utilities project folder, where we can gather values that are common to both shopifyAuth and verifyRequest.

We should also consider changing the AccessMode type into an enum so we don't have to rely on the actual string values in the code.

Type

  • New feature
  • Changes to existing features

Motivation

This is mostly to make maintenance easier, and to ensure that we have consistent code style / behaviour around AccessMode values.

Checklist

  • Please delete the labels section before submitting your issue
  • I have described this feature request in a way that is actionable (if possible)

@shopify/koa-shopify-auth shopOrigin cookie not being refreshed again after quitting and re-opening chrome

Overview

I am using a pretty much standard react/node sales channel app modified from the tutorial here. With the first installation of my app everything works, and the shopOrigin cookie is set using this afterAuth code block

  server.use(
    createShopifyAuth({
      apiKey: SHOPIFY_API_KEY,
      secret: SHOPIFY_API_SECRET_KEY,
      scopes: ["read_product_listings", "write_checkouts"],
      async afterAuth(ctx) {
        const { shop, accessToken } = ctx.session;
        ctx.cookies.set("shopOrigin", shop, {
          httpOnly: false,
          secure: true,
          sameSite: "none",
        });
 
        ctx.redirect("/");
      },
    })
  );

The app works ok as long as I don't quit chrome at any point after this. When I do quit out of chrome and revisit the Shopify Admin, I get a blank gray screen and the below error in a loop in the console, which ends up crashing chrome.

name: "AppBridgeError", message: "APP::ERROR::INVALID_CONFIG: shopOrigin must be provided", action: undefined, type: "APP::ERROR::INVALID_CONFIG", stack: "AppBridgeError: APP::ERROR::INVALID_CONFIG: shopOrigin must be provided"}

My app has worked fine in the past after a chrome restart and I have not made any breaking updates to my knowledge. The past behavior was that when chrome is quit and the Shopify admin is re-launched it does a force full refresh, which I assume re-sets the shopOrigin cookie for AppBridge. I am no longer seeing that force refresh functionality, which I imagine is not setting the shopOrigin cookie and causing the error. Can anyone help me pinpoint this issue?

On a related note is there an update on when the cookie-less experience is going to be released? This will have an impact on my current development and I am curious to know when it is coming.

Consuming repo

What repo were you working in when this issue occurred?
I am using the latest version of
@shopify/koa-shopify-auth

Redirect loop

Hello everyone,

Overview

I am having an issue with this package @shopify/koa-shopify-auth with my app (Next.js + Node).

On Chrome when you disable 3rd party cookies, when you try to access (install or use) an embedded Shopify app there will be an infinite redirect loop between the app and /auth/enable_cookies.

I tried to explore a bit the module and it seems that there already is some code in it present to handle that sort of things, but it does not seem to work for me. And as far as I searched around the internet, I saw some other people struggling with this as well without any concluent solution.

Steps to reproduce

  • Open Chrome
  • Disable 3rd part cookies
  • Try to use an embedded Shopify App

Thank you !

[koa-shopify-auth] add the ability to specify the host of the redirect url instead of ctx.host

Overview

The developer should be able to define the host of the redirect URL to be https://{Host}/auth/callback instead of taking the host from context (ctx).

  • When having separate backend and frontend applications and deploying to a server with a reverse proxy like on Heroku or AWS Beanstalk. The Host header will be changed because of the proxy and even if we used koa's proxy feature which uses the forwarded host instead of host header, X-Forwarded-Host is not always passed by default.

  • This is why I think we should be able to pass an optional parameter to define the redirect URL host.

Type

  • [ x] Changes to existing features

Motivation

What inspired this feature request? What problems were you facing,

  • I am facing a problem were the redirect URL is having the backend host as its host because of the reverse proxy on my server and thus having to whitelist this URI and face cookies problem or having to edit the proxy behavior to add the X-Forwarded-Host.

[koa-shopify-auth] verifyToken perhaps should use GET not POST

Overview

Was stepping through the redirect paths and noticed verify-token.ts fetch always returns 400, not 401 or 200. The error body indicates "missing parameter". Perhaps the intent was for https://${session.shop}/admin/metafields.json to be a GET (Method.Get)?

Koa-Shopify-Auth - App getting rejected by shopify due to outh redirection issue.

I am using the latest koa-shopify-auth library version(3.1.59) for my embedded Shopify app authentication, I have set all the cookies as same site none. But it is getting rejected by Shopify.

This is the issue the Shopify have mentioned.
Screenshot from 2020-04-22 13-32-21 (1)

" The app must install successfully. App not requesting access to shop immediately after clicking "Add App". Expected OAuth to be initiated during install or reinstall at https://appstoretest5.myshopify.com/admin/oauth/request_grant but was directed to https://redirect_url. ".

How to get both an offline and online token

I only want my frontend application to have an online token, but I need an offline token for handling my server tasks. How can I get both tokens? Is is safe to give offline tokens to the end user?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.