Add Facebook/Google credentials to existing user account

2,450 views
Skip to first unread message

Dimitri Fischler

unread,
Apr 8, 2013, 11:45:06 AM4/8/13
to meteo...@googlegroups.com
I'd like to give the option to my users to link their Facebook or Google accounts to their existing user account on my app. So instead of creating a new account when the user logs in with Facebook or Google, I should check if the related email address already exists, and if so add the FB/G credentials to the profile of that user account (and log him/her in). Otherwise create a new account the usual way.

Any idea how I would go about achieving this?

Gabriel Pugliese

unread,
Apr 8, 2013, 11:57:05 AM4/8/13
to meteo...@googlegroups.com
You need to use Accounts.loginWithExternalService to achieve that:

--

Gabriel Pugliese
@gabrielsapo
+55 11 9-9374-2720
http://www.blogcloud.com.br


On Mon, Apr 8, 2013 at 12:45 PM, Dimitri Fischler <dimf...@gmail.com> wrote:
I'd like to give the option to my users to link their Facebook or Google accounts to their existing user account on my app. So instead of creating a new account when the user logs in with Facebook or Google, I should check if the related email address already exists, and if so add the FB/G credentials to the profile of that user account (and log him/her in). Otherwise create a new account the usual way.

Any idea how I would go about achieving this?

--
You received this message because you are subscribed to the Google Groups "meteor-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to meteor-talk...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Dimitri Fischler

unread,
Apr 8, 2013, 12:08:35 PM4/8/13
to meteo...@googlegroups.com
I thought about doing that, but I'm not sure how.

Here's one attempt:
  • in the callback, look for a user with an email address equal to the email address provided by the third party service
  • if one exists, copy all the third party service credentials to that account, and delete the newly created account
  • if not, don't do anything (i.e. let the new account be created)
What do you think?

Ken Yee

unread,
Apr 8, 2013, 12:13:30 PM4/8/13
to meteo...@googlegroups.com


On Monday, April 8, 2013 11:45:06 AM UTC-4, Dimitri Fischler wrote:
I'd like to give the option to my users to link their Facebook or Google accounts to their existing user account on my app. So instead of creating a new account when the user logs in with Facebook or Google, I should check if the related email address already exists, and if so add the FB/G credentials to the profile of that user account (and log him/her in). Otherwise create a new account the usual way.


Have you tried hooking the Accounts.validateNewUser?
You can at least validate the user there...not sure if FB email info is available there though...

Avital Oliver

unread,
Apr 8, 2013, 12:16:22 PM4/8/13
to meteo...@googlegroups.com
Hi Dmitri,

This is definitely something that has been requested many times, and Meteor is set up to support this (each user document can have multiple subdocuments on user.services.{facebook,google,...}). But at the moment there is no way to do this easily. If you read through the source code and tinker enough it should be possible though.

If you'd like, you can start looking around at packages/accounts-oauth2-helpers/oauth2_server.js. You'll see that when we complete the OAuth2 handshake, we call Accounts.updateOrCreateUserFromExternalService. You'll want to do something different in that case.


On Mon, Apr 8, 2013 at 8:45 AM, Dimitri Fischler <dimf...@gmail.com> wrote:
I'd like to give the option to my users to link their Facebook or Google accounts to their existing user account on my app. So instead of creating a new account when the user logs in with Facebook or Google, I should check if the related email address already exists, and if so add the FB/G credentials to the profile of that user account (and log him/her in). Otherwise create a new account the usual way.

Any idea how I would go about achieving this?

--

Gabriel Pugliese

unread,
Apr 8, 2013, 1:37:40 PM4/8/13
to meteo...@googlegroups.com
I don't think you must match e-mails. It will not work for everyone.
But now I'm thinking about what Avital said. You just can't do it in client side, because Meteor.users are not published entirely.
I would try to do a method on the server that updates the user (querying by Meteor.userId()) and inserts the service Object into the doc. That could be the callback for loginWithExternalService.

What do you think Avital ? Would that work if the user logs out and then logs in with another provider ?

--

Gabriel Pugliese
@gabrielsapo
+55 11 9-9374-2720
http://www.blogcloud.com.br


