Fix when multiple authData keys are passed

This commit is contained in:
Florent Vilmart
2016-03-10 18:59:19 -05:00
parent 9c5f14981e
commit bcffcbade2
3 changed files with 234 additions and 57 deletions

View File

@@ -905,6 +905,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({ var ExtendedUser = Parse.User.extend({
extended: function() { extended: function() {
return true; return true;
@@ -1285,6 +1329,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) => { it('set password then change password', (done) => {
Parse.User.signUp('bob', 'barker').then((bob) => { Parse.User.signUp('bob', 'barker').then((bob) => {
bob.setPassword('meower'); bob.setPassword('meower');

View File

@@ -163,6 +163,7 @@ describe('rest create', () => {
}, (err) => { }, (err) => {
expect(err.code).toEqual(Parse.Error.UNSUPPORTED_SERVICE); expect(err.code).toEqual(Parse.Error.UNSUPPORTED_SERVICE);
expect(err.message).toEqual('This authentication method is unsupported.'); expect(err.message).toEqual('This authentication method is unsupported.');
NoAnnonConfig.authDataManager.setEnableAnonymousUsers(true);
done(); done();
}) })
}); });

View File

@@ -211,74 +211,98 @@ RestWrite.prototype.validateAuthData = function() {
var authData = this.data.authData; var authData = this.data.authData;
var providers = Object.keys(authData); var providers = Object.keys(authData);
if (providers.length == 1) { if (providers.length > 0) {
var provider = providers[0]; var provider = providers[providers.length-1];
var providerAuthData = authData[provider]; var providerAuthData = authData[provider];
var hasToken = (providerAuthData && providerAuthData.id); var hasToken = (providerAuthData && providerAuthData.id);
if (providerAuthData === null || hasToken) { if (providerAuthData === null || hasToken) {
return this.handleOAuthAuthData(provider); return this.handleAuthData(authData);
} }
} }
throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE, throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE,
'This authentication method is unsupported.'); 'This authentication method is unsupported.');
}; };
RestWrite.prototype.handleOAuthAuthData = function(provider) { RestWrite.prototype.handleAuthDataValidation = function(authData) {
var authData = this.data.authData[provider]; let validations = Object.keys(authData).map((provider) => {
if (authData === null && this.query) { if (authData[provider] === null) {
// We are unlinking from the provider. return Promise.resolve();
this.data["_auth_data_" + provider ] = null; }
return; 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);
}
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,
{'$or': query}, {})
} }
let validateAuthData = this.config.authDataManager.getValidatorForProvider(provider); return findPromise;
}
if (!validateAuthData) { RestWrite.prototype.handleAuthData = function(authData) {
throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE, let results;
'This authentication method is unsupported.'); return this.handleAuthDataValidation(authData).then(() => {
}; return this.findUsersWithAuthData(authData);
}).then((r) => {
return validateAuthData(authData) results = r;
.then(() => { if (results.length > 1) {
// Check if this user already exists // More than 1 user with the passed id's
// TODO: does this handle re-linking correctly? throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED,
var query = {};
query['authData.' + provider + '.id'] = authData.id;
return 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,
'this auth is already used'); 'this auth is already used');
} }
} else { // set the proper keys
Object.keys(authData).forEach((provider) => {
delete this.data["_auth_data_" + provider ]; this.data[`_auth_data_${provider}`] = authData[provider];
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'THis should not be reached...');
}
}); });
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 // The non-third-party parts of User transformation