Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions spec/AuthenticationAdaptersV2.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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');
});
});
23 changes: 21 additions & 2 deletions src/Auth.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 };
Expand Down
Loading