Skip to content

feat(payments-next): Add FxA Webhook support#20300

Open
david1alvarez wants to merge 1 commit intomainfrom
PAY-3464
Open

feat(payments-next): Add FxA Webhook support#20300
david1alvarez wants to merge 1 commit intomainfrom
PAY-3464

Conversation

@david1alvarez
Copy link
Copy Markdown
Contributor

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

Checklist

Put an x in the boxes that apply

  • My commit is GPG signed.
  • If applicable, I have modified or added tests which pass locally.
  • I have added necessary documentation (if appropriate).
  • I have verified that my changes render correctly in RTL (if appropriate).
  • I have manually reviewed all AI generated code.

How to review (Optional)

To verify, take a look at apps/payments/api/src/scripts/test-fxa-webhook.ts

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
@david1alvarez david1alvarez requested a review from a team as a code owner April 1, 2026 01:45
Copilot AI review requested due to automatic review settings April 1, 2026 01:45
Copy link
Copy Markdown

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

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 + FxaWebhooksController to authenticate and dispatch FxA webhook events.
  • Adds FxaWebhookConfig and wires it into payments-api RootConfig / AppModule to 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.

Comment on lines +101 to +112
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;
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +163 to +176
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',
});
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
@IsString()
public readonly fxaWebhookAudience!: string;

@Transform(({ value }) => (typeof value === 'string' ? JSON.parse(value) : value))
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
@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);
}
})

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +15
#!/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]
*
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
this.statsd.increment('fxa.webhook.error');
}
this.log.error(error);
Sentry.captureException(error);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
Sentry.captureException(error);
if (!(error instanceof FxaWebhookAuthError)) {
Sentry.captureException(error);
}

Copilot uses AI. Check for mistakes.
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.

2 participants