Dimitri Fischler

unread,
Apr 10, 2013, 11:50:11 AM4/10/13
to meteo...@googlegroups.com
@Ken
I have tried validateNewUser and also onCreateUser
Problem is that I'd like to not log the user in with this new account and instead log him/her in with the existing account, having added the third-party credentials to the existing user document. Not sure if that can be achieved within these functions.

@Gabriel
Yes it should definitely happen server-side.

@Avital
I've started looking at the code of the Accounts.updateOrCreateUserFromExternalService function in packages/accounts-base/accounts_server.js. It looks like what's happening in the if (user) {} clause is exactly what I want, except it's just looking for an existing account created from the same third-party service, and it only processes the token. In my case I would additionally want to look for users with an email in their emails array equal to the newly received services.facebook/google.email, and also deal with the options, not just the token. So far, so clear? Let me know what you think, and if/how/where it would be possible to implement this.

Another option I'm thinking of is to merge the user documents after log in. I'm sure it can be done easily with MongoDB, but I'm wondering how to deal with logging the user out and back in with the "merged" account...

Dimitri Fischler

unread,
Apr 11, 2013, 4:31:41 AM4/11/13
to meteo...@googlegroups.com
Within Accounts.validateNewUser, is the new user document already inserted in Meteor.users or not yet?

Dimitri Fischler

unread,
Apr 11, 2013, 9:25:51 AM4/11/13
to meteo...@googlegroups.com
I solved it. But it's a bit of a hack...
Remember, this is for a user who already has a "usual" account and now wants to connect with either Google or Facebook, or both. The code links those third-party credentials to the existing account. The only remaining issue is that the user can't be logged in automatically to the other account (as this happens on the server). That's why I've added the Meteor.Error, which displays the message to the user in the login box provided with accounts-ui.
Any comments/suggestions, please let me know.

Accounts.validateNewUser(function (user) {
  var service = user.services.google || user.services.facebook;

  if (! service)
    return true;

  var email = service.email;

  var existingUser = Meteor.users.findOne({$or: [{'emails.address': email}, {'services.google.email': email}, {'services.facebook.email': email}]});

  if (! existingUser)
    return true;

    Meteor.users.update({_id: existingUser._id}, {
      $set: {
        'profile': user.profile,
      }, 
      $push: {
        'services.resume.loginTokens': user.services.resume.loginTokens[0]
      }
    });
  } else {
    Meteor.users.update({_id: existingUser._id}, {
      $set: {
        'profile': user.profile,
        'services.facebook': user.services.facebook
      }, 
      $push: {
        'services.resume.loginTokens': user.services.resume.loginTokens[0]
      }
    });    
  };

  throw new Meteor.Error(205, "Merged with your existing Patio account. Try again, it'll work now.");
});

Ken Yee

unread,
Apr 11, 2013, 11:19:39 AM4/11/13
to meteo...@googlegroups.com

On Thursday, April 11, 2013 9:25:51 AM UTC-4, Dimitri Fischler wrote:
The only remaining issue is that the user can't be logged in automatically to the other account

