Skip to content

fix(config): resolve Meta app credentials per product instead of sharing one secret - #8

Merged
JOY (JOY) merged 1 commit into
devfrom
fix/per-product-meta-app-credentials
Sep 12, 2026
Merged

JOY (JOY) merged 1 commit into
devfrom
fix/per-product-meta-app-credentials

Conversation

@JOY

@JOY JOY (JOY) commented Sep 12, 2026 •

Copy link
Copy Markdown

The bug

Messenger, Instagram and WhatsApp all read a single META_APP_SECRET. That works only when one Meta app serves all three products.

This deployment registers two Meta apps:

Meta App ID Products
1348391745185182 Facebook Messenger
1866791907324285 Instagram + WhatsApp

Two apps means two different app secrets, and one variable can only hold one. Whichever product it does not belong to breaks:

  • Inbound webhook — X-Hub-Signature-256 is verified against the wrong app's secret, so every delivery is rejected. After feat(whatsapp): store inbound media, close the OAuth loop, fail closed on signatures #7 made WhatsApp verification fail closed, this stops WhatsApp inbound entirely rather than silently accepting unsigned traffic.
  • OAuth code exchange — whatsapp_oauth_service.go sent the Messenger app id and secret for a code issued by the WhatsApp app. Meta refuses that outright, so the Connect flow could never have worked on a two-app deployment.

Neither symptom names its cause; both look like a broken Meta integration.

The fix

One resolution order, used by both the webhook path and the OAuth path:

channel-level value → the product's own deployment value → the shared Messenger value

That last step is what keeps a single-app deployment working with no configuration change, so this is not breaking for anyone who set only META_APP_*.

New per-product variables, following the naming Crove Post already uses so the two products read the same way:

FACEBOOK_APP_ID  / FACEBOOK_APP_SECRET  / FACEBOOK_VERIFY_TOKEN
INSTAGRAM_APP_ID / INSTAGRAM_APP_SECRET / INSTAGRAM_VERIFY_TOKEN
WHATSAPP_APP_ID  / WHATSAPP_APP_SECRET  / WHATSAPP_VERIFY_TOKEN

META_APP_ID, META_APP_SECRET, FB_APP_ID, FB_APP_SECRET and MESSENGER_* stay bound, to the Messenger app only. The three WHATSAPP_* names were already listed in .env.example but nothing read them — they are real bindings now.

Also fixed while in here

  • The WhatsApp channel form has Meta App ID and App Secret fields. The backend already honoured a per-channel appSecret, but there was no field to set one, so a channel on a second Meta app could only be configured by editing the database.
  • The three Meta authorization URL handlers are folded into one helper. Instagram and WhatsApp both read the Messenger app id regardless of product, and all three fell back to a fabricated 123456789012345 that produced a Meta error page indistinguishable from an application bug. They now report which variable to set. Messenger and Instagram also gain response_type=code.
  • The OAuth connect no longer copies a resolved app secret onto the channel. That would freeze a fallback in place, where it keeps winning over the deployment-level value even after the deployment-level value is corrected.
  • error.e0349 / error.e0351 reworded to name the real variables; error.e0353 added. Both locales.

Deliberately not done

Messenger and Instagram webhook verification is still conditional — it only runs when a secret is configured and a signature header arrived, and verifyMessengerSignature returns true for a header without the sha256= prefix. That is the same defect #7 closed for WhatsApp.

Closing it here too would start rejecting live Messenger and Instagram traffic on any deployment whose secret is not yet correct, in the same change that moves where the secret is read from. Two variables changing at once is not debuggable from the outside, so it is sequenced as a follow-up once this split is deployed and the right secret is confirmed in place. Recorded under Known issues in the CHANGELOG.

