Skip to content

Add apiSecretKeyFallback to validate inbound HMACs during client-secret rotation#3240

Open
morgan-coded wants to merge 2 commits into
Shopify:mainfrom
morgan-coded:fix/3183-webhook-secret-rotation-fallback
Open

Add apiSecretKeyFallback to validate inbound HMACs during client-secret rotation#3240
morgan-coded wants to merge 2 commits into
Shopify:mainfrom
morgan-coded:fix/3183-webhook-secret-rotation-fallback

Conversation

@morgan-coded
Copy link
Copy Markdown

WHY are these changes introduced?

Fixes #3183.

Shopify signs outbound requests with the oldest non-revoked client secret, and
(as reported in #3183) continues to do so for a window observed up to ~30 minutes
after the old secret is revoked. @shopify/shopify-api only ever validates inbound
HMACs against the single apiSecretKey, so the moment an app rotates apiSecretKey
to the new secret, every still-in-flight request signed with the old secret fails
HMAC validation and is rejected with a 401. On high-volume topics (orders/create,
orders/updated) the drop volume during a rotation is not negligible.

WHAT is this pull request doing?

Adds an optional apiSecretKeyFallback config option, consulted only for
inbound HMAC validation (webhooks, Flow, and fulfillment-service — all of which
share validateHmacString):

  • The primary apiSecretKey is checked first; the fallback is tried only if the
    primary fails. If both fail, the request is rejected exactly as before.
  • Outbound signing (generateLocalHmac) is unchanged and always uses
    apiSecretKey, so the app never emits requests signed with the old secret.
  • When a request validates via the fallback, a Warning-level log is emitted (via
    the existing logger(config)). Operators watch these logs to know when Shopify has
    stopped using the old secret, at which point the fallback can be safely unset.

Rotation procedure with this option:

  1. Create the new secret in Partners (old secret stays valid).
  2. Set apiSecretKey = S_new, apiSecretKeyFallback = S_old; deploy.
  3. Revoke S_old in Partners.
  4. Watch the warn logs; once they stop, unset apiSecretKeyFallback and redeploy.

This is additive and backward compatible: when apiSecretKeyFallback is unset,
behavior is identical to today.

How to test your changes?

Targeted jest --selectProjects library --testPathPatterns "hmac-validator"
(25/25 passing after the hardening patch). The new coverage includes primary
match, fallback match, the warn log, no-warn-on-primary, both-fail rejection,
no-fallback rejection, and a Flow fallback request.

Checklist

  • I have added/updated tests for my changes
  • I have added a changeset (.changeset/webhook-hmac-secret-fallback.md)
  • I have updated relevant documentation

morgan-coded and others added 2 commits May 29, 2026 03:16
…tation

Inbound HMAC validation (webhooks, Flow, fulfillment-service) only ever
checked against config.apiSecretKey. During a client-secret rotation
Shopify keeps signing requests with the previous (oldest non-revoked)
secret for a window observed up to ~30 minutes after revocation, so once
an app rotates apiSecretKey to the new secret those in-flight requests
fail validation and are dropped — non-trivial volume on high-traffic
topics like orders/create.

Add an optional apiSecretKeyFallback config option, consulted only for
inbound HMAC validation in validateHmacString (the shared path behind
webhooks, Flow, and fulfillment-service). The primary secret is checked
first; the fallback is tried only if the primary fails. Outbound signing
(generateLocalHmac) is unchanged and always uses apiSecretKey.

When a request validates via the fallback, a Warning-level log is emitted
so operators can observe when Shopify has stopped using the old secret
and the fallback can be safely removed.

Fixes Shopify#3183

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 29, 2026 15:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds an optional apiSecretKeyFallback config to support seamless client-secret rotation by accepting inbound HMACs signed with a previous secret, while logging a warning when the fallback is used.

Changes:

  • New optional apiSecretKeyFallback field on ConfigParams.
  • validateHmacString falls back to the secondary secret and emits a warning log when matched.
  • Added test coverage for the rotation/fallback scenarios and a changeset.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
packages/apps/shopify-api/lib/base-types.ts Documents and introduces the optional apiSecretKeyFallback config field.
packages/apps/shopify-api/lib/utils/hmac-validator.ts Implements fallback-secret validation with a warning log on match.
packages/apps/shopify-api/lib/utils/tests/hmac-validator.test.ts Adds tests covering primary/fallback/no-match scenarios across webhook and Flow types.
.changeset/webhook-hmac-secret-fallback.md Minor-version changeset describing the new config option.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +111 to +113
if (safeCompare(hmac, localHmac)) {
return true;
}
@github-actions github-actions Bot added the devtools-gardener Post the issue or PR to Slack for the gardener label May 29, 2026
@morgan-coded
Copy link
Copy Markdown
Author

I have signed the CLA!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

devtools-gardener Post the issue or PR to Slack for the gardener

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Webhooks dropped for ~30 min post-revocation: @shopify/shopify-api has no way to accept a second (rotating) secret

2 participants