2

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
    });
Community
  • 1
  • 1
Gautam
  • 1,463
  • 1
  • 10
  • 19
  • Why do you want to reset user's password once you update its accessToken? You can just return the user object . – Ralphilius Jan 07 '16 at 11:08
  • See the flow is like this -> if a user exists with `email` - "abcd@gmail.com" and then he tries to signup using google and his email is same - "abcd@gmail.com" I want to link that user to his existing account and not create a new account. So I can do the linking but to get the session token I need to login. And to login I need his username and password to login using the normal login trend in parse. Without login I don't get a session_token. – Gautam Jan 07 '16 at 12:14

1 Answers1

1

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:

Ralphilius
  • 1,696
  • 2
  • 15
  • 26
  • The answer you have written I tried this only before. But it doesn't work. I always get the response as undefined. – Gautam Jan 07 '16 at 17:18
  • Can you try printing `user` object after `tokenData.get..` and before `return Parse.Promise...`? If the first one found and the latter undefined, try to assign the `user` object to an intermediate variable. – Ralphilius Jan 07 '16 at 17:23
  • Ok I will do that..I checked the user after getting the tokenData it doesn't have the session token. Wait I will post the log. One more thing it stores the data in the DB but still returns undefined. – Gautam Jan 07 '16 at 17:24
  • I have added the log of user just before returning above and this gives undefined for `getSessionToken()`. – Gautam Jan 07 '16 at 17:34
  • OK. I know why. You have to query on `User` class in order to get it work as per the docs says: `Returns the session token for this user, if the user has been logged in, or if it is the result of a query with the master key. Otherwise, returns undefined.`. Rather than having `User` as pointer in `TokenStorage`, can you do the opposite? – Ralphilius Jan 07 '16 at 17:39
  • So you mean a `TokenStorage` pointer in `User` table? Ya I guess I can do that . That's like the whole logic will change right? – Gautam Jan 07 '16 at 17:43
  • Yes. Please do that and let me know. I'll reply tomorrow. – Ralphilius Jan 07 '16 at 17:44
  • I can do one more than I can get `objectId` from the present `TokenStorage` query and then query the `User` table. That should work right? – Gautam Jan 07 '16 at 17:46
  • Yes. First, do a query on `TokenStorage`, then use the returned object to query on the pointer in `User`. – Ralphilius Jan 07 '16 at 17:49
  • It doesn't work even if I query `User` table directly. – Gautam Jan 07 '16 at 18:07
  • Can you add your second approach to the question so I can check? – Ralphilius Jan 07 '16 at 18:13
  • I have added the code for upsert function. I have tried with a totally separate function also directly querying the `User` table but I still get undefined. Link to separate function also to check this -> http://pastebin.com/S0Y9mWtx – Gautam Jan 07 '16 at 18:17
  • Remove the last `then` and move `return Parse.Promise.as..` below `console.log("******")`. Remember to change `as(result)` to `as(user)` – Ralphilius Jan 07 '16 at 18:28
  • Hey have you done something like this in the past? And did it work without loging in the user? – Gautam Jan 08 '16 at 04:50
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/100111/discussion-between-ralphilius-and-nik). – Ralphilius Jan 08 '16 at 05:48