angular-fullstack / generator-angular-fullstack

Yeoman generator for an Angular app with an Express server
https://awk34.gitbook.io/generator-angular-fullstack
6.12k stars 1.24k forks source link

QUESTION: Create account with social login (facebook) and authenticate it #1450

Closed elmeerr closed 8 years ago

elmeerr commented 8 years ago

Hi, I'm facing a problem with social signup because its not logging after its created as in local signup. When redirects, api/users/me is not accessible..is unauthorized (401), different from what i get in local signup, that redirects with user information.

in facebook/index I have default gets

  .get('/', passport.authenticate('facebook', {
    scope: ['email', 'user_about_me'],
    failureRedirect: '/signup',
    session: false
  }))

  .get('/callback', passport.authenticate('facebook', {
    failureRedirect: '/signup',
    session: false
  }), auth.setTokenCookie);

and in auth.service I have the default functions

function isAuthenticated() {
  return compose()
    // Validate jwt
    .use(function(req, res, next) {
      // allow access_token to be passed through query parameter as well
      if (req.query && req.query.hasOwnProperty('access_token')) {
        req.headers.authorization = 'Bearer ' + req.query.access_token;
      }
      validateJwt(req, res, next);
    })
    // Attach user to request
    .use(function(req, res, next) {
      User.findByIdAsync(req.user._id)
        .then(function(user) {
            //user here is undefined, i want to find out why.
          if (!user) {
            return res.status(401).end();
          }
          req.user = user;
          next();
        })
        .catch(function(err) {
          return next(err);
        });
    });
}

function signToken(id, role) {
  return jwt.sign({ _id: id, role: role }, config.secrets.session, {
    expiresInMinutes: 60 * 5
  });
}

/**
 * Set token cookie directly for oAuth strategies
 */
function setTokenCookie(req, res) {
  if (!req.user) {
    return res.status(404).send('Something went wrong, please try again.');
  }
  var token = signToken(req.user._id, req.user.role);
  res.cookie('token', token);
  res.redirect('/');
}

Could anyone help me with this...Am I missing something?

Thanks in advance.

Awk34 commented 8 years ago

Can you try wording your question differently? I don't quite understand what you're asking.

elmeerr commented 8 years ago

@Awk34 sorry...let me try again: when I'm using facebook login, when I don't have an account on the system, I need to connect to facebook to create a new account, so...after that, the callback redirects to initial page. In that point, it should redirect with user information, this way, on route router.get('/me', auth.isAuthenticated(), controller.me); it won't get error 401

instead of this, I'm being redirected to login page (because of error 401)...and I need to connect to faceebok again and only then get all the user information.

