shopify-api-js
shopify-api-js copied to clipboard
Multiple instances of Shopify Context
Overview
I'm coming from my own custom implementation of OAuth and handling app access through a node (actually NextJs) backend. This library looks great! But I'd like to set up multiple Shopify Apps that use the same backend server for OAuth.
It looks like this package is meant to use the Singleton pattern, so initializing context upfront, and then accessing it everywhere else.
Is there a way I can set up multiple instances of the package so different Express routes can handle different Shopify apps?
i.e.
const myApp = new Shopify({ api_key, api_secret_key, ...etc });
const myApp2 = new Shopify({ api_key2, api_secret_key2, ...etc });
app.get('/my-app/auth/callback', async (req, res) => {
const session = await myApp.Auth.validateAuthCallback(
req,
res,
req.query as unknown as AuthQuery,
);
console.log(session.accessToken);
return res.redirect(`/?host=${req.query.host}&shop=${req.query.shop}`); // wherever you want your user to end up after OAuth completes
});
app.get('/my-app-2/auth/callback', async (req, res) => {
const session = await myApp2.Auth.validateAuthCallback(
req,
res,
req.query as unknown as AuthQuery,
);
console.log(session.accessToken);
return res.redirect(`/?host=${req.query.host}&shop=${req.query.shop}`); // wherever you want your user to end up after OAuth completes
});
@paulomarg Is there a way to achieve this in the current version? I know this was previously possible.
I need to setup multiple custom app connected to the same server.
Its pretty hacky, but I was able to do it with this package by setting
Shopify.Context.API_KEY Shopify.Context.API_SECRET_KEY and Shopify.Context.SCOPES Before calling the relevant auth functions
And for offline token, just skipping the Shopify.Auth.beginAuth and storing the token explicitly so I can find it
and call Shopify.Clients.Rest(shop,token) directly.
@ToyboxZach Are you saying that I should have middleware which initialises the context at the beginning of each request? But what happens when the server has to handle 2 request from 2 different shops at the same time?
I primarily work off of the offline tokens so I have this helper function, I had appOne working originally then added appTwo and privateApp later which is why I use the core package for that, and then hack for the other case:
async function getRestClient(type: "appOne" | "appTwo" | "privateApp"){
if (type == "appOne") {
Shopify.Context.API_KEY = APPONEAPIKEY
Shopify.Context.API_SECRET_KEY = APPONEAPISECRETKEY
const session = await Shopify.Utils.loadOfflineSession(STORE_NAME);
return new Shopify.Clients.Rest(session.shop, session.accessToken);
} else if (type == "appTwo") {
// I had previously saved this token after an explicit oauth flow with this id
const session = get_shopify_oauth_session({ id: "two_token", "session.isOnline": false })?.session;
if (!session) {
throw "invalid session";
}
return new Shopify.Clients.Rest(STORE_NAME, session.accessToken);
} else {
return new Shopify.Clients.Rest(STORE_NAME, private_app_shopify_password);
}
}
It works for me, I'm not saying what you should do, I am not a maintainer of this package, just giving some helpful advice. It would be much better if this package supported this in some way,
@ToyboxZach Thank you for the code snippet. It really helped a lot. I have just a couple of questions:
- How do you handle auth with this logic (auth for app installation, Shopify admin, etc)?
- How do you make it work for the frontend(since public keys are used at build time)?
Shopify.Context.API_KEY = APPONEAPIKEY or APPTWOAPIKEY
Shopify.Context.API_SECRET_KEY = APPONEAPISECRETKEY or APPTWOAPISECRETKEY
Shopify.Context.SCOPES = [.. list out correct scopes ]
const session = await Shopify.Utils.loadOfflineSession(STORE_NAME);
onst authRoute = await Shopify.Auth.beginAuth(
request,
response,
request.query.shop,
"/shopify/auth/callback",
isOffline
);
for storing the offline auth token for the second app instead of await Shopify.Auth.beginAuth
const cookies = new Cookies(request, response, {
keys: [API_SECRET_TWO],
secure: true,
});
const state = nonce();
const session = { id: `two_token`, shop: request.query.shop, state: state, isOnline: false };
await Shopify.Context.SESSION_STORAGE.storeSession(session);
cookies.set("shopify_app_session", "two_token", {
signed: true,
expires: new Date(Date.now() + 60000),
sameSite: "lax",
secure: true,
});
Shopify.Context.API_KEY = APPONEAPIKEY or APPTWOAPIKEY
Shopify.Context.API_SECRET_KEY = APPONEAPISECRETKEY or APPTWOAPISECRETKEY
Shopify.Context.SCOPES = [.. list out correct scopes ]
const query = {
client_id: API_KEY
scope: Shopify.Context.SCOPES.toString(),
redirect_uri: `${Shopify.Context.HOST_SCHEME}://${Shopify.Context.HOST_NAME}${"/shopify/plus/auth/callback"}`,
state,
"grant_options[]": "",
};
const queryString = querystring.stringify(query);
const authRoute = `https://${request.query.shop}/admin/oauth/authorize?${queryString}`;
return authRoute;
- You should never have any of these secrets or auth on the front end, anything I need I call into my back end with the query parameters, and validate the request with an hmac or however else I need to validate it.
@paulomarg Any updates on this?
I did it myself, you have to rewrite pretty much all of the authentication functions. I highly doubt Shopify has any intention to make this easier for developers.
Ya I also accomplished this by using some middleware. So depending on which route is called, context is initialized for the desired app.
I also needed to write custom functions for loading and deleting sessions. I accomplished this by using a mongodb database.
For example (shortened for brevity):
function initializeContext(appName, ...args) {
Shopify.Context.initialize({
API_KEY: apiKey,
API_SECRET_KEY: apiSecret,
SCOPES: scopes,
HOST_NAME,
IS_EMBEDDED_APP: true,
API_VERSION: apiVersion,
SESSION_STORAGE: {
storeSession: (session) => storeSession(session, appName), // these are my custom functions
loadSession: (id) => loadSession(id, appName),
deleteSession: (id) => deleteSession(id, appName)
}
});
}
function resolveShopifyContext(req, res, next) {
const appName = req.params.appName;
if (appName == 'app1') {
initializeContext(appName, apiKey1, secret1, version);
next();
} else if (appName == 'app2') {
initializeContext(appName, apiKey2, secret2, version);
next();
}
}
// then in my express routes....
router.get('/:appName/auth', resolveShopifyContext, async (req, res) => {
const query = req.query as unknown as AuthQuery;
const {
shop
} = query;
const authRoute = await Shopify.Auth.beginAuth(req, res as ServerResponse, shop, `/${req.params.appName}/auth/callback`, false);
return res.redirect(authRoute);
});
router.get('/:appName/auth/callback', resolveShopifyContext, async (req, res) => {
// save the session information
const session = await Shopify.Auth.validateAuthCallback(req, res as ServerResponse, req.query as unknown as AuthQuery);
res.setHeader('Content-Security-Policy', `frame-ancestors ${req.query.shop}`);
return res.redirect(`${process.env.FRONT_END_HOST}/apps/${req.params.appName}?host=${req.query.host}&shop=${req.query.shop}`);
});
It's a bit hacky, but seems to work well.
I hacked my way around it as well. Basically rewrote quite a few functions. So, it works for now.
But I'm curious what was the reasoning behind this Singleton behaviour? @paulomarg
This will now be possible with the upcoming version 6 of the API library. There's a release candidate currently available for download (v6.0.0-rc1
).
@mkevinosullivan Is v6.0.0-rc1
released?
If it is, Can you point me to the documentation for handling multiple shop context on the same server?
Its pretty hacky, but I was able to do it with this package by setting
Shopify.Context.API_KEY Shopify.Context.API_SECRET_KEY and Shopify.Context.SCOPES Before calling the relevant auth functions
And for offline token, just skipping the Shopify.Auth.beginAuth and storing the token explicitly so I can find it
and call Shopify.Clients.Rest(shop,token) directly.
have you fixed this issue?
Hi @mkevinosullivan, Was this functionality added in v6.0 release? I looked through the source code, but it wasn't obvious. If it was released, can you share or point to any documentation/implementation for having multiple Shopify contexts on the same server?
@avtans-kumar-shipsy Sorry I didn't see this comment until now ... with v6 you can create two different instances as follows:
const shopify1 = shopifyApi({config for 1});
const shopify2 = shopifyApi({config for 2});