Only thing I can think of for doing this is to force the user onto a page/url that redirects to the root URL via a bit of javascript in the .rendered event of the page (assuming you're using mini-pages).
That would screw up your Meteor.throw hack though...not sure what would happen if you just return false.

It's hackish :-P

Dimitri Fischler

unread,
Apr 11, 2013, 11:29:51 AM4/11/13
to meteo...@googlegroups.com
Same thing, without the error message.


--
You received this message because you are subscribed to a topic in the Google Groups "meteor-talk" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/meteor-talk/GedMfxVdohQ/unsubscribe?hl=en.
To unsubscribe from this group and all its topics, send an email to meteor-talk...@googlegroups.com.

Ken Yee

unread,
Apr 11, 2013, 2:39:21 PM4/11/13
to meteo...@googlegroups.com


On Thursday, April 11, 2013 11:29:51 AM UTC-4, Dimitri Fischler wrote:
Same thing, without the error message.

Interesting...I know there are Meteor login tokens in the user object. 
I just assumed they were cookies, but looking at the network captures, there's no evidence of cookies, so I'm puzzled :-P
 

Theodore Blackman

unread,
Apr 12, 2013, 10:16:30 AM4/12/13
to meteo...@googlegroups.com
Meteor stores user login data on the client in localStorage with the keys 'Meteor.userId' and 'Meteor.loginToken'

Ken Yee

unread,
Apr 12, 2013, 7:14:40 PM4/12/13
to meteo...@googlegroups.com


On Friday, April 12, 2013 10:16:30 AM UTC-4, Theodore Blackman wrote:
Meteor stores user login data on the client in localStorage with the keys 'Meteor.userId' and 'Meteor.loginToken'

Thanks for the hint Theodore.
Two quick questions:
 - how does that data get down to the client?
 - how does it get back to the server?
I don't see it going over as cookies.  I do see what's probably that info going over as XHR POSTs though...looks like they go over as cookies then.
I guess I'm wondering if Dimitri's problem can be solved by redirecting to a page w/ some javascript that tucks that info into the localStorage, then redirects back to the site root.  On other app servers, you can do this by mucking w/ cookies but I've never seen this localStorage usage :-)

Theodore Blackman

unread,
Apr 13, 2013, 1:53:29 AM4/13/13
to meteo...@googlegroups.com
I don't know the details of how it gets sent to the client, sorry.  I imagine it's in one of the "accounts" packages in the Meteor source.


Gadi Cohen

unread,
May 22, 2013, 9:33:28 AM5/22/13
to meteo...@googlegroups.com
Sorry for joining the party late.  I also have a kind of ugly hack work around for this, but it's seamless on the user's side: they click the button and log in first time with their existing account as expected.  On the server side, the hack works by (unfortunately) removing and then reinserting the augmented user record.  See the onCreateUser docs for how this works and why the latter hack is necessary.  On the server:

Accounts.onCreateUser(function(options, user) {
if (user.services) {
var service = _.keys(user.services)[0];
var email = user.services[service].email;

// see if any existing user has this email address, otherwise create new
var existingUser = Meteor.users.findOne({'emails.address': email});
if (!existingUser)
return user;

// precaution, these will exist from accounts-password if used
if (!existingUser.services)
existingUser.services = { resume: { loginTokens: [] }};

// copy accross new service info
existingUser.services[service] = user.services[service];
existingUser.services.resume.loginTokens.push(
user.services.resume.loginTokens[0]
);

// even worse hackery
Meteor.users.remove({_id: existingUser._id}); // remove existing record
return existingUser;       // record is re-inserted
}
});

This has been tested to work with Facebook and Google; in theory it should work with any accounts package which provides a single email address.  If you're using a service that provides multiple e-mail addresses, you'll need to modify the code accordingly.

David Glasser

unread,
May 22, 2013, 1:25:34 PM5/22/13
to meteo...@googlegroups.com
On devel, Tim H has refactored the OAuth code out of the
Meteor.users-specific functions, so there is a facebook and a google
package (etc) that does this stuff directly in addition to
accounts-facebook and accounts-google which interact with
Meteor.users.
> --
> You received this message because you are subscribed to the Google Groups
> "meteor-talk" group.
> To unsubscribe from this group and stop receiving emails from it, send an

Elliott Spira

unread,
Oct 8, 2014, 12:29:24 AM10/8/14
to meteo...@googlegroups.com
Hey all,

Thought I'd do an add to this item despite it being quite an old post.

I think Gadi Cohen's suggestion is OK, but if for some reason the subsequent write fails, the entire user record is lost.

I had a poke around the core accounts-base code and found a cleaner way to guarantee that if a user registered by email address logs in with a service with a matching email address, it writes this service data to his/her existing user record rather than creating a new one.

Adding a couple of lines to the method 'Accounts.updateOrCreateUserFromExternalService' we can modify the selector that checks whether the user exists in the database.

///
/// MANAGING USER OBJECTS
///

// Updates or creates a user after we authenticate with a 3rd party.
//
// @param serviceName {String} Service name (eg, twitter).
// @param serviceData {Object} Data to store in the user's record
//        under services[serviceName]. Must include an "id" field
//        which is a unique identifier for the user in the service.
// @param options {Object, optional} Other options to pass to insertUserDoc
//        (eg, profile)
// @returns {Object} Object with token and id keys, like the result
//        of the "login" method.
//
Accounts.updateOrCreateUserFromExternalService = function(
  serviceName, serviceData, options) {
  options = _.clone(options || {});

  if (serviceName === "password" || serviceName === "resume")
    throw new Error(
      "Can't use updateOrCreateUserFromExternalService with internal service "
        + serviceName);
  if (!_.has(serviceData, 'id'))
    throw new Error(
      "Service data for service " + serviceName + " must include id");

  // Look for a user with the appropriate service user id.
  var selector = {};
  var serviceIdKey = "services." + serviceName + ".id";

  // XXX Temporary special case for Twitter. (Issue #629)
  //   The serviceData.id will be a string representation of an integer.
  //   We want it to match either a stored string or int representation.
  //   This is to cater to earlier versions of Meteor storing twitter
  //   user IDs in number form, and recent versions storing them as strings.
  //   This can be removed once migration technology is in place, and twitter
  //   users stored with integer IDs have been migrated to string IDs.
  if (serviceName === "twitter" && !isNaN(serviceData.id)) {
    selector["$or"] = [{},{},{}];
    selector["$or"][0][serviceIdKey] = serviceData.id;
    selector["$or"][1][serviceIdKey] = parseInt(serviceData.id, 10);
    selector["$or"][2]["emails.address"] = serviceData.email; 
  } else {
    selector["$or"] = [{},{}]
    selector["$or"][0][serviceIdKey] = serviceData.id;
    selector["$or"][1]["emails.address"] = serviceData.email; 
  }

  var user = Meteor.users.findOne(selector);

  if (user) {
    pinEncryptedFieldsToUser(serviceData, user._id);

    // We *don't* process options (eg, profile) for update, but we do replace
    // the serviceData (eg, so that we keep an unexpired access token and
    // don't cache old email addresses in serviceData.email).
    // XXX provide an onUpdateUser hook which would let apps update
    //     the profile too
    var setAttrs = {};
    _.each(serviceData, function(value, key) {
      setAttrs["services." + serviceName + "." + key] = value;
    });

    // XXX Maybe we should re-use the selector above and notice if the update
    //     touches nothing?
    Meteor.users.update(user._id, {$set: setAttrs});
    return {
      type: serviceName,
      userId: user._id
    };
  } else {
    // Create a new user with the service data. Pass other options through to
    // insertUserDoc.
    user = {services: {}};
    user.services[serviceName] = serviceData;
    return {
      type: serviceName,
      userId: Accounts.insertUserDoc(options, user)
    };
  }
};

var OAuthEncryption = Package["oauth-encryption"] && Package["oauth-encryption"].OAuthEncryption;

// OAuth service data is temporarily stored in the pending credentials
// collection during the oauth authentication process.  Sensitive data
// such as access tokens are encrypted without the user id because
// we don't know the user id yet.  We re-encrypt these fields with the
// user id included when storing the service data permanently in
// the users collection.
//
var pinEncryptedFieldsToUser = function (serviceData, userId) {
  _.each(_.keys(serviceData), function (key) {
    var value = serviceData[key];
    if (OAuthEncryption && OAuthEncryption.isSealed(value))
      value = OAuthEncryption.seal(OAuthEncryption.open(value), userId);
    serviceData[key] = value;
  });
};



I think this (or something similar - perhaps checking the email is verified, or making this a configurable option) should be included in the core accounts-base code.

Thoughts?

Elliott

Luca Mussi (@splendido)

unread,
Dec 21, 2014, 3:13:55 AM12/21/14
to meteo...@googlegroups.com
I also originally miss this one...

A bit of time ago I wrote accounts-meld which seems working quite fine. It also plays with the above mentioned updateOrCreateUserFromExternalService and verifying 'verified' emails.
This is basically what Elliot is proposing.

...I also have plans to integrate this into useraccounts
Any feedback will be very welcome!

Luca
Reply all
Reply to author
Forward
0 new messages