I don't undestand why I can't get user information right after create the account. (you could look the code I already posted to see if I'm missing something)

I don't know if it's relevant but this part

      if (req.query && req.query.hasOwnProperty('access_token')) {
        req.headers.authorization = 'Bearer ' + req.query.access_token;
      }

is always returning false.

what I've understood was, when I'm creating a new account, the validateJwt function return as invalid so, my req.user._id is undefined when isAutheticated() function try to attach user to req throwing the 401 error. is it how it works and I really need to create a new account and after login using facebook button or can I create a new account and be redirected with all user information right after that (what I'm not getting)

I hope that's clear now...thanks again. and sorry for my english.

TimPchelintsev commented 8 years ago

I have same issue. No user login after all social auth providers signups. For example, you need to click "facebook", then you accept, then being redirected to site. We want user to be signed in for that moment, but now user need to click "facebook" again(on login page).

Awk34 commented 8 years ago

@TimPchelintsev I think that's an issue with how the generator isn't able to persist the user token across browser sessions

TimPchelintsev commented 8 years ago

@Awk34 I managed to solve this by replacing current "..Async" code with old versions in passport.js files. Also had problem with Facebook not returning email, fixed that with adding "profileFields: ['id', 'emails', 'name']" to "new FacebookStrategy.." part in facebook passport.js.

elmeerr commented 8 years ago

@TimPchelintsev could you show the code?

TimPchelintsev commented 8 years ago

@elmeerr I replaced this code from server/auth/facebook/passport.js(similarly for other providers):

import passport from 'passport';
import {Strategy as FacebookStrategy} from 'passport-facebook';

export function setup(User, config) {
  passport.use(new FacebookStrategy({
    clientID: config.facebook.clientID,
    clientSecret: config.facebook.clientSecret,
    callbackURL: config.facebook.callbackURL,
    profileFields: [
      'displayName',
      'emails'
    ]
  },
  function(accessToken, refreshToken, profile, done) {
    User.findOneAsync({
      'facebook.id': profile.id
    })
      .then(user => {
        if (user) {
          return done(null, user);
        }

        user = new User({
          name: profile.displayName,
          email: profile.emails[0].value,
          role: 'user',
          provider: 'facebook',
          facebook: profile._json
        });
        user.saveAsync()
          .then(user => done(null, user))
          .catch(err => done(err));
      })
      .catch(err => done(err));
  }));
}

with old variant of generator(save instead of saveAsync). Also I added profileFields: ['id', 'emails', 'name'] to solve problem with Facebook not returning profile.emails.

var passport = require('passport');
var FacebookStrategy = require('passport-facebook').Strategy;

exports.setup = function (User, config) {
  passport.use(new FacebookStrategy({
      clientID: config.facebook.clientID,
      clientSecret: config.facebook.clientSecret,
      callbackURL: config.facebook.callbackURL,
      profileFields: ['id', 'emails', 'name'] // <-- find this on stackoverflow
    },
    function(accessToken, refreshToken, profile, done) {
      User.findOne({
        'facebook.id': profile.id
      },
      function(err, user) {
        if (err) {
          return done(err);
        }
        if (!user) {
          console.log(profile);
          user = new User({
            name: profile.displayName,
            email: profile.emails[0].value,
            role: 'user',
            username: profile.username,
            provider: 'facebook',
            facebook: profile._json
          });
          user.save(function(err) {
            if (err) done(err);
            return done(err, user);
          });
        } else {
          return done(err, user);
        }
      })
    }
  ));
};

No investigation on this. It was quick intuitive solution, it will be interesting to understand what is going on there..

elmeerr commented 8 years ago

@TimPchelintsev have you solved the signup problem with this code?

taotau commented 8 years ago

Just spent the last couple of hours nutting out this problem, before finding this....

The issue as I experienced it can be recreated in the stock generated app:

The problem lies in the server/auth/facebook/passport.js file as @TimPchelintsev mentioned and is caused by Mongoose/Bluebird interactions.

Mongoose's save() function callback signature is function (err, product, numberAffected), but Bluebird expects only 2 parameters, so it wraps the multiple values in an array.

The save succeeds, but what should be a single User object passed to the done(...) passport callback, is in fact an array consisting of the actual User object and the number 1 (the number of documents affected by the create).

The passport code checks for the existence of an object and then happily passes it around as a User object, however none of the property access' work because we are not dereferencing the array.

On subsequent authentications, the User record already exists in the database and a single User object is correctly returned by the findOneAsync call at the top of the function.

Source : http://stackoverflow.com/questions/25798691/mongoose-with-bluebird-promisifyall-saveasync-on-model-object-results-in-an-ar

I fixed this by replacing the saveAsync call with

        user = new User({
          name: profile.displayName,
          email: profile.emails[0].value,
          role: 'user',
          provider: 'facebook',
          facebook: profile._json
        });
        user.saveAsync()
          .then(saveResult => {
              // mongoose save returns (err, obj, numaffected)
              // bluebird only expects 2 arguments so it wraps the extras in an array
              var user = saveResult[0];
              done(null, user);
           })
          .catch(err => done(err));
elmeerr commented 8 years ago

That's worked @taotau. Thanks!!! I hope the guys update the function to work like this.

using the default sintax used by generator, would be:

user.saveAsync()
            .then(function(user) {
              return done(null, user[0]);
            })

sorry to take so long to respond, I was working in another project.

Thank you guys...