Shopify / koa-shopify-auth

DEPRECATED Middleware to authenticate a Koa application with Shopify
MIT License
80 stars 63 forks source link

403 Error when using multiple staff accounts, no Oauth or session created when using CustomSessionStorage #137

Closed Michael-Gibbons closed 1 year ago

Michael-Gibbons commented 2 years ago

First I'd like to apologize if I'm simply not understanding something, I've been working on this for days and have scoured all resources I could find so if anyone can help I would be very appreciative.

Issue summary

When our app is freshly deployed, Staff member 1 goes to our embedded app in the Shopify admin. Staff member 1 goes through the Oauth process, a session is created and stored in a db.

Requests work as expected and are authenticated.

Staff member 2 goes to the app in the Shopify admin using a different account and requests are forbidden as they do not have a session access token, redirecting to the /auth route manually fixes this issue as a session is created for them.

Expected behavior

Staff member 2 should be redirected to the Oauth flow to have a session created for them.

Actual behavior

Requests are 403 forbidden and no Oauth flow is initiated

loadCallback fires, sees there is no session in the db corresponding to this user, and returns undefined. Nothing happens after this point.

Reduced test case

// server.js
import {
  storeCallback,
  loadCallback,
  deleteCallback,
} from "./custom-sessions.js";

 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: "2021-10",
  IS_EMBEDDED_APP: true,
  SESSION_STORAGE: null,
  SESSION_STORAGE: new Shopify.Session.CustomSessionStorage(
    storeCallback,
    loadCallback,
    deleteCallback
  ),
});

  router.get("(.*)", async (ctx) => {
    const shop = ctx.query.shop;

    if (ACTIVE_SHOPIFY_SHOPS[shop] === undefined) {
      ctx.redirect(`/auth?shop=${shop}`);
    } else {
      await handleRequest(ctx);
    }
  });
// custom-sessions.js
import { Session } from "@shopify/shopify-api/dist/auth/session";
const AppSession = require("../models/session");

module.exports.storeCallback = async function storeCallback(session) {
  console.log("Running storeCallback");
  const payload = { ...session };
  console.log("StoreCallback session===============================");
  console.log(session);
  console.log("StoreCallback Payload===============================");
  console.log(payload);
  const foundSession = await AppSession.findOne({ where: { id: session.id } });
  if (foundSession) {
    //update
    return AppSession.update(
      { payload: payload },
      { where: { id: session.id } }
    )
      .then((_) => {
        return true;
      })
      .catch((err) => {
        return false;
      });
  } else {
    //create
    return AppSession.create({
      id: session.id,
      payload: payload,
      shop: payload.shop,
    })
      .then((_) => {
        return true;
      })
      .catch((err) => {
        return false;
      });
  }
};

module.exports.loadCallback = async function loadCallback(id) {
  console.log("loadCallback ID===============================");
  const foundSession = await AppSession.findOne({ where: { id: id } });

  if (!foundSession) {
    console.log("no found session returning undefined");
    return undefined;
  }

  try {
    const session = new Session(foundSession.id);
    const {
      shop,
      state,
      scope,
      accessToken,
      isOnline,
      expires,
      onlineAccessInfo,
    } = foundSession.payload;
    session.shop = shop;
    session.state = state;
    session.scope = scope;
    session.expires = expires ? new Date(expires) : undefined;
    session.isOnline = isOnline;
    session.accessToken = accessToken;
    session.onlineAccessInfo = onlineAccessInfo;

    console.log(
      "loadCallback New session Complete==============================="
    );
    console.log(session);
    return session;
  } catch (err) {
    console.log(err);
    return undefined;
  }
};

module.exports.deleteCallback = async function deleteCallback(id) {
  const foundSession = await AppSession.findOne({ where: { id: id } });
  if (foundSession) {
    return foundSession
      .destroy()
      .then((_) => {
        return true;
      })
      .catch((err) => {
        return false;
      });
  }
};
Michael-Gibbons commented 2 years ago

So I don't have a good solution to this issue but I do have a bad one, I am now checking for a 403 response when the app is rendered. Below is the relevant part of my _app.js file generated by the shopify-cli.

function MyProvider(props) {
  const app = useAppBridge();
  if (typeof window !== "undefined") {
    window.app = app;
    //authFetch is just a fetch wrapper that adds the bearer token header,
    // you can slot this in for any authenticated request system you have set up, 
    //authenticatedFetch from app-bridge-utils for example

   // the "state" endpoint is specific to my application but this can be any request as long as the request won't return a 403 in any other scenario than this,
// I would recommend creating a custom endpoint which always returns 200
    authFetch("/state").then((response) => {
      if (response.status == 403) {
        window.location.href = "/auth?shop=YOUR-STORE.myshopify.com";
      }
    });
  }

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

  const Component = props.Component;

  return (
    <ApolloProvider client={client}>
      <StoreProvider store={store}>
        <Component {...props} />
      </StoreProvider>
    </ApolloProvider>
  );
}
github-actions[bot] commented 1 year ago

Note that this repo is no longer maintained and this issue will not be reviewed. Prefer the official JavaScript API library. If you still want to use Koa, see simple-koa-shopify-auth for a potential community solution.