All requests authenticating as `shopify-app-user` despite API token (begun Sept. 7th)

This is a REALLY odd situation I'm in, and it appeared out of nowhere starting September 7th. I have created an extension for the Shopify POS, which needs to call some globalActions in the Gadget backend. It uses an API key I created with the role shopify-pos-extension , which has access to the needed actions (see the attached image) . However, the POS extension is getting a GGT_PERMISSION_DENIED 403 response for all global actions it tries to invoke EXCEPT for my hasFeature action, which is permitted for shopify-app-users. I have experimented with adding and removing permissions from shopify-app-user and have verified that all requests are being given this role when invoked from the POS extension, despite the fact that I am passing in the API key in the Authorization header. Here's what makes this problem especially weird: - Everything in production was working as expected until September 7th, when suddenly all requests to prod from my client were being called as the shopify-app-users role. - Everything works correctly in the development environment, using the same role (and, of course, the dev-specific API key) - The API key/role is respected when called with CURL to hit the graphql endpoint directly:
curl -X POST https://<REDACTED_APP_NAME>.gadget.app/api/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer REDACTED_TOKEN" \
-d '{"query":"mutation { usageEvent(cartId: \"test\", shopId: 1, receiptsMade: 1) { success } }"

Result: βœ… 200 OK
curl -X POST https://<REDACTED_APP_NAME>.gadget.app/api/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer REDACTED_TOKEN" \
-d '{"query":"mutation { usageEvent(cartId: \"test\", shopId: 1, receiptsMade: 1) { success } }"

Result: βœ… 200 OK
No description
18 Replies
rayhanmemon
rayhanmemonOPβ€’7d ago
This is how I am making the call from my client. And as a reminder, this application code works perfectly fine in the development environment!
await fetch(`${backendUrl}/api/graphql`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${backendToken}`,
},
body: JSON.stringify({
query: `
mutation UsageEvent($cartId: String, $shopId: Float, $receiptsMade: Float) {
usageEvent(cartId: $cartId, shopId: $shopId, receiptsMade: $receiptsMade) {
success
errors {
message
}
result
}
}
`,
variables: {
cartId: cartId,
shopId: shopId,
receiptsMade: receiptsMade,
},
}),
});
await fetch(`${backendUrl}/api/graphql`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${backendToken}`,
},
body: JSON.stringify({
query: `
mutation UsageEvent($cartId: String, $shopId: Float, $receiptsMade: Float) {
usageEvent(cartId: $cartId, shopId: $shopId, receiptsMade: $receiptsMade) {
success
errors {
message
}
result
}
}
`,
variables: {
cartId: cartId,
shopId: shopId,
receiptsMade: receiptsMade,
},
}),
});
@[Gadget] Antoine @[Gadget] Harry , I was combing through some other issues that felt related and it seems like you folks have some good insight into these kinds of issues! Would love your support if available.
Chocci_Milk
Chocci_Milkβ€’7d ago
Hello, We are at the end of our week right now but I can give you a little information before we are off for the weekend. When working with extensions that offer session tokens, if you have used a Gadget provider and are passing it a session token, that context will automatically be tacked on to the requests and the appropriate role will be assigned (in that case the shopify-app-user role). It is not recommended (actually we urge against it) that you use api key auth in a frontend as it can be used to make malicious calls to your api if an unintended user gains access to it. A question for you, why are you not using the @gadgetinc/react and @gadgetinc/shopify-extensions packages instead of using useFetch? Also, why would you not want to use the shopify-app-users role (more curious than anything here)? Please note that it is against server rules to ping (@) Gadget employees unless its an emergency or we haven't responded within 24 hours of your message (during our 9-5 Mon-Fri EST work week).
rayhanmemon
rayhanmemonOPβ€’7d ago
Ah my apologies, I'll refrain from using @ mentions frivilously. Just been banging my head against my keyboard for a few days on this one. I'll try to migrate over to those packages and see if that resolves the issue! If that doesn't work, I'll at least have more info for you come Monday. Thanks a bunch!
rytisluko
rytislukoβ€’7d ago
I've struggled with how to access gadget api in the extensions half a year ago, but I remembered that somewhere in one random video I saw it. And then managed to make it work. I've never made POS extension, but it's an extension nonetheless. 1. Just added a screenshot of folder structure that I use. Then in the api.js you need to import your apps client package that gadget provides I randomly found it's link here now: https://docs.gadget.dev/guides/plugins/shopify/frontends in the table it provides the second one is psecific to your application. For example if your project is called 'raydev' it will be
@gadget-client/raydev
@gadget-client/raydev
that's how the api connects to your actual models and other things in your app. The api is used a little bit different in the extension code, but very similar to normal gadget app environment. You can see the library they provide here. https://docs.gadget.dev/reference/react#gadgetinc-react Here is the github gist with my code https://gist.github.com/pacifists/ae9c1ec47259b07cb6a6c06383976d4a 2. The very big struggle for ai agents was to create a correct <Provider> I think someone from the team help me to figure it out in the end. Because of how react components stack and pass along context. Even thought now it seems very simple πŸ˜„ 3. As I'm coding using cursor agent I just usually throw these samples at it and say 'this is the right way to do it' adapt and modify my code and it figures it out. Hope this helps, I remember struggled with it for a few days too πŸ™‚ Until I got the different between the app and how it works in shopify extensions.
No description
rytisluko
rytislukoβ€’7d ago
I would love for someone to do a Guide that shows just this, because when it was the first time it was a big struggle and not clear at all. (and now someone from the team will post a link with the exact guide that is already there.... :D) @rayhanmemon hope this helps and I've went the way you try it πŸ™‚ I have a full file of utils.js where I was trying to do exactly what you're doing - to use fetch over the network πŸ˜„ only when showing you the code I've realised it's still there and not linked to anything anymore πŸ™‚ So I feel your pain.
rayhanmemon
rayhanmemonOPβ€’6d ago
Thanks a bunch for the thorough help @rytisluko ! This is definitelty the right track. I ended up using both the @gadgetinc/shopify-extensions/react and the generated @gadget-client/<my-app> libraries to instantiate a gadget API client like so:
import { Client } from '@gadget-client/gift-receipts';
import type { GiftReceiptsClient } from '@gadget-client/gift-receipts';
import { DEV_SHOP_IDS, PROD_ENVIRONMENT, DEV_ENVIRONMENT } from './constants';
import { SessionToken } from '@gadgetinc/shopify-extensions/react';
import { SessionApi } from '@shopify/ui-extensions/point-of-sale';