Verification

  • go build ./... and go vet ./internal/... clean.
  • go test ./... — all 48 packages green.
  • 4 new config tests: per-product resolution, channel-level precedence (including a channel that sets only one of the two), single-app META_APP_* fallback, and the resolvers returning channel values when no configuration is loaded.
  • 2 new service tests: the OAuth exchange authenticates with the WhatsApp app and not the Messenger one; a webhook signed with the Messenger secret is rejected while one signed with the WhatsApp secret is stored.
  • The existing OAuth persistence test now asserts the mixed case it actually exercises — channel-level secret with deployment-level app id.
  • pnpm typecheck clean; eslint clean on both changed frontend files.
  • All three message catalogs parse and carry the same 29 WhatsApp keys.

Not verified: no live call against Meta's endpoints, and no browser check of the two new form fields.

What JOY needs to set

For the two-app layout above, on the GCP VM:

FACEBOOK_APP_ID=1348391745185182
FACEBOOK_APP_SECRET=<secret of the Messenger app>
WHATSAPP_APP_ID=1866791907324285
WHATSAPP_APP_SECRET=<secret of the IG/WhatsApp app>
INSTAGRAM_APP_ID=1866791907324285
INSTAGRAM_APP_SECRET=<secret of the IG/WhatsApp app>

Instagram and WhatsApp share one app here, so they take the same pair. Alternatively, enter App ID and App Secret on each channel in Dashboard → Channels and leave the environment alone.


Note

High Risk
Changes Meta webhook verification and OAuth credential resolution across Messenger, Instagram, and WhatsApp; misconfigured env after deploy can reject inbound traffic or break connect until the right per-product secrets are set.

Overview
Messenger, Instagram, and WhatsApp no longer share a single META_APP_SECRET. Credentials resolve in order: channel fields → product env (FACEBOOK_APP_*, INSTAGRAM_APP_*, WHATSAPP_APP_*) → legacy Messenger/META_APP_*, so one Meta app per deployment still works unchanged while multi-app setups verify webhooks and exchange OAuth codes with the correct app.

Webhook handlers and WhatsApp OAuth/connect now call config.Resolve*App with per-channel appId/appSecret. OAuth URL endpoints share writeMetaOAuthURL (pinned Graph version, response_type=code), stop using a fake app id, and WhatsApp auth URLs can pass channel_id so the issuing app matches the channel. The dashboard WhatsApp form exposes Meta App ID and App Secret; OAuth persist no longer copies resolved secrets onto the channel (avoids freezing env fallbacks). .env.example, i18n errors, and tests cover per-product resolution, channel precedence, and split-app OAuth/webhook behavior.

Reviewed by Cursor Bugbot for commit f26aa32. Configure here.

…ing one secret

Messenger, Instagram and WhatsApp all read a single META_APP_SECRET. That only
works when one Meta app serves all three products. A deployment that registered a
separate app per product has two or three different secrets, one variable can
only hold one, and whichever product it does not belong to fails:

- the inbound webhook verifies X-Hub-Signature-256 against the wrong secret, so
  every delivery is rejected;
- the WhatsApp OAuth exchange sent the Messenger app id and secret for a code
  issued by the WhatsApp app, which Meta refuses outright.

Neither symptom points at the actual cause, so both look like a broken
integration rather than a mis-set variable.

Credentials now resolve in one order, used by both the webhook path and the OAuth
path: the channel's own value, then the product's deployment-level value, then
the shared Messenger value. The last step is what keeps a single-app deployment
working without any configuration change.

Per-product variables follow the naming already used by Crove Post, so the two
products read the same way:

  FACEBOOK_APP_ID / FACEBOOK_APP_SECRET / FACEBOOK_VERIFY_TOKEN
  INSTAGRAM_APP_ID / INSTAGRAM_APP_SECRET / INSTAGRAM_VERIFY_TOKEN
  WHATSAPP_APP_ID / WHATSAPP_APP_SECRET / WHATSAPP_VERIFY_TOKEN

META_APP_ID, META_APP_SECRET, FB_APP_ID, FB_APP_SECRET and MESSENGER_* remain
bound, to the Messenger app only. The three WHATSAPP_* names were already
documented in .env.example but nothing read them; they are real bindings now.

The WhatsApp channel form gains Meta App ID and Meta App Secret fields. The
backend already honoured a per-channel appSecret, but no field existed to set
one, so a channel on a second Meta app could only be configured by editing the
database. The OAuth URL endpoint takes the channel id and resolves the app id
through the channel first, for the same reason.

