Search code examples
javascriptparse-platformauthenticationparse-cloud-codegoogle-signin

Linking Google account with existing account created using email in Parse.com


I have implemented google login in parse. Here is my code:

var querystring = require('querystring');
var _ = require('underscore');
var Buffer = require('buffer').Buffer;

var googleValidateEndpoint = 'https://www.googleapis.com/oauth2/v1/userinfo';
var TokenStorage = Parse.Object.extend("TokenStorage");

var restrictedAcl = new Parse.ACL();
restrictedAcl.setPublicReadAccess(false);
restrictedAcl.setPublicWriteAccess(false);

Parse.Cloud.define('accessGoogleUser', function(req, res) {
  var data = req.params;
  var token = data.code;
  /**
   * Validate that code and state have been passed in as query parameters.
   * Render an error page if this is invalid.
   */
  if (!(data && data.code)) {
    res.error('Invalid auth response received.');
    return;
  }
  Parse.Cloud.useMasterKey();
  Parse.Promise.as().then(function() {
    // Validate & Exchange the code parameter for an access token from Google
    return getGoogleAccessToken(data.code);
  }).then(function(httpResponse) {
    var userData = httpResponse.data;
    if (userData && userData.id) {
      return upsertGoogleUser(token, userData, data.email);
    } else {
      return Parse.Promise.error("Unable to parse Google data");
    }
  }).then(function(user) {
    /**
     * Send back the session token in the response to be used with 'become/becomeInBackground' functions
     */
    res.success(user.getSessionToken());
  }, function(error) {
    /**
     * If the error is an object error (e.g. from a Parse function) convert it
     *   to a string for display to the user.
     */
    if (error && error.code && error.error) {
      error = error.code + ' ' + error.error;
    }
    res.error(JSON.stringify(error));
  });

});


var getGoogleAccessToken = function(code) {
  var body = querystring.stringify({
    access_token: code
  });
  return Parse.Cloud.httpRequest({
    url: googleValidateEndpoint + '?access_token=' + code
  });
}


var upsertGoogleUser = function(accessToken, googleData, emailId) {
  var query = new Parse.Query(TokenStorage);
  query.equalTo('accountId', googleData.id);
  //query.ascending('createdAt');
  // Check if this googleId has previously logged in, using the master key
  return query.first({ useMasterKey: true }).then(function(tokenData) {
    // If not, create a new user.
    if (!tokenData) {
      return newGoogleUser(accessToken, googleData, emailId);
    }
    // If found, fetch the user.
    var user = tokenData.get('user');
    return user.fetch({ useMasterKey: true }).then(function(user) {
      // Update the access_token if it is different.
      if (accessToken !== tokenData.get('accessToken')) {
        tokenData.set('accessToken', accessToken);
      }
      /**
       * This save will not use an API request if the token was not changed.
       * e.g. when a new user is created and upsert is called again.
       */
      return tokenData.save(null, { useMasterKey: true });
    }).then(function(obj) {
      // Reset password
      password = new Buffer(24);
      _.times(24, function(i) {
          password.set(i, _.random(0, 255));
      });
      password = password.toString('base64')
      user.setPassword(password);
      return user.save();
    }).then(function(user) {
      // ReLogin  
      // This line is what I am talking about
      return Parse.User.logIn(user.get('username'), password);  
    }).then(function(obj) {
      // Return the user object.
      return Parse.Promise.as(obj);
    });
  });
}



var newGoogleUser = function(accessToken, googleData, email) {
  var user = new Parse.User();
  // Generate a random username and password.
  var username = new Buffer(24);
  var password = new Buffer(24);
  _.times(24, function(i) {
    username.set(i, _.random(0, 255));
    password.set(i, _.random(0, 255));
  });
  var name = googleData.name;
  // name = name.split(" ");
  // var fullname = name;
  // if(name.length > 1)
  // var lastName = name[name.length-1];
  user.set("username", username.toString('base64'));
  user.set("password", password.toString('base64'));
  user.set("email", email);
  user.set("fullName", name);
  // user.set("last_name", lastName);
  user.set("accountType", 'google');
  // Sign up the new User
  return user.signUp().then(function(user) {
    // create a new TokenStorage object to store the user+Google association.
    var ts = new TokenStorage();
    ts.set('user', user);
    ts.set('accountId', googleData.id);
    ts.set('accessToken', accessToken);
    ts.setACL(restrictedAcl);
    // Use the master key because TokenStorage objects should be protected.
    return ts.save(null, { useMasterKey: true });
  }).then(function(tokenStorage) {
    return upsertGoogleUser(accessToken, googleData);
  });
}

