Treating every token as stolen despite clean 24-hour logs is the right call — logs that short can't prove a negative.
The part I'd steal for my own projects is the exceptions list with written reasons; without it, a daily scan just trains people to ignore the alerts it raises.
Shopify & Shopify Plus developer. I fix the problems apps don't: checkout logic, COD and payment reconciliation, ZATCA/FBR e-invoicing, inventory sync and custom dashboards.
Exactly. 24 hours of clean logs only tells you nobody used the tokens in those 24 hours. And yes, without the exceptions list the report was getting noisy fast: a few findings are intended, like a public chat widget that writes anonymous sessions, and seeing those every morning would have trained me to skim. Writing the reason down also forces the question "is this really intended?" once.
That chat widget example is a good one — an intended finding that keeps surfacing is exactly how a scan loses trust.
One thing I'd add is a review date on each exception, so an 'intended for now' decision gets revisited instead of living forever.
If the reason ever stops matching what the widget actually does, that date is what catches it.
Shopify & Shopify Plus developer. I fix the problems apps don't: checkout logic, COD and payment reconciliation, ZATCA/FBR e-invoicing, inventory sync and custom dashboards.
Ha, that's the gap in my setup right now. Every exception has a reason, but nothing makes me look at it again, so "intended for now" quietly turns into "intended forever". Your last line is the real point: the reason can be true the day I write it and wrong six months later. A review date on each entry is next on my list. Thanks, this thread has made the scanner better.
A light way to do it is a 15-minute recurring calendar block each week to re-read the exceptions list. Keep one line per exception — what was excepted and why — so anything past its review date expires instead of accumulating silently.
Shopify & Shopify Plus developer. I fix the problems apps don't: checkout logic, COD and payment reconciliation, ZATCA/FBR e-invoicing, inventory sync and custom dashboards.
Thanks, I actually added this after your first comment. Every exception now has an owner and a review-by date. If one has no date, it's flagged in the daily security report, and once the date passes it comes back as a high-priority finding until someone re-checks it. So nothing stays approved forever without anyone looking.
The calendar block is a good idea on top of that, because the report only catches what's already on the list.
The using (true) demo policy is the silent killer here. The rotation part is what most guides miss—re-running OAuth doesn't kill old tokens, so rotating the client secret is mandatory. Great reminder that AI makes building fast but doesn't do the security review.
Shopify & Shopify Plus developer. I fix the problems apps don't: checkout logic, COD and payment reconciliation, ZATCA/FBR e-invoicing, inventory sync and custom dashboards.
Thanks. The OAuth part surprised me too: you re-run it, get a fresh token, and assume the old one is dead. It isn't until you rotate the secret and revoke the old one. That's why the last step is testing with the old token on purpose and expecting a 401.
Testing with the old token to ensure a 401 is the only way to truly prove it's dead. Assuming a token expired just because you re-ran the OAuth flow is exactly how these leaks slip through the cracks.
Shopify & Shopify Plus developer. I fix the problems apps don't: checkout logic, COD and payment reconciliation, ZATCA/FBR e-invoicing, inventory sync and custom dashboards.
Exactly. The worrying part is that nothing tells you. The old token keeps working without a sound, so if you never test it, you'll never know. Now that 401 check is a fixed step on my list whenever I rotate anything.
"The old token keeps working without a sound" is the whole problem in one sentence. A successfully revoked token and a forgotten one look identical from the outside — both return 200, and nobody notices the difference until something goes wrong.
What helped on my side was adding the rotation date to the secret's metadata, so a stale credential is visible at a glance instead of living only in someone's memory. Combined with the 401 check as a fixed step, it's the closest thing to a real guarantee. Otherwise you're just trusting that the last person who rotated it was thorough.
Testing the old token to verify the 401 is the only real proof it's dead. Assuming re-running OAuth kills the old credentials is a classic, dangerous trap.
The attacker-side verification is the crucial closing step: “we deployed a fix” is not the same as proving old credentials fail and the intended path still works. The daily scanner is also better as a report than an auto-remediator; an explicit allowlist with an owner and expiry turns exceptions into reviewable risk instead of permanent prototype residue.
Shopify & Shopify Plus developer. I fix the problems apps don't: checkout logic, COD and payment reconciliation, ZATCA/FBR e-invoicing, inventory sync and custom dashboards.
Agreed on both. The expiry point is a fair hit. My allowlist has a written reason for every exception, but no owner and no expiry date, so an exception from last month looks as valid as one from today. I'm adding a review-by date to each entry, and the scan will flag any that have expired. Thanks, that's a better design than mine.
If some staff should only see a subset of the four stores, I'd add a staging test with one fake order in store A and one in B, and a staff login allowed to use only A. Check that the login can read A's order through the app API and gets no order data when it asks for B's by ID. If staff can edit orders, check that an edit to A works and that an attempted edit to B leaves its row unchanged. That covers the case where a caller passes the staff check but should still be denied access to a store.
We're a place where coders share, stay up-to-date and grow their careers.
Treating every token as stolen despite clean 24-hour logs is the right call — logs that short can't prove a negative.
The part I'd steal for my own projects is the exceptions list with written reasons; without it, a daily scan just trains people to ignore the alerts it raises.
Exactly. 24 hours of clean logs only tells you nobody used the tokens in those 24 hours. And yes, without the exceptions list the report was getting noisy fast: a few findings are intended, like a public chat widget that writes anonymous sessions, and seeing those every morning would have trained me to skim. Writing the reason down also forces the question "is this really intended?" once.
That chat widget example is a good one — an intended finding that keeps surfacing is exactly how a scan loses trust.
One thing I'd add is a review date on each exception, so an 'intended for now' decision gets revisited instead of living forever.
If the reason ever stops matching what the widget actually does, that date is what catches it.
Ha, that's the gap in my setup right now. Every exception has a reason, but nothing makes me look at it again, so "intended for now" quietly turns into "intended forever". Your last line is the real point: the reason can be true the day I write it and wrong six months later. A review date on each entry is next on my list. Thanks, this thread has made the scanner better.
A light way to do it is a 15-minute recurring calendar block each week to re-read the exceptions list. Keep one line per exception — what was excepted and why — so anything past its review date expires instead of accumulating silently.
Thanks, I actually added this after your first comment. Every exception now has an owner and a review-by date. If one has no date, it's flagged in the daily security report, and once the date passes it comes back as a high-priority finding until someone re-checks it. So nothing stays approved forever without anyone looking.
The calendar block is a good idea on top of that, because the report only catches what's already on the list.
The using (true) demo policy is the silent killer here. The rotation part is what most guides miss—re-running OAuth doesn't kill old tokens, so rotating the client secret is mandatory. Great reminder that AI makes building fast but doesn't do the security review.
Thanks. The OAuth part surprised me too: you re-run it, get a fresh token, and assume the old one is dead. It isn't until you rotate the secret and revoke the old one. That's why the last step is testing with the old token on purpose and expecting a 401.
Testing with the old token to ensure a 401 is the only way to truly prove it's dead. Assuming a token expired just because you re-ran the OAuth flow is exactly how these leaks slip through the cracks.
Exactly. The worrying part is that nothing tells you. The old token keeps working without a sound, so if you never test it, you'll never know. Now that 401 check is a fixed step on my list whenever I rotate anything.
"The old token keeps working without a sound" is the whole problem in one sentence. A successfully revoked token and a forgotten one look identical from the outside — both return 200, and nobody notices the difference until something goes wrong.
What helped on my side was adding the rotation date to the secret's metadata, so a stale credential is visible at a glance instead of living only in someone's memory. Combined with the 401 check as a fixed step, it's the closest thing to a real guarantee. Otherwise you're just trusting that the last person who rotated it was thorough.
Testing the old token to verify the 401 is the only real proof it's dead. Assuming re-running OAuth kills the old credentials is a classic, dangerous trap.
The attacker-side verification is the crucial closing step: “we deployed a fix” is not the same as proving old credentials fail and the intended path still works. The daily scanner is also better as a report than an auto-remediator; an explicit allowlist with an owner and expiry turns exceptions into reviewable risk instead of permanent prototype residue.
Agreed on both. The expiry point is a fair hit. My allowlist has a written reason for every exception, but no owner and no expiry date, so an exception from last month looks as valid as one from today. I'm adding a review-by date to each entry, and the scan will flag any that have expired. Thanks, that's a better design than mine.
If some staff should only see a subset of the four stores, I'd add a staging test with one fake order in store A and one in B, and a staff login allowed to use only A. Check that the login can read A's order through the app API and gets no order data when it asks for B's by ID. If staff can edit orders, check that an edit to A works and that an attempted edit to B leaves its row unchanged. That covers the case where a caller passes the staff check but should still be denied access to a store.