Skip to content

Commit bcffcba

Browse files
committed
Fix when multiple authData keys are passed
1 parent 9c5f149 commit bcffcba

File tree

3 files changed

+234
-57
lines changed

3 files changed

+234
-57
lines changed

spec/ParseUser.spec.js

+152
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,50 @@ describe('Parse.User testing', () => {
904904
}
905905
};
906906
};
907+
908+
var getMockMyOauthProvider = function() {
909+
return {
910+
authData: {
911+
id: "12345",
912+
access_token: "12345",
913+
expiration_date: new Date().toJSON(),
914+
},
915+
shouldError: false,
916+
loggedOut: false,
917+
synchronizedUserId: null,
918+
synchronizedAuthToken: null,
919+
synchronizedExpiration: null,
920+
921+
authenticate: function(options) {
922+
if (this.shouldError) {
923+
options.error(this, "An error occurred");
924+
} else if (this.shouldCancel) {
925+
options.error(this, null);
926+
} else {
927+
options.success(this, this.authData);
928+
}
929+
},
930+
restoreAuthentication: function(authData) {
931+
if (!authData) {
932+
this.synchronizedUserId = null;
933+
this.synchronizedAuthToken = null;
934+
this.synchronizedExpiration = null;
935+
return true;
936+
}
937+
this.synchronizedUserId = authData.id;
938+
this.synchronizedAuthToken = authData.access_token;
939+
this.synchronizedExpiration = authData.expiration_date;
940+
return true;
941+
},
942+
getAuthType: function() {
943+
return "myoauth";
944+
},
945+
deauthenticate: function() {
946+
this.loggedOut = true;
947+
this.restoreAuthentication(null);
948+
}
949+
};
950+
};
907951

908952
var ExtendedUser = Parse.User.extend({
909953
extended: function() {
@@ -1284,6 +1328,114 @@ describe('Parse.User testing', () => {
12841328
}
12851329
});
12861330
});
1331+
1332+
it("link multiple providers", (done) => {
1333+
var provider = getMockFacebookProvider();
1334+
var mockProvider = getMockMyOauthProvider();
1335+
Parse.User._registerAuthenticationProvider(provider);
1336+
Parse.User._logInWith("facebook", {
1337+
success: function(model) {
1338+
ok(model instanceof Parse.User, "Model should be a Parse.User");
1339+
strictEqual(Parse.User.current(), model);
1340+
ok(model.extended(), "Should have used the subclass.");
1341+
strictEqual(provider.authData.id, provider.synchronizedUserId);
1342+
strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
1343+
strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
1344+
ok(model._isLinked("facebook"), "User should be linked to facebook");
1345+
Parse.User._registerAuthenticationProvider(mockProvider);
1346+
let objectId = model.id;
1347+
model._linkWith("myoauth", {
1348+
success: function(model) {
1349+
expect(model.id).toEqual(objectId);
1350+
ok(model._isLinked("facebook"), "User should be linked to facebook");
1351+
ok(model._isLinked("myoauth"), "User should be linked to myoauth");
1352+
done();
1353+
},
1354+
error: function(error) {
1355+
console.error(error);
1356+
fail('SHould not fail');
1357+
done();
1358+
}
1359+
})
1360+
},
1361+
error: function(model, error) {
1362+
ok(false, "linking should have worked");
1363+
done();
1364+
}
1365+
});
1366+
});
1367+
1368+
it("link multiple providers and update token", (done) => {
1369+
var provider = getMockFacebookProvider();
1370+
var mockProvider = getMockMyOauthProvider();
1371+
Parse.User._registerAuthenticationProvider(provider);
1372+
Parse.User._logInWith("facebook", {
1373+
success: function(model) {
1374+
ok(model instanceof Parse.User, "Model should be a Parse.User");
1375+
strictEqual(Parse.User.current(), model);
1376+
ok(model.extended(), "Should have used the subclass.");
1377+
strictEqual(provider.authData.id, provider.synchronizedUserId);
1378+
strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
1379+
strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
1380+
ok(model._isLinked("facebook"), "User should be linked to facebook");
1381+
Parse.User._registerAuthenticationProvider(mockProvider);
1382+
let objectId = model.id;
1383+
model._linkWith("myoauth", {
1384+
success: function(model) {
1385+
expect(model.id).toEqual(objectId);
1386+
ok(model._isLinked("facebook"), "User should be linked to facebook");
1387+
ok(model._isLinked("myoauth"), "User should be linked to myoauth");
1388+
model._linkWith("facebook", {
1389+
success: () => {
1390+
ok(model._isLinked("facebook"), "User should be linked to facebook");
1391+
ok(model._isLinked("myoauth"), "User should be linked to myoauth");
1392+
done();
1393+
},
1394+
error: () => {
1395+
fail('should link again');
1396+
done();
1397+
}
1398+
})
1399+
},
1400+
error: function(error) {
1401+
console.error(error);
1402+
fail('SHould not fail');
1403+
done();
1404+
}
1405+
})
1406+
},
1407+
error: function(model, error) {
1408+
ok(false, "linking should have worked");
1409+
done();
1410+
}
1411+
});
1412+
});
1413+
1414+
it('should fail linking with existing', (done) => {
1415+
var provider = getMockFacebookProvider();
1416+
Parse.User._registerAuthenticationProvider(provider);
1417+
Parse.User._logInWith("facebook", {
1418+
success: function(model) {
1419+
Parse.User.logOut().then(() => {
1420+
let user = new Parse.User();
1421+
user.setUsername('user');
1422+
user.setPassword('password');
1423+
return user.signUp().then(() => {
1424+
// try to link here
1425+
user._linkWith('facebook', {
1426+
success: () => {
1427+
fail('should not succeed');
1428+
done();
1429+
},
1430+
error: (err) => {
1431+
done();
1432+
}
1433+
});
1434+
});
1435+
});
1436+
}
1437+
});
1438+
});
12871439

