From eb5156c454b27094f8b8dcb86484f37b64368564 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 23:00:30 +0000 Subject: [PATCH 1/4] Initial plan From 629b2dac161902df6504b23b86826b6f6bfff24a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 23:05:00 +0000 Subject: [PATCH 2/4] Add test for multi-provider authData issue Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- spec/AuthenticationAdaptersV2.spec.js | 73 +++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/spec/AuthenticationAdaptersV2.spec.js b/spec/AuthenticationAdaptersV2.spec.js index 7301ab54c1..27d06987b1 100644 --- a/spec/AuthenticationAdaptersV2.spec.js +++ b/spec/AuthenticationAdaptersV2.spec.js @@ -76,6 +76,41 @@ describe('Auth Adapter features', () => { validateAppId: () => Promise.resolve(), }; + // Code-based adapter that requires 'code' field (like gpgames) + const codeBasedAdapter = { + validateAppId: () => Promise.resolve(), + validateSetUp: authData => { + if (!authData.code) { + throw new Error('code is required.'); + } + return Promise.resolve({ save: { id: authData.id } }); + }, + validateUpdate: authData => { + if (!authData.code) { + throw new Error('code is required.'); + } + return Promise.resolve({ save: { id: authData.id } }); + }, + validateLogin: authData => { + if (!authData.code) { + throw new Error('code is required.'); + } + return Promise.resolve({ save: { id: authData.id } }); + }, + afterFind: authData => { + // Strip sensitive 'code' field when returning to client + return { id: authData.id }; + }, + }; + + // Simple adapter that doesn't require code + const simpleAdapter = { + validateAppId: () => Promise.resolve(), + validateSetUp: () => Promise.resolve(), + validateUpdate: () => Promise.resolve(), + validateLogin: () => Promise.resolve(), + }; + const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', @@ -1302,4 +1337,42 @@ describe('Auth Adapter features', () => { await user.fetch({ useMasterKey: true }); expect(user.get('authData')).toEqual({ adapterB: { id: 'test' } }); }); + + it('should handle multiple providers: add one while another remains unchanged (code-based)', async () => { + await reconfigureServer({ + auth: { + codeBasedAdapter, + simpleAdapter, + }, + }); + + // Login with code-based provider + const user = new Parse.User(); + await user.save({ authData: { codeBasedAdapter: { id: 'user1', code: 'code1' } } }); + const sessionToken = user.getSessionToken(); + await user.fetch({ sessionToken }); + + // At this point, authData.codeBasedAdapter only has {id: 'user1'} due to afterFind + const current = user.get('authData') || {}; + expect(current.codeBasedAdapter).toEqual({ id: 'user1' }); + + // Add a second provider while keeping the first unchanged + user.set('authData', { + ...current, + simpleAdapter: { id: 'simple1' }, + // codeBasedAdapter is NOT modified (no new code provided) + }); + + // This should succeed without requiring 'code' for codeBasedAdapter + await user.save(null, { sessionToken }); + + // Verify both providers are present + const reloaded = await new Parse.Query(Parse.User).get(user.id, { + useMasterKey: true, + }); + + const authData = reloaded.get('authData') || {}; + expect(authData.simpleAdapter && authData.simpleAdapter.id).toBe('simple1'); + expect(authData.codeBasedAdapter && authData.codeBasedAdapter.id).toBe('user1'); + }); }); From 5018858342497fb2c43a8a03caa8da9c571e4dbc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 23:06:55 +0000 Subject: [PATCH 3/4] Fix hasMutatedAuthData to only validate when provider id changes Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- spec/AuthenticationAdaptersV2.spec.js | 2 +- src/Auth.js | 23 +++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/spec/AuthenticationAdaptersV2.spec.js b/spec/AuthenticationAdaptersV2.spec.js index 27d06987b1..e7bde12239 100644 --- a/spec/AuthenticationAdaptersV2.spec.js +++ b/spec/AuthenticationAdaptersV2.spec.js @@ -1362,7 +1362,7 @@ describe('Auth Adapter features', () => { simpleAdapter: { id: 'simple1' }, // codeBasedAdapter is NOT modified (no new code provided) }); - + // This should succeed without requiring 'code' for codeBasedAdapter await user.save(null, { sessionToken }); diff --git a/src/Auth.js b/src/Auth.js index d8bf7e651f..f08f7293aa 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -1,5 +1,4 @@ const Parse = require('parse/node'); -import { isDeepStrictEqual } from 'util'; import { getRequestObject, resolveError } from './triggers'; import { logger } from './logger'; import { LRUCache as LRU } from 'lru-cache'; @@ -456,9 +455,29 @@ const hasMutatedAuthData = (authData, userAuthData) => { if (provider === 'anonymous') { return; } const providerData = authData[provider]; const userProviderAuthData = userAuthData[provider]; - if (!isDeepStrictEqual(providerData, userProviderAuthData)) { + + // If unlinking (setting to null), consider it mutated + if (providerData === null) { + mutatedAuthData[provider] = providerData; + return; + } + + // If provider doesn't exist in stored data, it's new + if (!userProviderAuthData) { mutatedAuthData[provider] = providerData; + return; } + + // If provider exists, check if the id has changed + // Only consider it mutated if the id is different + // This prevents re-validation when auth adapters strip fields via afterFind + if (providerData?.id !== userProviderAuthData?.id) { + mutatedAuthData[provider] = providerData; + return; + } + + // If id is the same, don't treat as mutation even if other fields differ + // This handles the case where afterFind strips sensitive fields like 'code' }); const hasMutatedAuthData = Object.keys(mutatedAuthData).length !== 0; return { hasMutatedAuthData, mutatedAuthData }; From b11e22846d66ef9467ea544c9834b643daf3b436 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 27 Jan 2026 08:23:49 +0100 Subject: [PATCH 4/4] fix --- src/Auth.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Auth.js b/src/Auth.js index f08f7293aa..0601151ca4 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -1,4 +1,5 @@ const Parse = require('parse/node'); +import { isDeepStrictEqual } from 'util'; import { getRequestObject, resolveError } from './triggers'; import { logger } from './logger'; import { LRUCache as LRU } from 'lru-cache'; @@ -468,16 +469,21 @@ const hasMutatedAuthData = (authData, userAuthData) => { return; } - // If provider exists, check if the id has changed - // Only consider it mutated if the id is different - // This prevents re-validation when auth adapters strip fields via afterFind - if (providerData?.id !== userProviderAuthData?.id) { + // Check if incoming data represents actual changes vs just echoing back + // what afterFind returned. If incoming data is a subset of stored data + // (all incoming fields match stored values), it's not mutated. + // If incoming data has different values or fields not in stored data, it's mutated. + // This handles the case where afterFind strips sensitive fields like 'code': + // - Incoming: { id: 'x' }, Stored: { id: 'x', code: 'secret' } -> NOT mutated (subset) + // - Incoming: { id: 'x', token: 'new' }, Stored: { id: 'x', token: 'old' } -> MUTATED + const incomingKeys = Object.keys(providerData || {}); + const hasChanges = incomingKeys.some(key => { + return !isDeepStrictEqual(providerData[key], userProviderAuthData[key]); + }); + + if (hasChanges) { mutatedAuthData[provider] = providerData; - return; } - - // If id is the same, don't treat as mutation even if other fields differ - // This handles the case where afterFind strips sensitive fields like 'code' }); const hasMutatedAuthData = Object.keys(mutatedAuthData).length !== 0; return { hasMutatedAuthData, mutatedAuthData };