I’m using…
- customSessionStorage with PostgreSQL
- offline tokens, I think… I removed the default things that specified online tokens:
in auth.js:
app.get("use-online-tokens")
in /server/index.js:
app.set("use-online-tokens", USE_ONLINE_TOKENS);
There are two issues happening right now:
- After clicking install, I’m directed to my app url outside of shopify (not embedded) and quickly get an Invalid OAuth callback error. If I refresh that page, the authorization proceeds and I’m directed to my app embedded in Shopify admin.
- …but then it keeps looping the redirect out of shopify, then back to my embedded app, then back out to auth again, then back into embedded app.
My impression is that I’ve only implemented customSessionStorage halfway… my auth middleware (/middleware/auth.js) is still basically the default generated by the Shopify CLI when creating a node app skeleton.
Here’s my auth.js:
import { Shopify } from "@shopify/shopify-api";
import topLevelAuthRedirect from "../helpers/top-level-auth-redirect.js";
export default function applyAuthMiddleware(app) {
app.get("/auth", async (req, res) => {
if (!req.signedCookies[app.get("top-level-oauth-cookie")]) {
return res.redirect(
`/auth/toplevel?${new URLSearchParams(req.query).toString()}`
);
}
const redirectUrl = await Shopify.Auth.beginAuth(
req,
res,
req.query.shop,
"/auth/callback",
app.get("use-offline-tokens")
);
res.redirect(redirectUrl);
});
app.get("/auth/toplevel", (req, res) => {
res.cookie(app.get("top-level-oauth-cookie"), "1", {
signed: true,
httpOnly: true,
sameSite: "strict",
});
res.set("Content-Type", "text/html");
res.send(
topLevelAuthRedirect({
apiKey: Shopify.Context.API_KEY,
hostName: Shopify.Context.HOST_NAME,
host: req.query.host,
query: req.query,
})
);
});
app.get("/auth/callback", async (req, res) => {
try {
const session = await Shopify.Auth.validateAuthCallback(
req,
res,
req.query
);
const host = req.query.host;
app.set(
"active-shopify-shops",
Object.assign(app.get("active-shopify-shops"), {
[session.shop]: session.scope,
})
);
const response = await Shopify.Webhooks.Registry.register({
shop: session.shop,
accessToken: session.accessToken,
topic: "APP_UNINSTALLED",
path: "/webhooks",
});
if (!response["APP_UNINSTALLED"].success) {
console.log(
`Failed to register APP_UNINSTALLED webhook: ${response.result}`
);
}
// Redirect to app with shop parameter upon auth
res.redirect(`/?shop=${session.shop}&host=${host}`);
} catch (e) {
switch (true) {
case e instanceof Shopify.Errors.InvalidOAuthError:
res.status(400);
res.send(e.message);
break;
case e instanceof Shopify.Errors.CookieNotFound:
case e instanceof Shopify.Errors.SessionNotFound:
// This is likely because the OAuth session cookie expired before the merchant approved the request
res.redirect(`/auth?shop=${req.query.shop}`);
break;
default:
res.status(500);
res.send(e.message);
break;
}
}
});
}
App.jsx is still the default generated for Shopify CLI node app.
Here’s my custom-session-storage.js:
import { Session } from '@shopify/shopify-api/dist/auth/session/index.js';
import db from '../models/index.js';
class CustomSessionStorage {
storeCallback = async (session) => {
try {
// Inside our try, we use the `setAsync` method to save our session.
// This method returns a boolean (true if successful, false if not)
const [instance] = await db.Session.upsert({
sessionId: session.id,
sessionData: JSON.stringify(session),
});
return Boolean(instance);
} catch (err) {
// throw errors, and handle them gracefully in your application
throw new Error(err);
}
};
loadCallback = async (id) => {
try {
// Inside our try, we use `getAsync` to access the method by id
// If we receive data back, we parse and return it
// If not, we return `undefined`
let instance = await db.Session.findOne({
where: {
sessionId: id,
},
});
if (instance) {
const sessionObj = JSON.parse(instance.sessionData);
// console.log('sessionObj: ', sessionObj);
return Session.cloneSession(sessionObj, sessionObj.sessionId);
// return JSON.parse(instance.sessionData);
} else {
return undefined;
}
} catch (err) {
throw new Error(err);
}
};
/*
The deleteCallback takes in the id, and uses the redis `del` method to delete it from the store
If the session can be deleted, return true
Otherwise, return false
*/
deleteCallback = async (id) => {
try {
// Inside our try, we use the `delAsync` method to delete our session.
// This method returns a boolean (true if successful, false if not)
const numDestroyed = await db.Session.destroy({
where: {
sessionId: id,
},
});
return numDestroyed > 0;
} catch (err) {
throw new Error(err);
}
};
}
// Export the class
export default CustomSessionStorage;