12881440
it('set password then change password', (done) => {
12891441
Parse.User.signUp('bob', 'barker').then((bob) => {

spec/RestCreate.spec.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ describe('rest create', () => {
163163
}, (err) => {
164164
expect(err.code).toEqual(Parse.Error.UNSUPPORTED_SERVICE);
165165
expect(err.message).toEqual('This authentication method is unsupported.');
166+
NoAnnonConfig.authDataManager.setEnableAnonymousUsers(true);
166167
done();
167168
})
168169
});
@@ -199,7 +200,7 @@ describe('rest create', () => {
199200
done();
200201
});
201202
});
202-
203+
203204
it('stores pointers with a _p_ prefix', (done) => {
204205
var obj = {
205206
foo: 'bar',

src/RestWrite.js

+80-56
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function RestWrite(config, auth, className, query, data, originalData) {
3232
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'objectId ' +
3333
'is an invalid field name.');
3434
}
35-
35+
3636
// When the operation is complete, this.response may have several
3737
// fields.
3838
// response: the actual data to be returned
@@ -211,74 +211,98 @@ RestWrite.prototype.validateAuthData = function() {
211211

212212
var authData = this.data.authData;
213213
var providers = Object.keys(authData);
214-
if (providers.length == 1) {
215-
var provider = providers[0];
214+
if (providers.length > 0) {
215+
var provider = providers[providers.length-1];
216216
var providerAuthData = authData[provider];
217217
var hasToken = (providerAuthData && providerAuthData.id);
218218
if (providerAuthData === null || hasToken) {
219-
return this.handleOAuthAuthData(provider);
219+
return this.handleAuthData(authData);
220220
}
221221
}
222222
throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE,
223223
'This authentication method is unsupported.');
224224
};
225225

226-
RestWrite.prototype.handleOAuthAuthData = function(provider) {
227-
var authData = this.data.authData[provider];
228-
if (authData === null && this.query) {
229-
// We are unlinking from the provider.
230-
this.data["_auth_data_" + provider ] = null;
231-
return;
232-
}
233-
234-
let validateAuthData = this.config.authDataManager.getValidatorForProvider(provider);
226+
RestWrite.prototype.handleAuthDataValidation = function(authData) {
227+
let validations = Object.keys(authData).map((provider) => {
228+
if (authData[provider] === null) {
229+
return Promise.resolve();
230+
}
231+
let validateAuthData = this.config.authDataManager.getValidatorForProvider(provider);
232+
if (!validateAuthData) {
233+
throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE,
234+
'This authentication method is unsupported.');
235+
};
236+
return validateAuthData(authData[provider]);
237+
});
238+
return Promise.all(validations);
239+
}
235240

236-
if (!validateAuthData) {
237-
throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE,
238-
'This authentication method is unsupported.');
239-
};
240-
241-
return validateAuthData(authData)
242-
.then(() => {
243-
// Check if this user already exists
244-
// TODO: does this handle re-linking correctly?
245-
var query = {};
246-
query['authData.' + provider + '.id'] = authData.id;
247-
return this.config.database.find(
241+
RestWrite.prototype.findUsersWithAuthData = function(authData) {
242+
let providers = Object.keys(authData);
243+
let query = providers.reduce((memo, provider) => {
244+
if (!authData[provider]) {
245+
return memo;
246+
}
247+
let queryKey = `authData.${provider}.id`;
248+
let query = {};
249+
query[queryKey] = authData[provider].id;
250+
memo.push(query);
251+
return memo;
252+
}, []).filter((q) => {
253+
return typeof q !== undefined;
254+
});
255+
256+
let findPromise = Promise.resolve([]);
257+
if (query.length > 0) {
258+
findPromise = this.config.database.find(
248259
this.className,
249-
query, {});
250-
}).then((results) => {
251-
this.storage['authProvider'] = provider;
252-
253-
// Put the data in the proper format
254-
this.data["_auth_data_" + provider ] = authData;
255-
256-
if (results.length == 0) {
257-
// this a new user
258-
this.data.username = cryptoUtils.newToken();
259-
} else if (!this.query) {
260-
// Login with auth data
261-
// Short circuit
262-
delete results[0].password;
263-
this.response = {
264-
response: results[0],
265-
location: this.location()
266-
};
267-
this.data.objectId = results[0].objectId;
268-
} else if (this.query && this.query.objectId) {
269-
// Trying to update auth data but users
270-
// are different
271-
if (results[0].objectId !== this.query.objectId) {
272-
delete this.data["_auth_data_" + provider ];
273-
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED,
260+
{'$or': query}, {})
261+
}
262+
263+
return findPromise;
264+
}
265+
266+
RestWrite.prototype.handleAuthData = function(authData) {
267+
let results;
268+
return this.handleAuthDataValidation(authData).then(() => {
269+
return this.findUsersWithAuthData(authData);
270+
}).then((r) => {
271+
results = r;
272+
if (results.length > 1) {
273+
// More than 1 user with the passed id's
274+
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED,
274275
'this auth is already used');
275-
}
276-
} else {
277-
278-
delete this.data["_auth_data_" + provider ];
279-
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'THis should not be reached...');
280-
}
276+
}
277+
// set the proper keys
278+
Object.keys(authData).forEach((provider) => {
279+
this.data[`_auth_data_${provider}`] = authData[provider];
281280
});
281+
282+
if (results.length == 0) {
283+
this.data.username = cryptoUtils.newToken();
284+
} else if (!this.query) {
285+
// Login with auth data
286+
// Short circuit
287+
delete results[0].password;
288+
this.response = {
289+
response: results[0],
290+
location: this.location()
291+
};
292+
this.data.objectId = results[0].objectId;
293+
} else if (this.query && this.query.objectId) {
294+
// Trying to update auth data but users
295+
// are different
296+
if (results[0].objectId !== this.query.objectId) {
297+
Object.keys(authData).forEach((provider) => {
298+
delete this.data[`_auth_data_${provider}`];
299+
});
300+
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED,
301+
'this auth is already used');
302+
}
303+
}
304+
return Promise.resolve();
305+
});
282306
}
283307

284308
// The non-third-party parts of User transformation

0 commit comments

Comments
 (0)