From b09a38836ebc397402df9f785edb3382c81b4af7 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 10 Mar 2016 18:59:19 -0500 Subject: [PATCH] Fix when multiple authData keys are passed --- spec/ParseUser.spec.js | 152 ++++++++++++++++++++++++++++++++++++++++ spec/RestCreate.spec.js | 3 +- src/RestWrite.js | 151 +++++++++++++++++++++++++-------------- 3 files changed, 251 insertions(+), 55 deletions(-) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index a74644ae90..50f29331ee 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -904,6 +904,50 @@ describe('Parse.User testing', () => { } }; }; + + var getMockMyOauthProvider = function() { + return { + authData: { + id: "12345", + access_token: "12345", + expiration_date: new Date().toJSON(), + }, + shouldError: false, + loggedOut: false, + synchronizedUserId: null, + synchronizedAuthToken: null, + synchronizedExpiration: null, + + authenticate: function(options) { + if (this.shouldError) { + options.error(this, "An error occurred"); + } else if (this.shouldCancel) { + options.error(this, null); + } else { + options.success(this, this.authData); + } + }, + restoreAuthentication: function(authData) { + if (!authData) { + this.synchronizedUserId = null; + this.synchronizedAuthToken = null; + this.synchronizedExpiration = null; + return true; + } + this.synchronizedUserId = authData.id; + this.synchronizedAuthToken = authData.access_token; + this.synchronizedExpiration = authData.expiration_date; + return true; + }, + getAuthType: function() { + return "myoauth"; + }, + deauthenticate: function() { + this.loggedOut = true; + this.restoreAuthentication(null); + } + }; + }; var ExtendedUser = Parse.User.extend({ extended: function() { @@ -1284,6 +1328,114 @@ describe('Parse.User testing', () => { } }); }); + + it("link multiple providers", (done) => { + var provider = getMockFacebookProvider(); + var mockProvider = getMockMyOauthProvider(); + Parse.User._registerAuthenticationProvider(provider); + Parse.User._logInWith("facebook", { + success: function(model) { + ok(model instanceof Parse.User, "Model should be a Parse.User"); + strictEqual(Parse.User.current(), model); + ok(model.extended(), "Should have used the subclass."); + strictEqual(provider.authData.id, provider.synchronizedUserId); + strictEqual(provider.authData.access_token, provider.synchronizedAuthToken); + strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration); + ok(model._isLinked("facebook"), "User should be linked to facebook"); + Parse.User._registerAuthenticationProvider(mockProvider); + let objectId = model.id; + model._linkWith("myoauth", { + success: function(model) { + expect(model.id).toEqual(objectId); + ok(model._isLinked("facebook"), "User should be linked to facebook"); + ok(model._isLinked("myoauth"), "User should be linked to myoauth"); + done(); + }, + error: function(error) { + console.error(error); + fail('SHould not fail'); + done(); + } + }) + }, + error: function(model, error) { + ok(false, "linking should have worked"); + done(); + } + }); + }); + + it("link multiple providers and update token", (done) => { + var provider = getMockFacebookProvider(); + var mockProvider = getMockMyOauthProvider(); + Parse.User._registerAuthenticationProvider(provider); + Parse.User._logInWith("facebook", { + success: function(model) { + ok(model instanceof Parse.User, "Model should be a Parse.User"); + strictEqual(Parse.User.current(), model); + ok(model.extended(), "Should have used the subclass."); + strictEqual(provider.authData.id, provider.synchronizedUserId); + strictEqual(provider.authData.access_token, provider.synchronizedAuthToken); + strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration); + ok(model._isLinked("facebook"), "User should be linked to facebook"); + Parse.User._registerAuthenticationProvider(mockProvider); + let objectId = model.id; + model._linkWith("myoauth", { + success: function(model) { + expect(model.id).toEqual(objectId); + ok(model._isLinked("facebook"), "User should be linked to facebook"); + ok(model._isLinked("myoauth"), "User should be linked to myoauth"); + model._linkWith("facebook", { + success: () => { + ok(model._isLinked("facebook"), "User should be linked to facebook"); + ok(model._isLinked("myoauth"), "User should be linked to myoauth"); + done(); + }, + error: () => { + fail('should link again'); + done(); + } + }) + }, + error: function(error) { + console.error(error); + fail('SHould not fail'); + done(); + } + }) + }, + error: function(model, error) { + ok(false, "linking should have worked"); + done(); + } + }); + }); + + it('should fail linking with existing', (done) => { + var provider = getMockFacebookProvider(); + Parse.User._registerAuthenticationProvider(provider); + Parse.User._logInWith("facebook", { + success: function(model) { + Parse.User.logOut().then(() => { + let user = new Parse.User(); + user.setUsername('user'); + user.setPassword('password'); + return user.signUp().then(() => { + // try to link here + user._linkWith('facebook', { + success: () => { + fail('should not succeed'); + done(); + }, + error: (err) => { + done(); + } + }); + }); + }); + } + }); + }); it('set password then change password', (done) => { Parse.User.signUp('bob', 'barker').then((bob) => { diff --git a/spec/RestCreate.spec.js b/spec/RestCreate.spec.js index 7af4e346f5..553d37ad84 100644 --- a/spec/RestCreate.spec.js +++ b/spec/RestCreate.spec.js @@ -163,6 +163,7 @@ describe('rest create', () => { }, (err) => { expect(err.code).toEqual(Parse.Error.UNSUPPORTED_SERVICE); expect(err.message).toEqual('This authentication method is unsupported.'); + NoAnnonConfig.authDataManager.setEnableAnonymousUsers(true); done(); }) }); @@ -199,7 +200,7 @@ describe('rest create', () => { done(); }); }); - + it('stores pointers with a _p_ prefix', (done) => { var obj = { foo: 'bar', diff --git a/src/RestWrite.js b/src/RestWrite.js index d51d61ad23..6f6d616c36 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -32,7 +32,7 @@ function RestWrite(config, auth, className, query, data, originalData) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'objectId ' + 'is an invalid field name.'); } - + // When the operation is complete, this.response may have several // fields. // response: the actual data to be returned @@ -211,74 +211,117 @@ RestWrite.prototype.validateAuthData = function() { var authData = this.data.authData; var providers = Object.keys(authData); - if (providers.length == 1) { - var provider = providers[0]; + if (providers.length > 0) { + var provider = providers[providers.length-1]; var providerAuthData = authData[provider]; var hasToken = (providerAuthData && providerAuthData.id); if (providerAuthData === null || hasToken) { - return this.handleOAuthAuthData(provider); + return this.handleAuthData(authData); } } throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE, 'This authentication method is unsupported.'); }; -RestWrite.prototype.handleOAuthAuthData = function(provider) { - var authData = this.data.authData[provider]; - if (authData === null && this.query) { - // We are unlinking from the provider. - this.data["_auth_data_" + provider ] = null; - return; - } - - let validateAuthData = this.config.authDataManager.getValidatorForProvider(provider); +RestWrite.prototype.handleAuthDataValidation = function(authData) { + let validations = Object.keys(authData).map((provider) => { + if (authData[provider] === null) { + return Promise.resolve(); + } + let validateAuthData = this.config.authDataManager.getValidatorForProvider(provider); + if (!validateAuthData) { + throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE, + 'This authentication method is unsupported.'); + }; + return validateAuthData(authData[provider]); + }); + return Promise.all(validations); +} - if (!validateAuthData) { - throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE, - 'This authentication method is unsupported.'); - }; - - return validateAuthData(authData) - .then(() => { - // Check if this user already exists - // TODO: does this handle re-linking correctly? - var query = {}; - query['authData.' + provider + '.id'] = authData.id; - return this.config.database.find( +RestWrite.prototype.findUsersWithAuthData = function(authData) { + let providers = Object.keys(authData); + let query = providers.reduce((memo, provider) => { + if (!authData[provider]) { + return memo; + } + let queryKey = `authData.${provider}.id`; + let query = {}; + query[queryKey] = authData[provider].id; + memo.push(query); + return memo; + }, []).filter((q) => { + return typeof q !== undefined; + }); + + let findPromise = Promise.resolve([]); + if (query.length > 0) { + findPromise = this.config.database.find( this.className, - query, {}); - }).then((results) => { - this.storage['authProvider'] = provider; - - // Put the data in the proper format - this.data["_auth_data_" + provider ] = authData; - - if (results.length == 0) { - // this a new user - this.data.username = cryptoUtils.newToken(); - } else if (!this.query) { - // Login with auth data - // Short circuit - delete results[0].password; - this.response = { - response: results[0], - location: this.location() - }; - this.data.objectId = results[0].objectId; - } else if (this.query && this.query.objectId) { - // Trying to update auth data but users - // are different - if (results[0].objectId !== this.query.objectId) { - delete this.data["_auth_data_" + provider ]; - throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED, + {'$or': query}, {}) + } + + return findPromise; +} + +RestWrite.prototype.handleAuthData = function(authData) { + let results; + return this.findUsersWithAuthData(authData).then((r) => { + results = r; + if (results.length > 1) { + // More than 1 user with the passed id's + throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED, 'this auth is already used'); + } else if (results.length == 1) { + // One user has this auth data registered + let knownProviders = Object.keys(results[0].authData); + let providers = Object.keys(authData); + // Find the exising linked + // Keep only the new ones + let newAuthData = Object.assign({}, authData); + newAuthData = providers.reduce((memo, provider) => { + if (knownProviders.indexOf(provider) > -1) { + delete memo[provider]; } - } else { - - delete this.data["_auth_data_" + provider ]; - throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'THis should not be reached...'); + return memo; + }, newAuthData); + + if (Object.keys(newAuthData).length != 0) { + // the auth data was sent with more than 1 provider + // only validate the new ones + authData = newAuthData; } + } + return this.handleAuthDataValidation(authData); + }).then(() => { + // set the proper keys + Object.keys(authData).forEach((provider) => { + this.data[`_auth_data_${provider}`] = authData[provider]; }); + + if (results.length == 0) { + this.data.username = cryptoUtils.newToken(); + } else if (!this.query) { + // Login with auth data + // Short circuit + delete results[0].password; + this.response = { + response: results[0], + location: this.location() + }; + this.data.objectId = results[0].objectId; + } else if (this.query && this.query.objectId) { + // Trying to update auth data but users + // are different + if (results[0].objectId !== this.query.objectId) { + Object.keys(authData).forEach((provider) => { + delete this.data[`_auth_data_${provider}`]; + }); + throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED, + 'this auth is already used'); + } + } + return Promise.resolve(); + }); } // The non-third-party parts of User transformation