Closed zirkelc closed 1 year ago
You need to perform any routing on the client side. Any page that lands on /:someroute
needs make use of the same logic that loads an app skeleton and not perform any verifyRequest
calls.
Then once your app has loaded, all routing should be done on the client and load any sensitive data using calls to endpoints that do perform verifyRequest
. I use NextJS API routes for this.
// server.ts
router.all(
"(/api.*)",
verifyRequest({ accessMode: "online", returnHeader: true }),
handleRequest
);
// We perform client side authorization
// Make sure to never expose any secrets during SSR and only show a Skeleton page.
router.get("(.*)", async (ctx) => {
const { shop } = ctx.query;
const authenticatedShop = shop && (await ShopStorage.findShop(shop));
if (authenticatedShop) {
await handleRequest(ctx);
} else {
// This shop hasn't been seen yet, go through OAuth to create a session
ctx.redirect(`/auth?shop=${shop}`);
}
});
server.use(router.allowedMethods());
server.use(router.routes());
I use the AppBridge react components to perform routing on the client side:
// ClientRouter.js
import React from "react";
import { withRouter } from "next/router";
import { ClientRouter as AppBridgeClientRouter } from "@shopify/app-bridge-react";
function ClientRouter(props) {
const { router } = props;
return <AppBridgeClientRouter history={router} />;
}
export default withRouter(ClientRouter);
And I make sure that Polaris uses the ClientRouter in every case:
// LinkComponent.js
import Link from "next/link";
const LinkComponent = ({ children, url, ...props }) => {
return (
<Link href={url} passHref>
<a {...props}>{children}</a>
</Link>
);
};
export default LinkComponent;
And I use these two components in my pages/_app.js
:
// pages/_app.js
function MyProvider(props) {
const app = useAppBridge();
const client = new ApolloClient({
link: new HttpLink({
fetch: userLoggedInFetch(app),
fetchOptions: {
credentials: "include",
},
}),
cache: new InMemoryCache(),
});
const Component = props.Component;
return (
<ApolloProvider client={client}>
<Frame>
<Component {...props} />
</Frame>
</ApolloProvider>
);
}
class MyApp extends App {
render() {
const { Component, pageProps, host, shop } = this.props;
return (
<AppProvider i18n={translations} linkComponent={LinkComponent}>
<Provider
config={{
apiKey: API_KEY,
forceRedirect: true,
host,
}}
>
<ClientRouter />
<MyProvider Component={Component} host={host} {...pageProps} />
</Provider>
</AppProvider>
);
}
}
MyApp.getInitialProps = async ({ ctx }) => {
const { host, shop } = ctx.query;
return {
host,
shop,
};
};
Thanks for your comment!
I'm building a non-embedded app (without AppBridge) and use the authentication only to retrieve an offline access token. So do I need verifyRequest()
at all?
At some point you will use that offline access token in one of your requests to retrieve things from Shopify (or maybe your own local database as well?), so yes you will need verifyRequest()
.
@tolgap I'm getting a 403 error when using Nextjs /api
folder and verifyRequest. If I comment out the verifyRequest portion of the below code, everything works (feels like that's kind of the point). Have you experienced this?
router.all(
"(/api.*)",
verifyRequest({ accessMode: "online", returnHeader: true }),
handleRequest
);
@PurplePineapple123 what does your request look like? Please provide the code for the API request that fails, at least, so we could see what is going wrong.
@tolgap See below. I create an axios instance for an authenticated request getting the session token from app bridge. Then I pass that instance as a prop and use it in my component.
/pages/_app.js
function MyProvider(props) {
//console.log('this is run, axios request for current user?');
const app = useAppBridge();
const client = new ApolloClient({
fetch: userLoggedInFetch(app),
fetchOptions: {
credentials: "include",
},
});
// Create axios instance for authenticated request
const authAxios = axios.create();
// intercept all requests on this axios instance
authAxios.interceptors.request.use(function (config) {
return getSessionToken(app).then((token) => {
console.log(token); //<= This successfully logs the token
// append your request headers with an authenticated token
config.headers["Authorization"] = `Bearer ${token}`;
return config;
});
});
const Component = props.Component;
return (
<ApolloProvider client={client}>
<Component {...props} authAxios={authAxios} />
</ApolloProvider>
);
}
/pages/create Destructure authAxios prop and use in request:
const handleSubmit = async (event) => {
let res = await authAxios.get("/api/test");
console.log(res.data);
};
/api/test
export default function handler(req, res) {
console.log(req.body);
res.status(200).json({ name: 'John Doe' })
}
@zirkelc and @PurplePineapple123 please confirm have you go answer I am facing this same issue , I am creating the un-embedd app and I want my URL when the app loads to look like abc.com and not abc/hmac=sdfdsf&shop=sdfdf
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.
I'm trying to build a non-embedded custom app with offline access and struggle with the migration from cookie-based to session-based auth.
I followed the example app (https://github.com/Shopify/koa-shopify-auth#example-app) but can't make any sense of the actual auth flow and the reason for the routes:
Click on app install link (generated via Partner Dashboard) -> redirect to App on / route: GET /hmac=xxx&shop=shop.myshopify.com×tamp=1628157491
Check if app is already installed for shop param and redirect to /auth route: GET /install/auth?shop=shop.myshopify.com
Redirect to /auth/inline route GET /install/auth/inline?shop=monte-stivo.myshopify.com
Redirect to Shopify to confirm install of app https://shop.myshopify.com/admin/oauth/request_grant?client_id=xxx
Shopify calls callback route /install/auth/callback GET /install/auth/callback?code=xxx&hmac=xxx&host=xxx&shop=shop.myshopify.com&state=596690710300988×tamp=1628157499
afterAuth method will be invoked to retrieve Access Token and register Webhooks; redirect to route /?shop=shop
Route / checks if Shop has been installed and loads app skeleton
After successful auth, I would expect that any GET request to a route like /test goes through verifyRequest() and would return the "secured" data. But the request to /test gets redirected to /install/auth, then /install/auth/inline, then to Shopify and back via callback to /install/auth/callback and we are back into our afterAuth handler. From there we redirect to / and we are right at the end of the Oauth flow again.
I would like to now what is the meaning of verifyRequest at all? And what is the reason we load an app skeleton after successful auth on the / route?
Here's my code for completeness: