Skip to content

Commit 690f51e

Browse files
committed
Refactor authData handling: streamline validation, enhance security, and improve efficiency
1 parent 2b26079 commit 690f51e

16 files changed

Lines changed: 5150 additions & 280 deletions

package-lock.json

Lines changed: 258 additions & 221 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
"@semantic-release/changelog": "6.0.3",
7979
"@semantic-release/commit-analyzer": "13.0.1",
8080
"@semantic-release/git": "10.0.1",
81-
"@semantic-release/github": "11.0.2",
81+
"@semantic-release/github": "11.0.3",
8282
"@semantic-release/npm": "12.0.1",
8383
"@semantic-release/release-notes-generator": "14.0.3",
8484
"all-node-versions": "13.0.1",

spec/AuthenticationAdaptersV2.spec.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,7 @@ describe('Auth Adapter features', () => {
803803
const user = new Parse.User();
804804

805805
const payload = { someData: true };
806+
const payload2 = { someData: false };
806807

807808
await user.save({
808809
username: 'test',
@@ -829,10 +830,10 @@ describe('Auth Adapter features', () => {
829830
expect(firstCall[2].user.id).toEqual(user.id);
830831
expect(firstCall.length).toEqual(3);
831832

832-
await user.save({ authData: { baseAdapter2: payload } }, { useMasterKey: true });
833+
await user.save({ authData: { baseAdapter2: payload2 } }, { useMasterKey: true });
833834

834835
const secondCall = baseAdapter2.validateAuthData.calls.argsFor(1);
835-
expect(secondCall[0]).toEqual(payload);
836+
expect(secondCall[0]).toEqual(payload2);
836837
expect(secondCall[1]).toEqual(baseAdapter2);
837838
expect(secondCall[2].isChallenge).toBeUndefined();
838839
expect(secondCall[2].master).toEqual(true);

spec/RestWrite.handleAuthData.spec.js

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ describe('RestWrite.handleAuthData', () => {
3434
gpgames: {
3535
clientId: 'validClientId',
3636
clientSecret: 'validClientSecret',
37-
}
37+
},
38+
instagram: {
39+
clientId: 'validClientId',
40+
clientSecret: 'validClientSecret',
41+
redirectUri: 'https://example.com/callback',
42+
},
3843
},
3944
});
4045
};
@@ -51,17 +56,54 @@ describe('RestWrite.handleAuthData', () => {
5156
const sessionToken = user.getSessionToken();
5257

5358
await user.fetch({ sessionToken });
54-
const currentAuthData = user.get('authData') || {};
59+
const currentAuthData = user.get('authData');
60+
expect(currentAuthData).toBeDefined();
61+
62+
// Add another provider to ensure gpgames removal doesn't delete all authData
63+
mockFetch([
64+
{
65+
url: 'https://api.instagram.com/oauth/access_token',
66+
method: 'POST',
67+
response: {
68+
ok: true,
69+
json: () => Promise.resolve({ access_token: 'ig_token' }),
70+
},
71+
},
72+
{
73+
url: 'https://graph.instagram.com/me?fields=id&access_token=ig_token',
74+
method: 'GET',
75+
response: {
76+
ok: true,
77+
json: () => Promise.resolve({ id: 'I1' }),
78+
},
79+
},
80+
]);
5581

5682
user.set('authData', {
5783
...currentAuthData,
84+
instagram: { id: 'I1', code: 'IC1' },
85+
});
86+
await user.save(null, { sessionToken });
87+
88+
await user.fetch({ sessionToken });
89+
const authDataWithInstagram = user.get('authData');
90+
expect(authDataWithInstagram).toBeDefined();
91+
expect(authDataWithInstagram.gpgames).toBeDefined();
92+
expect(authDataWithInstagram.instagram).toBeDefined();
93+
94+
// Now unlink gpgames
95+
user.set('authData', {
96+
...authDataWithInstagram,
5897
gpgames: null,
5998
});
6099
await user.save(null, { sessionToken });
61100

62101
const updatedUser = await new Parse.Query(Parse.User).get(user.id, { useMasterKey: true });
63-
const finalAuthData = updatedUser.get('authData') || {};
102+
const finalAuthData = updatedUser.get('authData');
64103

104+
expect(finalAuthData).toBeDefined();
65105
expect(finalAuthData.gpgames).toBeUndefined();
106+
expect(finalAuthData.instagram).toBeDefined();
107+
expect(finalAuthData.instagram.id).toBe('I1');
66108
});
67109
});

spec/Users.authdata.helpers.js

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
/**
2+
* Helper functions and constants for authData tests
3+
* DRY principle: avoid code duplication across test files
4+
*/
5+
6+
// ============================================
7+
// Constants
8+
// ============================================
9+
10+
const MOCK_USER_ID = 'mockUserId';
11+
const MOCK_USER_ID_2 = 'mockUserId2';
12+
const MOCK_ACCESS_TOKEN = 'mockAccessToken123';
13+
const MOCK_ACCESS_TOKEN_2 = 'mockAccessToken456';
14+
15+
const VALID_CLIENT_ID = 'validClientId';
16+
const VALID_CLIENT_SECRET = 'validClientSecret';
17+
18+
const TEST_USERNAME = 'test';
19+
const TEST_PASSWORD = 'password123';
20+
21+
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
22+
const GOOGLE_PLAYER_URL = (userId) => `https://www.googleapis.com/games/v1/players/${userId}`;
23+
const IG_TOKEN_URL = 'https://api.instagram.com/oauth/access_token';
24+
const IG_ME_URL = (accessToken) =>
25+
`https://graph.instagram.com/me?fields=id&access_token=${accessToken}`;
26+
27+
// ============================================
28+
// Auth Configuration Helpers
29+
// ============================================
30+
31+
/**
32+
* Setup auth configuration with gpgames, instagram, and other providers
33+
* @param {Object} options - Additional configuration options
34+
* @param {boolean} options.includeInstagram - Include instagram provider (default: true)
35+
* @param {boolean} options.includeOther - Include other provider (default: true)
36+
* @param {Object} options.gpgamesConfig - Additional gpgames config (e.g., enableInsecureAuth)
37+
* @returns {Promise} Promise that resolves when server is reconfigured
38+
*/
39+
function setupAuthConfig(options = {}) {
40+
const {
41+
includeInstagram = true,
42+
includeOther = true,
43+
gpgamesConfig = {},
44+
} = options;
45+
46+
const auth = {
47+
gpgames: {
48+
clientId: VALID_CLIENT_ID,
49+
clientSecret: VALID_CLIENT_SECRET,
50+
...gpgamesConfig,
51+
},
52+
};
53+
54+
if (includeInstagram) {
55+
auth.instagram = {
56+
clientId: VALID_CLIENT_ID,
57+
clientSecret: VALID_CLIENT_SECRET,
58+
redirectUri: 'https://example.com/callback',
59+
};
60+
}
61+
62+
if (includeOther) {
63+
auth.other = {
64+
validateAuthData: () => Promise.resolve(),
65+
validateAppId: () => Promise.resolve(),
66+
validateOptions: () => {},
67+
};
68+
}
69+
70+
return reconfigureServer({ auth });
71+
}
72+
73+
// ============================================
74+
// Mock Fetch Helpers
75+
// ============================================
76+
77+
/**
78+
* Create mock for Google Play Games token exchange
79+
* @param {string} accessToken - Access token to return
80+
* @param {Function} onCall - Optional callback when mock is called
81+
* @returns {Object} Mock response object
82+
*/
83+
function mockGpgamesTokenExchange(accessToken = MOCK_ACCESS_TOKEN, onCall = null) {
84+
return {
85+
url: GOOGLE_TOKEN_URL,
86+
method: 'POST',
87+
response: {
88+
ok: true,
89+
json: (options) => {
90+
if (onCall) onCall(options);
91+
return Promise.resolve({ access_token: accessToken });
92+
},
93+
},
94+
};
95+
}
96+
97+
/**
98+
* Create mock for Google Play Games player info
99+
* @param {string} userId - Player ID to return
100+
* @param {Function} onCall - Optional callback when mock is called
101+
* @returns {Object} Mock response object
102+
*/
103+
function mockGpgamesPlayerInfo(userId = MOCK_USER_ID, onCall = null) {
104+
return {
105+
url: GOOGLE_PLAYER_URL(userId),
106+
method: 'GET',
107+
response: {
108+
ok: true,
109+
json: (options) => {
110+
if (onCall) onCall(options);
111+
return Promise.resolve({ playerId: userId });
112+
},
113+
},
114+
};
115+
}
116+
117+
/**
118+
* Create mock for Instagram token exchange
119+
* @param {string} accessToken - Access token to return
120+
* @param {Function} onCall - Optional callback when mock is called
121+
* @returns {Object} Mock response object
122+
*/
123+
function mockInstagramTokenExchange(accessToken = 'ig_token_1', onCall = null) {
124+
return {
125+
url: IG_TOKEN_URL,
126+
method: 'POST',
127+
response: {
128+
ok: true,
129+
json: (options) => {
130+
if (onCall) onCall(options);
131+
return Promise.resolve({ access_token: accessToken });
132+
},
133+
},
134+
};
135+
}
136+
137+
/**
138+
* Create mock for Instagram user info
139+
* @param {string} accessToken - Access token used in URL
140+
* @param {string} userId - User ID to return (default: 'I1')
141+
* @param {Function} onCall - Optional callback when mock is called
142+
* @returns {Object} Mock response object
143+
*/
144+
function mockInstagramUserInfo(accessToken = 'ig_token_1', userId = 'I1', onCall = null) {
145+
return {
146+
url: IG_ME_URL(accessToken),
147+
method: 'GET',
148+
response: {
149+
ok: true,
150+
json: (options) => {
151+
if (onCall) onCall(options);
152+
return Promise.resolve({ id: userId });
153+
},
154+
},
155+
};
156+
}
157+
158+
/**
159+
* Create complete mock for gpgames login flow
160+
* @param {Object} options - Configuration options
161+
* @param {string} options.userId - User ID (default: MOCK_USER_ID)
162+
* @param {string} options.accessToken - Access token (default: MOCK_ACCESS_TOKEN)
163+
* @param {Function} options.onTokenExchange - Callback for token exchange
164+
* @param {Function} options.onPlayerInfo - Callback for player info
165+
* @returns {Array} Array of mock responses
166+
*/
167+
function mockGpgamesLogin(options = {}) {
168+
const {
169+
userId = MOCK_USER_ID,
170+
accessToken = MOCK_ACCESS_TOKEN,
171+
onTokenExchange = null,
172+
onPlayerInfo = null,
173+
} = options;
174+
175+
return [
176+
mockGpgamesTokenExchange(accessToken, onTokenExchange),
177+
mockGpgamesPlayerInfo(userId, onPlayerInfo),
178+
];
179+
}
180+
181+
/**
182+
* Create complete mock for instagram login flow
183+
* @param {Object} options - Configuration options
184+
* @param {string} options.userId - User ID (default: 'I1')
185+
* @param {string} options.accessToken - Access token (default: 'ig_token_1')
186+
* @param {Function} options.onTokenExchange - Callback for token exchange
187+
* @param {Function} options.onUserInfo - Callback for user info
188+
* @returns {Array} Array of mock responses
189+
*/
190+
function mockInstagramLogin(options = {}) {
191+
const {
192+
userId = 'I1',
193+
accessToken = 'ig_token_1',
194+
onTokenExchange = null,
195+
onUserInfo = null,
196+
} = options;
197+
198+
return [
199+
mockInstagramTokenExchange(accessToken, onTokenExchange),
200+
mockInstagramUserInfo(accessToken, userId, onUserInfo),
201+
];
202+
}
203+
204+
/**
205+
* Create mock for error response
206+
* @param {number} status - HTTP status code
207+
* @param {Object} errorData - Error data to return
208+
* @returns {Object} Mock error response object
209+
*/
210+
function mockErrorResponse(status = 400, errorData = { error: 'invalid_grant' }) {
211+
return {
212+
ok: false,
213+
status,
214+
json: () => Promise.resolve(errorData),
215+
};
216+
}
217+
218+
// ============================================
219+
// User Creation Helpers
220+
// ============================================
221+
222+
/**
223+
* Create user with gpgames authData
224+
* @param {Object} options - Configuration options
225+
* @param {string} options.userId - User ID (default: MOCK_USER_ID)
226+
* @param {string} options.code - Auth code (default: 'C1')
227+
* @returns {Promise<Parse.User>} Created user
228+
*/
229+
async function createUserWithGpgames(options = {}) {
230+
const { userId = MOCK_USER_ID, code = 'C1' } = options;
231+
232+
mockFetch(mockGpgamesLogin({ userId }));
233+
234+
return await Parse.User.logInWith('gpgames', {
235+
authData: { id: userId, code },
236+
});
237+
}
238+
239+
/**
240+
* Create user with password auth
241+
* @param {Object} options - Configuration options
242+
* @param {string} options.username - Username (default: TEST_USERNAME)
243+
* @param {string} options.password - Password (default: TEST_PASSWORD)
244+
* @returns {Promise<Parse.User>} Created user
245+
*/
246+
async function createUserWithPassword(options = {}) {
247+
const { username = TEST_USERNAME, password = TEST_PASSWORD } = options;
248+
249+
return await Parse.User.signUp(username, password);
250+
}
251+
252+
// ============================================
253+
// Exports
254+
// ============================================
255+
256+
module.exports = {
257+
// Constants
258+
MOCK_USER_ID,
259+
MOCK_USER_ID_2,
260+
MOCK_ACCESS_TOKEN,
261+
MOCK_ACCESS_TOKEN_2,
262+
VALID_CLIENT_ID,
263+
VALID_CLIENT_SECRET,
264+
TEST_USERNAME,
265+
TEST_PASSWORD,
266+
GOOGLE_TOKEN_URL,
267+
GOOGLE_PLAYER_URL,
268+
IG_TOKEN_URL,
269+
IG_ME_URL,
270+
271+
// Auth Configuration
272+
setupAuthConfig,
273+
274+
// Mock Helpers
275+
mockGpgamesTokenExchange,
276+
mockGpgamesPlayerInfo,
277+
mockInstagramTokenExchange,
278+
mockInstagramUserInfo,
279+
mockGpgamesLogin,
280+
mockInstagramLogin,
281+
mockErrorResponse,
282+
283+
// User Creation
284+
createUserWithGpgames,
285+
createUserWithPassword,
286+
};
287+

0 commit comments

Comments
 (0)