feat(payments-next): Add FxA Webhook support#20300
feat(payments-next): Add FxA Webhook support#20300david1alvarez wants to merge 1 commit intomainfrom
Conversation
Because: * FxA has several webhooks that SubPlat can make use of This commit: * Adds a FxaWebhookService class * Adds routes to the payments-api service to receive webhooks * Adds validations to only handle valid webhook requests Closes #PAY-3464
There was a problem hiding this comment.
Pull request overview
Adds Firefox Accounts (FxA) Security Event Token (SET) webhook handling to the payments-next API by introducing a new FxA webhook service/controller pair, associated config/types/errors, and a local test script to generate and send signed events.
Changes:
- Introduces
FxaWebhookService+FxaWebhooksControllerto authenticate and dispatch FxA webhook events. - Adds
FxaWebhookConfigand wires it into payments-apiRootConfig/AppModuleto enable configuration-driven validation. - Adds unit tests and a local integration script (
test-fxa-webhook.ts) to exercise the endpoint.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| libs/payments/webhooks/src/lib/fxa-webhooks.types.ts | Defines FxA event URIs and SET payload types. |
| libs/payments/webhooks/src/lib/fxa-webhooks.service.ts | Implements SET bearer token extraction, signature verification, and event dispatch. |
| libs/payments/webhooks/src/lib/fxa-webhooks.service.spec.ts | Adds tests for auth validation, event dispatch, and unhandled event reporting. |
| libs/payments/webhooks/src/lib/fxa-webhooks.error.ts | Adds structured errors for auth failures and unhandled event types. |
| libs/payments/webhooks/src/lib/fxa-webhooks.controller.ts | Exposes POST /webhooks/fxa endpoint. |
| libs/payments/webhooks/src/lib/fxa-webhooks.controller.spec.ts | Tests controller-to-service wiring. |
| libs/payments/webhooks/src/lib/fxa-webhooks.config.ts | Adds typed config for issuer/audience/public JWK (with env JSON parsing). |
| libs/payments/webhooks/src/index.ts | Exports new FxA webhook modules from the webhooks library. |
| apps/payments/api/src/scripts/test-fxa-webhook.ts | Local script to sign and POST a SET to the webhook endpoint. |
| apps/payments/api/src/config/index.ts | Adds FxA webhook config to the API root typed config schema. |
| apps/payments/api/src/app/app.module.ts | Registers the new FxA controller/service in the payments API module. |
| apps/payments/api/.env | Adds FxA webhook env var placeholders for local config. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const signed = match[1] + '.' + match[2]; | ||
| const signature = Buffer.from(match[3], 'base64'); | ||
| const verifier = crypto.createVerify('RSA-SHA256'); | ||
| verifier.update(signed); | ||
|
|
||
| if (!verifier.verify(this.publicPem, signature)) { | ||
| return null; | ||
| } | ||
|
|
||
| const payload = JSON.parse( | ||
| Buffer.from(match[2], 'base64').toString() | ||
| ) as FxaSecurityEventTokenPayload; |
There was a problem hiding this comment.
JWTs use base64url encoding for the header/payload/signature segments, but this code decodes them with Buffer.from(..., 'base64'). Since the regex explicitly allows '-' and '_' (base64url alphabet), using 'base64' here can lead to incorrect decoding and failed signature verification/parsing in some Node versions. Consider decoding with 'base64url' (or normalizing base64url to base64) for both the signature and payload segments.
| this.log.log('handlePasswordChange', { sub, event }); | ||
| this.statsd.increment('fxa.webhook.event', { | ||
| eventType: 'password-change', | ||
| }); | ||
| } | ||
|
|
||
| private async handleProfileChange( | ||
| sub: string, | ||
| event: FxaProfileChangeEvent | ||
| ): Promise<void> { | ||
| this.log.log('handleProfileChange', { sub, event }); | ||
| this.statsd.increment('fxa.webhook.event', { | ||
| eventType: 'profile-change', | ||
| }); |
There was a problem hiding this comment.
These handlers log the full event payload (and sub). For profile-change this can include email and other account state fields, which is likely PII/sensitive and could end up in centralized logs. Consider logging only a minimal, non-PII subset (e.g., event type + uid hash/last4) or redacting specific fields before logging.
| @IsString() | ||
| public readonly fxaWebhookAudience!: string; | ||
|
|
||
| @Transform(({ value }) => (typeof value === 'string' ? JSON.parse(value) : value)) |
There was a problem hiding this comment.
The JSON.parse() in this @Transform will throw a raw SyntaxError when the env var is malformed (or an empty string), which can make configuration failures harder to diagnose. Consider catching parse errors and surfacing a clearer configuration/validation error message for FXA_WEBHOOK_PUBLIC_JWK.
| @Transform(({ value }) => (typeof value === 'string' ? JSON.parse(value) : value)) | |
| @Transform(({ value }) => { | |
| if (typeof value !== 'string') { | |
| return value; | |
| } | |
| try { | |
| return JSON.parse(value); | |
| } catch (err: any) { | |
| const message = | |
| 'Invalid JSON provided for FXA_WEBHOOK_PUBLIC_JWK environment variable: ' + | |
| (err && err.message ? err.message : String(err)); | |
| throw new Error(message); | |
| } | |
| }) |
| #!/usr/bin/env ts-node | ||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
|
||
| /** | ||
| * Local integration test script for the FxA webhook endpoint. | ||
| * | ||
| * Generates a signed JWT Security Event Token and POSTs it to the | ||
| * payments API webhook route. Uses the test RSA key pair from the | ||
| * event-broker test suite. | ||
| * | ||
| * Usage: | ||
| * npx tsx apps/payments/api/src/scripts/test-fxa-webhook.ts [options] | ||
| * |
There was a problem hiding this comment.
The script header says to run with npx tsx ..., but the shebang is #!/usr/bin/env ts-node. This mismatch can confuse users and may fail depending on what runtime is installed. Consider aligning the shebang and the documented invocation (either tsx everywhere or ts-node everywhere).
| this.statsd.increment('fxa.webhook.error'); | ||
| } | ||
| this.log.error(error); | ||
| Sentry.captureException(error); |
There was a problem hiding this comment.
handleWebhookEvent captures all errors (including expected auth failures like missing/invalid Authorization) to Sentry. This can create noisy alerting and higher ingestion costs if the endpoint receives routine invalid traffic. Consider skipping Sentry.captureException for FxaWebhookAuthError (while still incrementing StatsD) or capturing it at a lower severity/sampled rate.
| Sentry.captureException(error); | |
| if (!(error instanceof FxaWebhookAuthError)) { | |
| Sentry.captureException(error); | |
| } |
Because:
This commit:
Closes #PAY-3464
Checklist
Put an
xin the boxes that applyHow to review (Optional)
To verify, take a look at apps/payments/api/src/scripts/test-fxa-webhook.ts