The three Meta authorization URL handlers are folded into one helper. Instagram
and WhatsApp both read the Messenger app id regardless of product, and all three
fell back to a fabricated "123456789012345" that produced a Meta error page
indistinguishable from an application bug. They now report which variable to
set, and Messenger and Instagram gain response_type=code, which WhatsApp already
needed and without which Meta returns a token fragment the server never sees.

The OAuth connect no longer copies a resolved app secret onto the channel. Doing
so would freeze a fallback in place, where it keeps winning over the
deployment-level credential even after that credential is corrected.

Messenger and Instagram verification is left conditional in this change. Closing
it the way WhatsApp was closed would start rejecting live traffic on any
deployment whose secret is not yet correct, and that has to be sequenced after
this split is deployed and the right secret is in place.
@coderabbitai

coderabbitai Bot commented Sep 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 4e91b67d-caa3-4f52-859e-59c90965bbbd

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor

cursor Bot commented Sep 12, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_9f05a23f-e9fc-439d-8527-a7ebdf1f5d7e)

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces per-product Meta app credential resolution for Facebook Messenger, Instagram, and WhatsApp, allowing deployments to use separate Meta apps for each channel. It updates environment variable bindings, adds Meta App ID and Secret fields to the WhatsApp channel configuration UI, and includes focused tests for the new resolution logic. The review feedback highlights critical issues where potential nil pointer dereferences could occur when accessing channel configurations in the Instagram and Messenger inbound services, and suggests verifying the channel type before parsing WhatsApp configurations in the OAuth handler.

if appSecret == "" {
appSecret = strings.TrimSpace(os.Getenv("FB_APP_SECRET"))
}
appSecret := config.ResolveInstagramApp(cfg.AppID, cfg.AppSecret).AppSecret

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

If cfg is nil (which can happen if the channel configuration is missing or invalid), accessing cfg.AppID and cfg.AppSecret directly will cause a nil pointer dereference panic. We should safely extract these values only when cfg is non-nil.

		var appID, channelSecret string
		if cfg != nil {
			appID, channelSecret = cfg.AppID, cfg.AppSecret
		}
		appSecret := config.ResolveInstagramApp(appID, channelSecret).AppSecret

if appSecret == "" {
appSecret = strings.TrimSpace(os.Getenv("FB_APP_SECRET"))
}
appSecret := config.ResolveMessengerApp(cfg.AppID, cfg.AppSecret).AppSecret

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

If cfg is nil (which can happen if the channel configuration is missing or invalid), accessing cfg.AppID and cfg.AppSecret directly will cause a nil pointer dereference panic. We should safely extract these values only when cfg is non-nil.

		var appID, channelSecret string
		if cfg != nil {
			appID, channelSecret = cfg.AppID, cfg.AppSecret
		}
		appSecret := config.ResolveMessengerApp(appID, channelSecret).AppSecret

// issued it, so the channel's own app id wins.
channelAppID := ""
if channelID, parseErr := strconv.ParseInt(strings.TrimSpace(ctx.Query("channel_id")), 10, 64); parseErr == nil && channelID > 0 {
if channel := services.ChannelService.Get(channelID); channel != nil && channel.Status != enums.StatusDeleted {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

We should verify that the retrieved channel is actually of type WhatsApp before attempting to parse its configuration as a WhatsApp channel configuration. If a channel of a different type (e.g., Messenger) is passed, parsing its configuration as a WhatsApp configuration could lead to unexpected behavior or incorrect credential resolution.

Suggested change
if channel := services.ChannelService.Get(channelID); channel != nil && channel.Status != enums.StatusDeleted {
if channel := services.ChannelService.Get(channelID); channel != nil && channel.Status != enums.StatusDeleted && strings.TrimSpace(channel.ChannelType) == enums.ChannelTypeWhatsApp {

@JOY
JOY (JOY) merged commit 6342ec4 into dev Sep 12, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant