Skip to content

Commit

Permalink
Fix when multiple authData keys are passed
Browse files Browse the repository at this point in the history
  • Loading branch information
flovilmart committed Mar 11, 2016
1 parent 9c5f149 commit b09a388
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 55 deletions.
152 changes: 152 additions & 0 deletions spec/ParseUser.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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) => {
Expand Down
3 changes: 2 additions & 1 deletion spec/RestCreate.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
})
});
Expand Down Expand Up @@ -199,7 +200,7 @@ describe('rest create', () => {
done();
});
});

it('stores pointers with a _p_ prefix', (done) => {
var obj = {
foo: 'bar',
Expand Down
151 changes: 97 additions & 54 deletions src/RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit b09a388

Please sign in to comment.