It works perfectly fine. Now the problem I am facing is that I want to link google account with an existing parse account created using email or username & password. The problem in doing so is that to login/signup using google I have to reset the password of the user to login so as to get the session token. See this line in the code -> [This line is what I am talking about]. So if I do so an existing user who had earlier used username/email & password to login won't be able to login again using email since I have reset his/her password. I have seen this and all the other links related to this but none of which solves this problem.

Can somebody here guide me in the right direction?

Log added as response to one of the comments:

{"accountType":"google","createdAt":"2016-01-07T17:30:57.429Z","email":"skdkaney@gmail.com","fullName":"ashdakhs basdkbney","updatedAt":"2016-01-07T17:30:57.429Z","username":"owt3h0ZZEZQ1K7if55W2oo3TBLfeWM6m","objectId":"lSlsdsZ9"}

Added upsert function as per comment request:

  var upsertGoogleUser = function(accessToken, googleData, emailId) {
  var query = new Parse.Query(TokenStorage);
  query.equalTo('accountId', googleData.id);
  //query.ascending('createdAt');
  // Check if this googleId has previously logged in, using the master key
  return query.first({ useMasterKey: true }).then(function(tokenData) {
    // If not, create a new user.
    if (!tokenData) {
      return newGoogleUser(accessToken, googleData, emailId);
    }
    // If found, fetch the user.
    var userw = tokenData.get('user');
    var users_id = userw.id;

    var query2 = new Parse.Query(Parse.User);
    query2.equalTo('objectId',users_id);

     // The new query added
    return query2.first({ useMasterKey: true }).then(function(user) {
      // Update the access_token if it is different.
      // if (accessToken !== tokenData.get('accessToken')) {
      //   tokenData.set('accessToken', accessToken);
      // }
      console.log(user);
      console.log("******");
      /**
       * This save will not use an API request if the token was not changed.
       * e.g. when a new user is created and upsert is called again.
       */
      // return tokenData.save(null, { useMasterKey: true });
    }).then(function(obj) {
      console.log(obj);
      // console.log(user);
      var result = user ;
      // Return the user object.
      return Parse.Promise.as(result); // this is the user object acquired above
    });

Solution

  • After a discussion with OP, there are possible solutions to this matter but each of them have pros and cons.

    Disabling Revocable Session

    Since the introduction of Revocable Session, getSessionToken will always return undefined even with master key. To turn it off, go to App Settings >> Users >> Turn off Require revocable sessions. Then, in upsertGoogleUser method, you just need to return the user object from tokenData.get('user'). It is enough to call user.getSessionToken() in your main cloud function. The final method should look like:

    var upsertGoogleUser = function(accessToken, googleData, emailId) {
        Parse.Cloud.useMasterKey();
        var query = new Parse.Query(TokenStorage);
        query.equalTo('accountId', googleData.id);
        //query.ascending('createdAt');
        // Check if this googleId has previously logged in, using the master key
        return query.first().then(function(tokenData) {
            // If not, create a new user.
            if (!tokenData) {
              return newGoogleUser(accessToken, googleData, emailId);
            }
            // If found, fetch the user.
            var userw = tokenData.get('user');
            var users_id = userw.id;
    
            var query2 = new Parse.Query(Parse.User);
            query2.equalTo('objectId',users_id);
    
            return query2.first().then(function(user) {
              console.log(user);
              console.log(user.getSessionToken());
              console.log("******");
              return Parse.Promise.as(user);
            });
        });
    };
    

    User Password Input

    In order not to change user's password, we can ask user to input his password once we successfully authenticated Google data. We then use the input password to log user in. This is not a good UX, since the purpose of Google Login is to increase usability by letting users not entering password.

    Query on Parse.Session

    This is a possible solution if you want to use "Revocable Session" feature. In the code above, instead of querying on Parse.User, we can look for any revocable session in Parse.Session class. Then we can call getSessionToken on returned object. This is not optimal solution in cases that we need to know which devices the user is logged in.


    Reference: