fix(config): resolve Meta app credentials per product instead of sharing one secret - #8
Conversation
…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.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
Bugbot couldn't run - usage limit reachedBugbot 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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
| 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 { |
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:
13483917451851821866791907324285Two apps means two different app secrets, and one variable can only hold one. Whichever product it does not belong to breaks:
X-Hub-Signature-256is 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.whatsapp_oauth_service.gosent 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:
META_APP_ID,META_APP_SECRET,FB_APP_ID,FB_APP_SECRETandMESSENGER_*stay bound, to the Messenger app only. The threeWHATSAPP_*names were already listed in.env.examplebut nothing read them — they are real bindings now.Also fixed while in here
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.123456789012345that produced a Meta error page indistinguishable from an application bug. They now report which variable to set. Messenger and Instagram also gainresponse_type=code.error.e0349/error.e0351reworded to name the real variables;error.e0353added. 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
verifyMessengerSignaturereturnstruefor a header without thesha256=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 ./...andgo vet ./internal/...clean.go test ./...— all 48 packages green.META_APP_*fallback, and the resolvers returning channel values when no configuration is loaded.pnpm typecheckclean;eslintclean on both changed frontend files.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:
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*Appwith per-channelappId/appSecret. OAuth URL endpoints sharewriteMetaOAuthURL(pinned Graph version,response_type=code), stop using a fake app id, and WhatsApp auth URLs can passchannel_idso 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.