let gadgetApiClient: GiftReceiptsClient | null = null;

export function getGadgetApiClient(shopId: number): GiftReceiptsClient {
if (!gadgetApiClient) {
gadgetApiClient = new Client({
environment: DEV_SHOP_IDS.includes(shopId)
? DEV_ENVIRONMENT
: PROD_ENVIRONMENT,
});
}
return gadgetApiClient;
}

export function getSessionToken<T extends SessionApi>(api: T): SessionToken {
return {
get: async () => {
try {
const token = await api.session.getSessionToken();
if (!token) {
throw new Error('Session token not found');
}
return token;
} catch (error) {
console.error('Error getting session token:', error);
throw error;
}
},
};
}
import { Client } from '@gadget-client/gift-receipts';
import type { GiftReceiptsClient } from '@gadget-client/gift-receipts';
import { DEV_SHOP_IDS, PROD_ENVIRONMENT, DEV_ENVIRONMENT } from './constants';
import { SessionToken } from '@gadgetinc/shopify-extensions/react';
import { SessionApi } from '@shopify/ui-extensions/point-of-sale';

let gadgetApiClient: GiftReceiptsClient | null = null;

export function getGadgetApiClient(shopId: number): GiftReceiptsClient {
if (!gadgetApiClient) {
gadgetApiClient = new Client({
environment: DEV_SHOP_IDS.includes(shopId)
? DEV_ENVIRONMENT
: PROD_ENVIRONMENT,
});
}
return gadgetApiClient;
}

export function getSessionToken<T extends SessionApi>(api: T): SessionToken {
return {
get: async () => {
try {
const token = await api.session.getSessionToken();
if (!token) {
throw new Error('Session token not found');
}
return token;
} catch (error) {
console.error('Error getting session token:', error);
throw error;
}
},
};
}
And provide this client to my POS UI extension components like so:
...
import { getGadgetApiClient, getSessionToken } from '../../api';

const OrderDetailsModal = () => {
const api = useApi<'pos.order-details.action.render'>();
const gadgetApiClient = getGadgetApiClient(api.session.currentSession.shopId);
const sessionToken = getSessionToken(api);

return (
<Provider api={gadgetApiClient} sessionToken={sessionToken}>
<MyComponent />
</Provider>
);
};

export default reactExtension('pos.order-details.action.render', () => {
return <OrderDetailsModal />;
});
...
import { getGadgetApiClient, getSessionToken } from '../../api';

const OrderDetailsModal = () => {
const api = useApi<'pos.order-details.action.render'>();
const gadgetApiClient = getGadgetApiClient(api.session.currentSession.shopId);
const sessionToken = getSessionToken(api);

return (
<Provider api={gadgetApiClient} sessionToken={sessionToken}>
<MyComponent />
</Provider>
);
};

