1

I'm using Sails with Passport for authentication. I'm using passport-google-oauth(OAuth2Strategy) and passport-facebook for enabling Google Sign-in.

I'm not too well-versed with Passport, so pardon me if this is a rookie question. I've set up login via Facebook and it works just fine. With Google, I do receive an authorization code after allowing access to the app, but the I'm eventually not authenticated. I'm guessing the same code should work for both Facebook and Google since the strategies are both based on oauth2.

I'm not even sure what code to share, since I'm using the auto-generated code from sails-generate-auth, but do let me know if there's anything else I can share.

Any ideas on why this might be happening? The app is locally hosted but that's unlikely to be the problem since I am getting to the authorization stage anyway.

galactocalypse
  • 1,905
  • 1
  • 14
  • 29

2 Answers2

3

I faced the same problem and it was located here in in api/services/passport.js:

// If the profile object contains a list of emails, grab the first one and
// add it to the user.
if (profile.hasOwnProperty('emails')) {
  user.email = profile.emails[0].value;
}
// If the profile object contains a username, add it to the user.
if (profile.hasOwnProperty('username')) {
  user.username = profile.username;
}

// If neither an email or a username was available in the profile, we don't
// have a way of identifying the user in the future. Throw an error and let
// whoever's next in the line take care of it.
if (!user.username && !user.email) {
  return next(new Error('Neither a username nor email was available'));
}

The Google service was not returning a profile.username property.

Because of it, the user is not saved in the database and cannot be authenticated. Then the passport callback receives an empty user, so the function that handles errors is fired and the user is redirected to the login page.

This change allows to use the displayName property as the username:

// If the profile object contains a list of emails, grab the first one and
// add it to the user.
if (profile.hasOwnProperty('emails')) {
  user.email = profile.emails[0].value;
}
// If the profile object contains a username, add it to the user.
if (profile.hasOwnProperty('username')) {
  user.username = profile.username;
}


/** Content not generated BEGIN */
// If the username property was empty and the profile object
// contains a property "displayName", add it to the user.
if (!user.username && profile.hasOwnProperty('displayName')) {
  console.log(profile);  // <= Use it to check the content given by Google about the user
  user.username = profile.displayName;
}
/** Content not generated END */


// If neither an email or a username was available in the profile, we don't
// have a way of identifying the user in the future. Throw an error and let
// whoever's next in the line take care of it.
if (!user.username && !user.email) {
  return next(new Error('Neither a username nor email was available'));
}

You could also use the profile.id property because profile.displayName is not necessarily unique (ie: two Google accounts can have an identical displayName). But it is also true accross different services: a Twitter account could also have the same username than a Facebook account. If both register on your application, you will have a bug. This is a problem from the code generated by sails-generate-auth and you should adapt it with the behavior that you want.

I will propose a PR if this solution works for you too.

Alexis N-o
  • 3,954
  • 26
  • 34
  • I mentioned that the user isn't getting authenticated in the first place. In the `callback ` I get an authentication code, but `passport.authenticate` doesn't return any authenticated user and I'm simply directed back to the `/login` page. I'm guessing now that this is a problem with the accounts settings but I had fiddled with them and didn't find anything blocking any kind of access. I'll update after trying out with a new account. – galactocalypse Jul 30 '15 at 15:12
  • Alright, I tried logging in with a different account. Didn't work. I would have taken care of the attribute names had the `connect` method been called in the first place. My request ends within the `callback` endpoint. – galactocalypse Jul 30 '15 at 17:06
  • I think the answer I gave fits to your description. I added some explanations, I hope it makes it more clear. Or maybe I am missing something? – Alexis N-o Jul 30 '15 at 17:30
  • My problem was with the authentication itself, not the parsing of the profile. Finally managed to get to the root of it by `console.log` all the way upto `passport-google-oauth/node_modules/passport-oauth/node_modules/passport-oauth2/lib/strategy.js`. Attaching the details as an answer. – galactocalypse Jul 30 '15 at 17:54
1

Alright, so this ultimately turned out to be a known issue with the API.

TL;DR: Enable the Google+ API and the Contacts API as mentioned here. (The Contacts API isn't required, as @AlexisN-o pointed out in the comments. My setup worked as desired with Contacts API disabled. This obviously depends on what scope you're using.)

I believe it's not a nice way of failing since this was an API error that was prevented from bubbling up. Anyway, I dug into passport.authenticate to figure out what was going wrong. This eventually calls the authenticate method defined in the package corresponding to the strategy (oauth2 in this case). In here (passport-google-oauth/lib/passport-google-oauth/oauth2.js) I found that the accessToken was indeed being fetched from Google, so things should be working. This indicated that there was a problem with the requests being made to the token urls. So I ventured a little further into passport-oauth2/lib/strategy.js and finally managed to log this error:

{ [InternalOAuthError: failed to fetch user profile]
  name: 'InternalOAuthError',
  message: 'failed to fetch user profile',
  oauthError: 
   { statusCode: 403,
     data: '{
        "error": {
            "errors": [{
                  "domain": "usageLimits",
                  "reason": "accessNotConfigured",
                  "message": "Access Not Configured. The API (Google+ API) is not enabled for your project. Please use the Google Developers Console to update your configuration.",
                  "extendedHelp": "https://console.developers.google.com" 
            }],
            "code": 403,
            "message": "Access Not Configured. The API (Google+ API) is not enabled for your project. Please use the Google Developers Console to update your configuration." 
      }
    }'
} }

This was the end of the hunt for me and the first result for the error search led to the correct answer. Weird fix though.

Community
  • 1
  • 1
galactocalypse
  • 1,905
  • 1
  • 14
  • 29
  • 1
    Good spot ! I completely had forgotten that I had done this and will add this question to my favorites to remember it ! For me enabling *Google+ API* was sufficient. Did you have to manage the `profile.username` thing too? I am curious to know if you faced the same problem. – Alexis N-o Jul 30 '15 at 20:59
  • I just tried disabling the Contacts API and the sign-in was smooth. Thanks for pointing that out! I had faced the `profile.username` problem while setting up login via Facebook, so I was aware of `profile` attribute variations and just dumped the whole object to find out what it contained. I posted an [identifier-related question](http://stackoverflow.com/questions/31732278/is-it-safe-to-expose-a-users-third-party-identifiers) in this regard just a couple of hours back. – galactocalypse Jul 30 '15 at 21:39