From 6960f36bc1f2a5953cb426d7c11fe400555484da Mon Sep 17 00:00:00 2001 From: Valerie Pomerleau Date: Tue, 17 Feb 2026 14:28:24 -0800 Subject: [PATCH] fix(oauth): Prevent validation errors from orphaned client tokens Because: * Sentry showed ValidationError: "[0].name" is required * OAuth token queries use LEFT OUTER JOIN with clients table * When a client is deleted but tokens remain (orphaned), the JOIN returns NULL * This may be converted to undefined, which fails Joi validation This commit: * Add nullish coalescing in factories.ts when merging OAuth client names; Joi validation explicitely allows null * Fix shared reference bug in getDefaultClientFields() to return copy of defaults * Add regression test for undefined client_name handling Closes #FXA-13132 --- .../fxa-shared/connected-services/factories.ts | 4 ++-- .../test/connected-services/factories.ts | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/fxa-shared/connected-services/factories.ts b/packages/fxa-shared/connected-services/factories.ts index 685de66723f..2cb0d58f438 100644 --- a/packages/fxa-shared/connected-services/factories.ts +++ b/packages/fxa-shared/connected-services/factories.ts @@ -257,7 +257,7 @@ export class ConnectedServicesFactory { // We fill in a default device name from the OAuth client name, // but individual clients can override this in their device record registration. if (!client.name) { - client.name = oauthClient.client_name; + client.name = oauthClient.client_name ?? null; } // For now we assume that all oauth clients that register a device record are mobile apps. // Ref https://github.com/mozilla/fxa/issues/449 @@ -292,6 +292,6 @@ export class ConnectedServicesFactory { } protected getDefaultClientFields(): AttachedClient { - return attachedClientsDefaults; + return { ...attachedClientsDefaults }; } } diff --git a/packages/fxa-shared/test/connected-services/factories.ts b/packages/fxa-shared/test/connected-services/factories.ts index 69c460bb640..75d7b04548b 100644 --- a/packages/fxa-shared/test/connected-services/factories.ts +++ b/packages/fxa-shared/test/connected-services/factories.ts @@ -193,5 +193,23 @@ describe('connected-services/factories', () => { Sinon.assert.calledOnce(bStubbed.oauthClients); Sinon.assert.calledOnce(bStubbed.sessions); }); + + it('handles undefined client_name without validation errors', async () => { + oauthClients = [ + { + refresh_token_id: 'test-oauth', + created_time: Date.now(), + last_access_time: Date.now(), + client_name: undefined as any, // Simulate undefined from database + } as AttachedOAuthClient, + ]; + deviceList = []; + sessions = []; + + const results = await factory.build('1234', 'en'); + + // Verify name is null, not undefined (required for validation) + assert.strictEqual(results[0].name, null); + }); }); });