export default reactExtension('pos.order-details.action.render', () => {
return <OrderDetailsModal />;
});
I am still facing one issue, however, similar to the one outlined in this thread: https://discord.com/channels/836317518595096598/1358871712619434014/1358876881440604452. I have two environments, production and development. When I instantiate my gadgetApiClient with environment: 'production', like so:
export function getGadgetApiClient(shopId: number): GiftReceiptsClient {
if (!gadgetApiClient) {
gadgetApiClient = new Client({
environment: 'production',
});
}
return gadgetApiClient;
}
export function getGadgetApiClient(shopId: number): GiftReceiptsClient {
if (!gadgetApiClient) {
gadgetApiClient = new Client({
environment: 'production',
});
}
return gadgetApiClient;
}
It works perfectly. But when I use environment: 'development', I get the error:
GGT_INVALID_SHOPIFY_SESSION_TOKEN: Invalid Shopify Session Token passed in Authorization header, token could not be decoded
InvalidShopifySessionTokenError: GGT_INVALID_SHOPIFY_SESSION_TOKEN: Invalid Shopify Session Token passed in Authorization header, token could not be decoded
at validateShopifySessionToken (/app/packages/api/src/services/app-auth/ShopifySessionTokenStrategy.js:271:24)
at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
at async ShopifySessionTokenStrategy.authenticate (/app/packages/api/src/services/app-auth/ShopifySessionTokenStrategy.js:380:34)
GGT_INVALID_SHOPIFY_SESSION_TOKEN: Invalid Shopify Session Token passed in Authorization header, token could not be decoded
InvalidShopifySessionTokenError: GGT_INVALID_SHOPIFY_SESSION_TOKEN: Invalid Shopify Session Token passed in Authorization header, token could not be decoded
at validateShopifySessionToken (/app/packages/api/src/services/app-auth/ShopifySessionTokenStrategy.js:271:24)
at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
at async ShopifySessionTokenStrategy.authenticate (/app/packages/api/src/services/app-auth/ShopifySessionTokenStrategy.js:380:34)
The JWT claims seem correct when decoded in jwt.io:
{
"iss": "https://quickgift-receipts-dev.myshopify.com/admin",
"dest": "https://quickgift-receipts-dev.myshopify.com",
"aud": "203b9e77bc13fa4fbb2b9468eb839278",
"sub": "87418142912",
"exp": 1759002822,
"nbf": 1759002762,
"iat": 1759002762,
"jti": "d300b543-428b-4d72-9dd0-7dfa2dae9edf",
"sid": "e0b383a9-6b00-43d8-adc4-618bacaf8254",
"sig": "e0beec777e9351677f4ced307b3506f40df801acf167589a55bb4a7fcb4693ac"
}
{
"iss": "https://quickgift-receipts-dev.myshopify.com/admin",
"dest": "https://quickgift-receipts-dev.myshopify.com",
"aud": "203b9e77bc13fa4fbb2b9468eb839278",
"sub": "87418142912",
"exp": 1759002822,
"nbf": 1759002762,
"iat": 1759002762,
"jti": "d300b543-428b-4d72-9dd0-7dfa2dae9edf",
"sid": "e0b383a9-6b00-43d8-adc4-618bacaf8254",
"sig": "e0beec777e9351677f4ced307b3506f40df801acf167589a55bb4a7fcb4693ac"
}
So I wonder what could be fishy about dev vs. prod here...
rytisluko
rytislukoβ€’6d ago
Yeah that's out of my depth πŸ™‚ happy it helped you move forward hope you find your solution
Chocci_Milk
Chocci_Milkβ€’4d ago
Do you mind making sure that the environment variable that you set on the client is correctly targeting your environment? If the session token sent doesn't match any shops that installed your app, the session token will be marked as invalid So there are a number of things that can be the issue here. Could you please share the name of the application so that I can take a closer look?
rayhanmemon
rayhanmemonOPβ€’4d ago
Sure thing! The app's name in Gadget is gift-receipts Thanks for taking a look. I'm almost positive that the environment variable set in the POS client code is correctβ€”I tried hard-coding it to 'development', plus the GGT_INVALID_SHOPIFY_SESSION_TOKEN I pasted above was from the development environment itself
Chocci_Milk
Chocci_Milkβ€’4d ago
Might you also have an example traceId that I could take a look at? It would be on the request as x-trace-id or in the backend logs What file is the relevant code in? I'm having a hard time finding the origin of the requests
rayhanmemon
rayhanmemonOPβ€’4d ago
Sure thing. Here are some trace IDs you might find interesting: c58a42bc467174d07bc657f06917f2a9 c58a42bc467174d07bc657f06917f2a9 56d69c85ae83de57a21ed6b464eae8d0 56d69c85ae83de57a21ed6b464eae8d0 And here's some relevant code in the client: extensions/gift-receipts-pos-extension/src/api.ts is where we define a couple of helpers for creating the gadget client and session token. extensions/gift-receipts-pos-extension/src/targets/orderDetails/Modal.tsx is an example of how an extension target uses the @gadgetinc/shopify-extensions/react Provider. extensions/gift-receipts-pos-extension/src/PostPurchaseModal.tsx is an example of how the provided client is used to make requests to the Gadget backend.
Chocci_Milk
Chocci_Milkβ€’3d ago
What store are you testing this on? Thought that the logs would have that information but its not included You said that you changed your code to use the Gadget provider but I'm seeing fetch requests. Have you possibly changed branches? Have you checked what session.shopId is returning? You shared a JSON blob that has shopId: 1 I think that the fact that you aren't using our provider is the biggest blocker for you right now The thing for me right now is that I can't really determine which is the issue to tackle cause the message returned from an invalid auth header is the same
rayhanmemon
rayhanmemonOPβ€’3d ago
I am using it on the store: QuickGift Receipts DEV (https://quickgift-receipts-dev.myshopify.com). The app is installed on that store (see image below)
No description
rayhanmemon
rayhanmemonOPβ€’3d ago
Also, would you be able to take another look at my development environment? I took a look at the environment once again and confirmed that my extension code is up to date and that fetch is not being used anywhere. Here's a snippet from the PostPurchaseModal, the function is called fetchOrderItems, but internally it's using the gadget api client

const fetchOrderItems = async () => {
setLoading(true);
setFetchError(null);
setItems([]);
try {
// Fetch items from this order
const result = await gadgetApi.getOrderLineItems({ shopId, orderId });

if (result?.orderItems) {
// Normalize the items to SingleItem
const items: SingleItem[] = result.orderItems.flatMap((item: any) =>
expandOrderLineItem(item)
);
setItems(items);
setFetchError(null);
} else {
setFetchError('Failed to load order items');
setItems([]);
}
} catch (error: any) {
const message = error?.message || 'An unexpected error occurred';
setFetchError(message);
api.toast.show(`Error fetching order items: ${message}`);
} finally {
setLoading(false);
}
};

const fetchOrderItems = async () => {
setLoading(true);
setFetchError(null);
setItems([]);
try {
// Fetch items from this order
const result = await gadgetApi.getOrderLineItems({ shopId, orderId });

if (result?.orderItems) {
// Normalize the items to SingleItem
const items: SingleItem[] = result.orderItems.flatMap((item: any) =>
expandOrderLineItem(item)
);
setItems(items);
setFetchError(null);
} else {
setFetchError('Failed to load order items');
setItems([]);
}
} catch (error: any) {
const message = error?.message || 'An unexpected error occurred';
setFetchError(message);
api.toast.show(`Error fetching order items: ${message}`);
} finally {
setLoading(false);
}
};
Chocci_Milk
Chocci_Milkβ€’3d ago
Might I ask where your Gadget provider is in the extension? I don't see it Its crucial that you add the provider for requests to be set with the correct headers Take a look at this template for example: https://github.com/gadget-inc/templates/tree/devaoc/wishlist-dx/shopify/wishlist-public-remix-ssr Specifically this file: https://github.com/gadget-inc/templates/blob/devaoc/wishlist-dx/shopify/wishlist-public-remix-ssr/extensions/wishlist-account-ui/src/Wishlist.tsx Each of your modals/targets need to have the provider since there's not one centralized spot where the application gets rendered
rayhanmemon
rayhanmemonOPβ€’3d ago
Yep, I believe it has been correctly added to all extension targets in the app: extensions/gift-receipts-pos-extension/src/targets/orderDetails/Modal.tsx extensions/gift-receipts-pos-extension/src/targets/postPurchase/Modal.tsx extensions/gift-receipts-pos-extension/src/targets/smartGrid/Modal.tsx And as a reminder, it's working perfectly fine against my production environment! It's just the development environment where I'm experiencing this problem, which makes it impossible to test changes prior to deployment... I've tried renaming the development environment and using the new name, but no avail
Chocci_Milk
Chocci_Milkβ€’19h ago
I'm really having a hard time understanding why this is happening. Could you please share some network request data with me (DM it)
rayhanmemon
rayhanmemonOPβ€’13h ago
Sure. Right now, I test the POS extension on my phone via the Shopify POS App. Is there a straightforward way for me to get you the network request data you need while testing on my phone? Were you able to reproduce this bug on your end by the way? Just want to sanity check that this is really an issue. Because from what I can tell, it's all wired up correctly, the app is installed on the test store, etc.

Did you find this page helpful?