Skip to content
Open
Show file tree
Hide file tree
Changes from all 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');
});
});
27 changes: 26 additions & 1 deletion src/Auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,32 @@ 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;
}

// 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;
}
});
Expand Down
Loading