From 9aaaf78a3615cc75500bb8b17769724e3ef4af32 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Wed, 9 Mar 2016 23:17:40 -0500 Subject: [PATCH 1/5] Anonymous is an OAuth --- src/RestWrite.js | 123 ++++++++++++++------------------------------- src/oauth/index.js | 10 +++- 2 files changed, 47 insertions(+), 86 deletions(-) diff --git a/src/RestWrite.js b/src/RestWrite.js index 9e07c93a10..f54d6a7385 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -211,17 +211,15 @@ RestWrite.prototype.validateAuthData = function() { } var authData = this.data.authData; - var anonData = this.data.authData.anonymous; - - if (this.config.enableAnonymousUsers === true && (anonData === null || - (anonData && anonData.id))) { - return this.handleAnonymousAuthData(); - } - - // Not anon, try other providers var providers = Object.keys(authData); - if (!anonData && providers.length == 1) { + if (providers.length == 1) { + var provider = providers[0]; + if (provider == 'anonymous' && !this.config.enableAnonymousUsers) { + throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE, + 'This authentication method is unsupported.'); + } + var providerAuthData = authData[provider]; var hasToken = (providerAuthData && providerAuthData.id); if (providerAuthData === null || hasToken) { @@ -232,55 +230,8 @@ RestWrite.prototype.validateAuthData = function() { 'This authentication method is unsupported.'); }; -RestWrite.prototype.handleAnonymousAuthData = function() { - var anonData = this.data.authData.anonymous; - if (anonData === null && this.query) { - // We are unlinking the user from the anonymous provider - this.data._auth_data_anonymous = null; - return; - } - - // Check if this user already exists - return this.config.database.find( - this.className, - {'authData.anonymous.id': anonData.id}, {}) - .then((results) => { - if (results.length > 0) { - if (!this.query) { - // We're signing up, but this user already exists. Short-circuit - delete results[0].password; - this.response = { - response: results[0], - location: this.location() - }; - return; - } - - // If this is a PUT for the same user, allow the linking - if (results[0].objectId === this.query.objectId) { - // Delete the rest format key before saving - delete this.data.authData; - return; - } - - // We're trying to create a duplicate account. Forbid it - throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED, - 'this auth is already used'); - } - - // This anonymous user does not already exist, so transform it - // to a saveable format - this.data._auth_data_anonymous = anonData; - - // Delete the rest format key before saving - delete this.data.authData; - }) - -}; - 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; @@ -298,7 +249,6 @@ RestWrite.prototype.handleOAuthAuthData = function(provider) { var validateAuthData; var validateAppId; - if (oauth[provider]) { validateAuthData = oauth[provider].validateAuthData; validateAppId = oauth[provider].validateAppId; @@ -343,37 +293,36 @@ RestWrite.prototype.handleOAuthAuthData = function(provider) { query, {}); }).then((results) => { this.storage['authProvider'] = provider; - if (results.length > 0) { - if (!this.query) { - // We're signing up, but this user already exists. Short-circuit - delete results[0].password; - this.response = { - response: results[0], - location: this.location() - }; - this.data.objectId = results[0].objectId; - return; - } - - // If this is a PUT for the same user, allow the linking - if (results[0].objectId === this.query.objectId) { - // Delete the rest format key before saving - delete this.data.authData; - return; - } - // We're trying to create a duplicate oauth auth. Forbid it - throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED, + + // 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 ]; + console.log("alerady linked!"); + throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED, 'this auth is already used'); + } } else { - this.data.username = cryptoUtils.newToken(); + + delete this.data["_auth_data_" + provider ]; + throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'THis should not be reached...'); } - - // This FB auth does not already exist, so transform it to a - // saveable format - this.data["_auth_data_" + provider ] = authData; - - // Delete the rest format key before saving - delete this.data.authData; }); } @@ -780,6 +729,10 @@ RestWrite.prototype.runDatabaseOperation = function() { if (this.data.ACL && this.data.ACL['*unresolved']) { throw new Parse.Error(Parse.Error.INVALID_ACL, 'Invalid ACL.'); } + + if (this.className === '_User') { + delete this.data.authData; + } if (this.query) { // Run an update diff --git a/src/oauth/index.js b/src/oauth/index.js index f39aea07cf..5067b4a137 100644 --- a/src/oauth/index.js +++ b/src/oauth/index.js @@ -13,5 +13,13 @@ module.exports = { instagram: instagram, linkedin: linkedin, meetup: meetup, - twitter: twitter + twitter: twitter, + anonymous: { + validateAuthData: function() { + return Promise.resolve(); + }, + validateAppId: function() { + return Promise.resolve(); + } + } } \ No newline at end of file From 54d154f7aa692da2dc07969774b90d061872a4a8 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 10 Mar 2016 09:41:56 -0500 Subject: [PATCH 2/5] Centralizes AuthData validation --- spec/RestCreate.spec.js | 3 +- spec/helper.js | 3 +- src/Config.js | 1 - src/RestWrite.js | 57 +++----------------- src/index.js | 6 +-- src/oauth/index.js | 113 ++++++++++++++++++++++++++++++++-------- 6 files changed, 104 insertions(+), 79 deletions(-) diff --git a/spec/RestCreate.spec.js b/spec/RestCreate.spec.js index f9b94b379e..f593e22178 100644 --- a/spec/RestCreate.spec.js +++ b/spec/RestCreate.spec.js @@ -148,7 +148,8 @@ describe('rest create', () => { }); it('handles no anonymous users config', (done) => { - var NoAnnonConfig = Object.assign({}, config, {enableAnonymousUsers: false}); + var NoAnnonConfig = Object.assign({}, config); + NoAnnonConfig.oauth.setEnableAnonymousUsers(false); var data1 = { authData: { anonymous: { diff --git a/spec/helper.js b/spec/helper.js index e2daa6ed25..8ed9fe2619 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -7,6 +7,7 @@ var DatabaseAdapter = require('../src/DatabaseAdapter'); var express = require('express'); var facebook = require('../src/oauth/facebook'); var ParseServer = require('../src/index').ParseServer; +var path = require('path'); var databaseURI = process.env.DATABASE_URI; var cloudMain = process.env.CLOUD_CODE_MAIN || '../spec/cloud/main.js'; @@ -36,7 +37,7 @@ var defaultConfiguration = { oauth: { // Override the facebook provider facebook: mockFacebook(), myoauth: { - module: "../spec/myoauth" // relative path as it's run from src + module: path.resolve(__dirname, "myoauth") // relative path as it's run from src } } }; diff --git a/src/Config.js b/src/Config.js index 8042d6db4d..f65b17d74f 100644 --- a/src/Config.js +++ b/src/Config.js @@ -20,7 +20,6 @@ export class Config { this.restAPIKey = cacheInfo.restAPIKey; this.fileKey = cacheInfo.fileKey; this.facebookAppIds = cacheInfo.facebookAppIds; - this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers; this.allowClientClassCreation = cacheInfo.allowClientClassCreation; this.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix); diff --git a/src/RestWrite.js b/src/RestWrite.js index f54d6a7385..b071997591 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -9,7 +9,6 @@ var Auth = require('./Auth'); var Config = require('./Config'); var cryptoUtils = require('./cryptoUtils'); var passwordCrypto = require('./password'); -var oauth = require("./oauth"); var Parse = require('parse/node'); var triggers = require('./triggers'); @@ -213,13 +212,7 @@ RestWrite.prototype.validateAuthData = function() { var authData = this.data.authData; var providers = Object.keys(authData); if (providers.length == 1) { - - var provider = providers[0]; - if (provider == 'anonymous' && !this.config.enableAnonymousUsers) { - throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE, - 'This authentication method is unsupported.'); - } - + var provider = providers[0]; var providerAuthData = authData[provider]; var hasToken = (providerAuthData && providerAuthData.id); if (providerAuthData === null || hasToken) { @@ -238,52 +231,15 @@ RestWrite.prototype.handleOAuthAuthData = function(provider) { return; } - var appIds; - var oauthOptions = this.config.oauth[provider]; - if (oauthOptions) { - appIds = oauthOptions.appIds; - } else if (provider == "facebook") { - appIds = this.config.facebookAppIds; - } + let validateAuthData = this.config.oauth.getValidatorForProvider(provider); - var validateAuthData; - var validateAppId; - - if (oauth[provider]) { - validateAuthData = oauth[provider].validateAuthData; - validateAppId = oauth[provider].validateAppId; - } - - // Try the configuration methods - if (oauthOptions) { - if (oauthOptions.module) { - validateAuthData = require(oauthOptions.module).validateAuthData; - validateAppId = require(oauthOptions.module).validateAppId; - }; - - if (oauthOptions.validateAuthData) { - validateAuthData = oauthOptions.validateAuthData; - } - if (oauthOptions.validateAppId) { - validateAppId = oauthOptions.validateAppId; - } - } - // try the custom provider first, fallback on the oauth implementation - - if (!validateAuthData || !validateAppId) { - return false; + if (!validateAuthData) { + throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE, + 'This authentication method is unsupported.'); }; - return validateAuthData(authData, oauthOptions) + return validateAuthData(authData) .then(() => { - if (appIds && typeof validateAppId === "function") { - return validateAppId(appIds, authData, oauthOptions); - } - - // No validation required by the developer - return Promise.resolve(); - - }).then(() => { // Check if this user already exists // TODO: does this handle re-linking correctly? var query = {}; @@ -314,7 +270,6 @@ RestWrite.prototype.handleOAuthAuthData = function(provider) { // are different if (results[0].objectId !== this.query.objectId) { delete this.data["_auth_data_" + provider ]; - console.log("alerady linked!"); throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED, 'this auth is already used'); } diff --git a/src/index.js b/src/index.js index 131f1f691a..5c53a413f2 100644 --- a/src/index.js +++ b/src/index.js @@ -8,7 +8,8 @@ var batch = require('./batch'), express = require('express'), middlewares = require('./middlewares'), multer = require('multer'), - Parse = require('parse/node').Parse; + Parse = require('parse/node').Parse, + oauthManager = require('./oauth'); //import passwordReset from './passwordReset'; import cache from './cache'; @@ -163,9 +164,8 @@ function ParseServer({ hooksController: hooksController, userController: userController, verifyUserEmails: verifyUserEmails, - enableAnonymousUsers: enableAnonymousUsers, allowClientClassCreation: allowClientClassCreation, - oauth: oauth, + oauth: oauthManager(oauth, enableAnonymousUsers), appName: appName, publicServerURL: publicServerURL, customPages: customPages, diff --git a/src/oauth/index.js b/src/oauth/index.js index 5067b4a137..04ef89717f 100644 --- a/src/oauth/index.js +++ b/src/oauth/index.js @@ -1,25 +1,94 @@ -var facebook = require('./facebook'); -var instagram = require("./instagram"); -var linkedin = require("./linkedin"); -var meetup = require("./meetup"); -var google = require("./google"); -var github = require("./github"); -var twitter = require("./twitter"); - -module.exports = { - facebook: facebook, - github: github, - google: google, - instagram: instagram, - linkedin: linkedin, - meetup: meetup, - twitter: twitter, - anonymous: { - validateAuthData: function() { - return Promise.resolve(); - }, - validateAppId: function() { - return Promise.resolve(); +let facebook = require('./facebook'); +let instagram = require("./instagram"); +let linkedin = require("./linkedin"); +let meetup = require("./meetup"); +let google = require("./google"); +let github = require("./github"); +let twitter = require("./twitter"); + +let anonymous = { + validateAuthData: () => { + return Promise.resolve(); + }, + validateAppId: () => { + return Promise.resolve(); + } +} + +let providers = { + facebook, + instagram, + linkedin, + meetup, + google, + github, + twitter, + anonymous +} + +module.exports = function(oauthOptions = {}, enableAnonymousUsers = true) { + let _enableAnonymousUsers = enableAnonymousUsers; + let setEnableAnonymousUsers = function(enable) { + _enableAnonymousUsers = enable; + } + // To handle the test cases on configuration + let getValidatorForProvider = function(provider) { + + if (provider === 'anonymous' && !_enableAnonymousUsers) { + return; + } + + let defaultProvider = providers[provider]; + let optionalProvider = oauthOptions[provider]; + + if (!defaultProvider && !optionalProvider) { + return; + } + + let appIds; + if (optionalProvider) { + appIds = optionalProvider.appIds; + } + + var validateAuthData; + var validateAppId; + + if (defaultProvider) { + validateAuthData = defaultProvider.validateAuthData; + validateAppId = defaultProvider.validateAppId; + } + + // Try the configuration methods + if (optionalProvider) { + if (optionalProvider.module) { + validateAuthData = require(optionalProvider.module).validateAuthData; + validateAppId = require(optionalProvider.module).validateAppId; + }; + + if (optionalProvider.validateAuthData) { + validateAuthData = optionalProvider.validateAuthData; + } + if (optionalProvider.validateAppId) { + validateAppId = optionalProvider.validateAppId; + } + } + + if (!validateAuthData || !validateAppId) { + return; + } + + return function(authData) { + return validateAuthData(authData, optionalProvider).then(() => { + if (appIds) { + return validateAppId(appIds, authData, optionalProvider); + } + return Promise.resolve(); + }) } } + + return Object.freeze({ + getValidatorForProvider, + setEnableAnonymousUsers, + }) } \ No newline at end of file From 9c5f14981e4c8625b061d92b8c2d38e09bd7a01e Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 10 Mar 2016 09:48:02 -0500 Subject: [PATCH 3/5] Renames oauth to authDataManager in src --- spec/OAuth.spec.js | 4 ++-- spec/RestCreate.spec.js | 2 +- spec/helper.js | 2 +- src/Config.js | 2 +- src/RestWrite.js | 2 +- src/{oauth => authDataManager}/OAuth1Client.js | 0 src/{oauth => authDataManager}/facebook.js | 0 src/{oauth => authDataManager}/github.js | 0 src/{oauth => authDataManager}/google.js | 0 src/{oauth => authDataManager}/index.js | 0 src/{oauth => authDataManager}/instagram.js | 0 src/{oauth => authDataManager}/linkedin.js | 0 src/{oauth => authDataManager}/meetup.js | 0 src/{oauth => authDataManager}/twitter.js | 0 src/index.js | 4 ++-- 15 files changed, 8 insertions(+), 8 deletions(-) rename src/{oauth => authDataManager}/OAuth1Client.js (100%) rename src/{oauth => authDataManager}/facebook.js (100%) rename src/{oauth => authDataManager}/github.js (100%) rename src/{oauth => authDataManager}/google.js (100%) rename src/{oauth => authDataManager}/index.js (100%) rename src/{oauth => authDataManager}/instagram.js (100%) rename src/{oauth => authDataManager}/linkedin.js (100%) rename src/{oauth => authDataManager}/meetup.js (100%) rename src/{oauth => authDataManager}/twitter.js (100%) diff --git a/spec/OAuth.spec.js b/spec/OAuth.spec.js index 47e4349d93..48f22aceb9 100644 --- a/spec/OAuth.spec.js +++ b/spec/OAuth.spec.js @@ -1,4 +1,4 @@ -var OAuth = require("../src/oauth/OAuth1Client"); +var OAuth = require("../src/authDataManager/OAuth1Client"); var request = require('request'); describe('OAuth', function() { @@ -138,7 +138,7 @@ describe('OAuth', function() { ["facebook", "github", "instagram", "google", "linkedin", "meetup", "twitter"].map(function(providerName){ it("Should validate structure of "+providerName, (done) => { - var provider = require("../src/oauth/"+providerName); + var provider = require("../src/authDataManager/"+providerName); jequal(typeof provider.validateAuthData, "function"); jequal(typeof provider.validateAppId, "function"); jequal(provider.validateAuthData({}, {}).constructor, Promise.prototype.constructor); diff --git a/spec/RestCreate.spec.js b/spec/RestCreate.spec.js index f593e22178..7af4e346f5 100644 --- a/spec/RestCreate.spec.js +++ b/spec/RestCreate.spec.js @@ -149,7 +149,7 @@ describe('rest create', () => { it('handles no anonymous users config', (done) => { var NoAnnonConfig = Object.assign({}, config); - NoAnnonConfig.oauth.setEnableAnonymousUsers(false); + NoAnnonConfig.authDataManager.setEnableAnonymousUsers(false); var data1 = { authData: { anonymous: { diff --git a/spec/helper.js b/spec/helper.js index 8ed9fe2619..6c7c94144b 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -5,7 +5,7 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000; var cache = require('../src/cache').default; var DatabaseAdapter = require('../src/DatabaseAdapter'); var express = require('express'); -var facebook = require('../src/oauth/facebook'); +var facebook = require('../src/authDataManager/facebook'); var ParseServer = require('../src/index').ParseServer; var path = require('path'); diff --git a/src/Config.js b/src/Config.js index f65b17d74f..e80c3b2872 100644 --- a/src/Config.js +++ b/src/Config.js @@ -33,7 +33,7 @@ export class Config { this.pushController = cacheInfo.pushController; this.loggerController = cacheInfo.loggerController; this.userController = cacheInfo.userController; - this.oauth = cacheInfo.oauth; + this.authDataManager = cacheInfo.authDataManager; this.customPages = cacheInfo.customPages || {}; this.mount = mount; } diff --git a/src/RestWrite.js b/src/RestWrite.js index b071997591..d51d61ad23 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -231,7 +231,7 @@ RestWrite.prototype.handleOAuthAuthData = function(provider) { return; } - let validateAuthData = this.config.oauth.getValidatorForProvider(provider); + let validateAuthData = this.config.authDataManager.getValidatorForProvider(provider); if (!validateAuthData) { throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE, diff --git a/src/oauth/OAuth1Client.js b/src/authDataManager/OAuth1Client.js similarity index 100% rename from src/oauth/OAuth1Client.js rename to src/authDataManager/OAuth1Client.js diff --git a/src/oauth/facebook.js b/src/authDataManager/facebook.js similarity index 100% rename from src/oauth/facebook.js rename to src/authDataManager/facebook.js diff --git a/src/oauth/github.js b/src/authDataManager/github.js similarity index 100% rename from src/oauth/github.js rename to src/authDataManager/github.js diff --git a/src/oauth/google.js b/src/authDataManager/google.js similarity index 100% rename from src/oauth/google.js rename to src/authDataManager/google.js diff --git a/src/oauth/index.js b/src/authDataManager/index.js similarity index 100% rename from src/oauth/index.js rename to src/authDataManager/index.js diff --git a/src/oauth/instagram.js b/src/authDataManager/instagram.js similarity index 100% rename from src/oauth/instagram.js rename to src/authDataManager/instagram.js diff --git a/src/oauth/linkedin.js b/src/authDataManager/linkedin.js similarity index 100% rename from src/oauth/linkedin.js rename to src/authDataManager/linkedin.js diff --git a/src/oauth/meetup.js b/src/authDataManager/meetup.js similarity index 100% rename from src/oauth/meetup.js rename to src/authDataManager/meetup.js diff --git a/src/oauth/twitter.js b/src/authDataManager/twitter.js similarity index 100% rename from src/oauth/twitter.js rename to src/authDataManager/twitter.js diff --git a/src/index.js b/src/index.js index 5c53a413f2..d496d161c5 100644 --- a/src/index.js +++ b/src/index.js @@ -9,7 +9,7 @@ var batch = require('./batch'), middlewares = require('./middlewares'), multer = require('multer'), Parse = require('parse/node').Parse, - oauthManager = require('./oauth'); + authDataManager = require('./authDataManager'); //import passwordReset from './passwordReset'; import cache from './cache'; @@ -165,7 +165,7 @@ function ParseServer({ userController: userController, verifyUserEmails: verifyUserEmails, allowClientClassCreation: allowClientClassCreation, - oauth: oauthManager(oauth, enableAnonymousUsers), + authDataManager: authDataManager(oauth, enableAnonymousUsers), appName: appName, publicServerURL: publicServerURL, customPages: customPages, From bcffcbade267850280707d8c20dd36ad2a39c595 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 10 Mar 2016 18:59:19 -0500 Subject: [PATCH 4/5] Fix when multiple authData keys are passed --- spec/ParseUser.spec.js | 152 ++++++++++++++++++++++++++++++++++++++++ spec/RestCreate.spec.js | 3 +- src/RestWrite.js | 136 ++++++++++++++++++++--------------- 3 files changed, 234 insertions(+), 57 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..a31c8e5baf 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,98 @@ 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.handleAuthDataValidation(authData).then(() => { + 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 { - - delete this.data["_auth_data_" + provider ]; - throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'THis should not be reached...'); - } + } + // 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 From daad05a00f890630203b9e2685e3c3d55b56d38f Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 11 Mar 2016 14:50:33 -0500 Subject: [PATCH 5/5] removes key transformation for authData from restWrite, ensures authData is set in hooks --- spec/ParseUser.spec.js | 37 ++++++++++++++++++++++++++++++++++++ spec/RestCreate.spec.js | 2 +- src/RestWrite.js | 25 +++++++++--------------- src/authDataManager/index.js | 2 +- src/transform.js | 19 +++++++++++++++++- 5 files changed, 66 insertions(+), 19 deletions(-) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 50f29331ee..1776ad56c1 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -1436,6 +1436,43 @@ describe('Parse.User testing', () => { } }); }); + + it('should have authData in beforeSave and afterSave', (done) => { + + Parse.Cloud.beforeSave('_User', (request, response) => { + let authData = request.object.get('authData'); + expect(authData).not.toBeUndefined(); + if (authData) { + expect(authData.facebook.id).toEqual('8675309'); + expect(authData.facebook.access_token).toEqual('jenny'); + } else { + fail('authData should be set'); + } + response.success(); + }); + + Parse.Cloud.afterSave('_User', (request, response) => { + let authData = request.object.get('authData'); + expect(authData).not.toBeUndefined(); + if (authData) { + expect(authData.facebook.id).toEqual('8675309'); + expect(authData.facebook.access_token).toEqual('jenny'); + } else { + fail('authData should be set'); + } + response.success(); + }); + + var provider = getMockFacebookProvider(); + Parse.User._registerAuthenticationProvider(provider); + Parse.User._logInWith("facebook", { + success: function(model) { + Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className); + Parse.Cloud._removeHook('Triggers', 'afterSave', Parse.User.className); + 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 553d37ad84..d07e18ed0e 100644 --- a/spec/RestCreate.spec.js +++ b/spec/RestCreate.spec.js @@ -200,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 a31c8e5baf..c2be50af47 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -212,10 +212,12 @@ RestWrite.prototype.validateAuthData = function() { var authData = this.data.authData; var providers = Object.keys(authData); if (providers.length > 0) { - var provider = providers[providers.length-1]; - var providerAuthData = authData[provider]; - var hasToken = (providerAuthData && providerAuthData.id); - if (providerAuthData === null || hasToken) { + let canHandleAuthData = providers.reduce((canHandle, provider) => { + var providerAuthData = authData[provider]; + var hasToken = (providerAuthData && providerAuthData.id); + return canHandle && (hasToken || providerAuthData == null); + }, true); + if (canHandleAuthData) { return this.handleAuthData(authData); } } @@ -274,11 +276,9 @@ RestWrite.prototype.handleAuthData = function(authData) { throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED, 'this auth is already used'); } - // set the proper keys - Object.keys(authData).forEach((provider) => { - this.data[`_auth_data_${provider}`] = authData[provider]; - }); - + + this.storage['authProvider'] = Object.keys(authData).join(','); + if (results.length == 0) { this.data.username = cryptoUtils.newToken(); } else if (!this.query) { @@ -294,9 +294,6 @@ RestWrite.prototype.handleAuthData = function(authData) { // 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'); } @@ -708,10 +705,6 @@ RestWrite.prototype.runDatabaseOperation = function() { if (this.data.ACL && this.data.ACL['*unresolved']) { throw new Parse.Error(Parse.Error.INVALID_ACL, 'Invalid ACL.'); } - - if (this.className === '_User') { - delete this.data.authData; - } if (this.query) { // Run an update diff --git a/src/authDataManager/index.js b/src/authDataManager/index.js index 04ef89717f..77ee7473ea 100644 --- a/src/authDataManager/index.js +++ b/src/authDataManager/index.js @@ -91,4 +91,4 @@ module.exports = function(oauthOptions = {}, enableAnonymousUsers = true) { getValidatorForProvider, setEnableAnonymousUsers, }) -} \ No newline at end of file +} diff --git a/src/transform.js b/src/transform.js index 738f245365..aae5cc2af7 100644 --- a/src/transform.js +++ b/src/transform.js @@ -87,7 +87,7 @@ export function transformKeyValue(schema, className, restKey, restValue, options return transformWhere(schema, className, s); }); return {key: '$and', value: mongoSubqueries}; - default: + default: // Other auth data var authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/); if (authDataMatch) { @@ -203,6 +203,9 @@ function transformWhere(schema, className, restWhere) { // restCreate is the "create" clause in REST API form. // Returns the mongo form of the object. function transformCreate(schema, className, restCreate) { + if (className == '_User') { + restCreate = transformAuthData(restCreate); + } var mongoCreate = transformACL(restCreate); for (var restKey in restCreate) { var out = transformKeyValue(schema, className, restKey, restCreate[restKey]); @@ -218,6 +221,10 @@ function transformUpdate(schema, className, restUpdate) { if (!restUpdate) { throw 'got empty restUpdate'; } + if (className == '_User') { + restUpdate = transformAuthData(restUpdate); + } + var mongoUpdate = {}; var acl = transformACL(restUpdate); if (acl._rperm || acl._wperm) { @@ -250,6 +257,16 @@ function transformUpdate(schema, className, restUpdate) { return mongoUpdate; } +function transformAuthData(restObject) { + if (restObject.authData) { + Object.keys(restObject.authData).forEach((provider) => { + restObject[`_auth_data_${provider}`] = restObject.authData[provider]; + }); + delete restObject.authData; + } + return restObject; +} + // Transforms a REST API formatted ACL object to our two-field mongo format. // This mutates the restObject passed in to remove the ACL key. function transformACL(restObject) {