Rerequest does not work with passport-facebook-token - node.js

I need emails from facebook for my app to work correctly. However, the user has an option of turning off email from the auth screen. When that happens I get an empty emails field like this
"emails":[{"value":""}]
Here's my original request
passport.authenticate('facebook-token', { scope :['email'] }, function(err, profile) {
fbhandler(req, res, err, profile);
})(req, res);
and here's where I handle the returned values
passport.use(new FacebookTokenStrategy({
clientID: config.facebookAuth.clientID,
clientSecret: config.facebookAuth.clientSecret,
profileFields: ['id', 'displayName', 'emails', 'name']
},
function(token, refreshToken, profile, done) {
console.log ("##### %j", profile);
var id = profile.id,
email = profile.emails[0].value,
methodId = 2;
if (email == "") {
return done(res.json({
errors: "Facebook",
}));
}
.. Do normal stuff.
}
and the fbhandler
var fbhandler = function(req, res, err, profile) {
console.log("###### %j" ,profile);
if (!profile) {
console.log("reauth####");
passport.authenticate('facebook-token', { authType: 'rerequest', scope :['email'] }, function(err, profile) {
//Do normal stuff
})(req, res);
} else {
//do normal stuff
}
}
The rerequest never puts up the auth screen again (actually it just flashes for a second) and it returns empty emails once again.
This seems to be a passport issue and not a FB rerequest issue since I was able to do this successfully with FB JS SDK. You can view this at www.kavyavidya.us (click on fb login at the bottom, edit info in the auth screen and uncheck email address) Notice how it pops up the email address auth screen once again with no option to turn it off.
How can I ensure that the auth screen will popup again for email?

Related

Passport JS Google oauth20 callback missing req object

I have seen some similar questions here but the answer is irrelevant to mine, as I have declared passReqToCallback.
I am trying to create an authentication system with passport. I have successfully integrated passport-local, which register/logs in user and creates a session but am having an issue in the logic of the google strategy:
passport.use(new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL,
passReqToCallback: true
},
(req, accessToken, refreshToken, profile, done) => {
/**
*
* This if statement is failing, as far as I can tell there is no req object being passed despite declaring it above
*
*/
// If user is logged in, proceed to simply link account
if (req.user) {
req.user.googleid = profile.id;
req.user.googleEmail = profile.emails[0].value;
req.user.googleDisplayName = profile.displayname;
pool.query('UPDATE users SET googleid = ?, google_email = ?, google_display_name = ? WHERE id = ?', [
req.user.googleid,
req.user.googleEmail,
req.user.googleDisplayName,
req.id
],
function(err, rows) {
// If google account is duplicate (linked to different account) will return error
if (err) {
return done(err, false, {message: "The Google account you tried to link is associated with another account."});
}
return done(null, req.user);
})
}
// Check if google account is registered
pool.query('SELECT * FROM users WHERE googleid = ?', [profile.id], function(err, rows) {
if (err) {
return done(err);
}
// If not logged in but user already registered, log in
if (rows.length) {
return done(null, rows[0]);
}
// If no existing record, register the user.
else {
let newUser = {
email: profile.emails[0].value,
// Google account specific fields
googleid: profile.id,
googleDisplayName: profile.displayName,
method: "gl", // This field ties this new user to the google account
// General fields (taken from the stuff google gives us)
firstName: profile.name.givenName,
lastName: profile.name.familyName,
googleEmail: profile.emails[0].value
}
let insertQuery = "INSERT INTO users (email, googleid, google_display_name, method, first_name, last_name, google_email) VALUES (?,?,?,?,?,?,?)";
pool.query(insertQuery, [
newUser.email,
newUser.googleid,
newUser.googleDisplayName,
newUser.method,
newUser.firstName,
newUser.lastName,
newUser.googleEmail
],
function(err, rows) {
if (err) return done(err, null);
newUser.id = rows.insertId;
return done(null, newUser);
})
}
})}));
So essentially the first if is supposed to see if the user is already authenticated and then just link the account as so. However, in practice, it will skip this even when authenticated and proceed to the rest of the logic, which works absolutely fine. It will go on to create a new account at the else statement (or return error if email is taken, I still need to implement that part).
But, interestingly, if I am logged in while using it, it doesn't log me in as the new user as it otherwise would, instead it keeps me logged in as the current session.
Where have I gone wrong? Why is the req.user not detected? I have also tried using req.isAuthenticated() with the same result.
Below is my callback route, if helpful:
// Google
router.get('/oauth/google', passport.authenticate('google', {
scope: ['email', 'profile']
}));
// Callback
router.get('/oauth/google/redirect', passport.authenticate('google', {
successRedirect: '/account',
failureFlash: 'Something went wrong. Please enable third party cookies to allow Google to sign in.',
failureRedirect: '/login'
}
));
UPDATE 1: If I try (!req.user), same result, skips to below, not sure what that means is happening

Google OAuth 2.0 doesn't call the callback function

I've tested this passport google strategy locally and it works well, the callback function is called successfully and I can login using the google account. However when I push this code to the production server I see that the callback function is not being called. From the user perspective when they click on the link to use the google strategy they are presented with the google 'pick an account' screen, and then it loads for a bit before dumping them back on the page they were on before. I don't see where the problem is. Thank you in advance for your help!
passport.use('google', new GoogleStrategy({
clientID: "redacted",
clientSecret: "redacted",
callbackURL: "/google/callback"
},
function(accessToken, refreshToken, profile, done) {
const userQueryString = `
SELECT firstName, lastName, AgentReference, agent_number, website_password, account_type, status, state, BIN(Flags) as flags
FROM compass.agent
WHERE googleID = ?`;
console.log("google profile: \n", profile);
//pull the user data for the agent with the same googleID as the selected accoutn at login.
db.query(userQueryString,[profile.id],(err, response, fields)=>{
if(err) {
done(err);
} else if(response.length === 0) {
done(null, false);
} else if(response[0].status === 'Terminated') {
console.log('account is terminated.');
done(null, false);
} else {
let user = {
id: response[0].AgentReference
, accountType: response[0].account_type
, agent_number:response[0].agent_number
, name:response[0].firstName + ' ' + response[0].lastName
, state: response[0].state
, flags: response[0].flags
};
let flagRegex = RegExp('^[0-1]*1[0-1]{9}$');
if(flagRegex.test(user.flags)){
console.log(JSON.stringify(user));
return done(null, user);
} else {
return done(null, false, {message: "You do not have access to this feature. Please speak with your manager for more information."})
}
}
});
}
)
);
Adding proxy:true to the google strategy fixed the issue for me. The callback URL was being sent to google as http instead of https because of this.

Authenticate user with passport through LinkedIn login

I have built a login system in Passport and works quite well. Now, I want to integrate LinkedIn login in my system. I already have clientID, clientSecret etc. needed to login. This is the code that is called when the LinkedIn login button is pressed.
passport.use('linkedin', new OAuth2Strategy({
authorizationURL: 'https://www.linkedin.com/uas/oauth2/authorization',
tokenURL: 'https://www.linkedin.com/uas/oauth2/accessToken',
clientID: clientid,
clientSecret: clientsecret,
callbackURL: '/linkedinLogin/linkedinCallbackUrlLogin',
passReqToCallback: true
},
function(req,accessToken, refreshToken, profile, done) {
console.log('authenticated');
console.log(accessToken);
req.session.code = accessToken;
process.nextTick(function () {
done(null, {
code : req.code
});
});
}));
Both the console.log() calls in the callback function are successfully fired, this means I am successfully logged in through LinkedIn and I receive my access token. The part where I connect with LinkedIn is thus correct, what I am missing is the part where I actually log in the user. As you can see, the callbackURL points to /linkedinLogin/linkedinCallbackUrlLogin. This is what I do in that route:
app.get('/linkedinLogin/linkedinCallbackUrlLogin', passport.authenticate('linkedin', {
session: false,
successRedirect:'/linkedinLogin/success',
failureRedirect:'/linkedinLogin/fail'
}));
I just specify a successRedirect and a failureRedirect. Note that if I put session : true I receive as an error Failed to serialize user into session, so for now I keep it to false.
The successRedirect is successfully called. In that route I call a GET request to LinkedIn to access some data about the user. I want to store this data in my DB and remember the user that logged in. This is how I do it:
https.get(
{
host: 'api.linkedin.com' ,
path: '/v1/people/~?format=json' ,
port:443 ,
headers : {'Authorization': ' Bearer ' + req.session.code}
},
function(myres) {
myres.on("data", function(chunk) {
var linkedinJsonResult = JSON.parse(chunk);
User.findOne({linkedinLogin : linkedinJsonResult.id}, function(err, userSearchResult){
if(err) {
throw err;
}
//user found, login
if(userSearchResult){
console.log(userSearchResult);
}
else {
//create user
var newUser = new User(
{
url : linkedinJsonResult.siteStandardProfileRequest.url,
name : linkedinJsonResult.firstName + " " + linkedinJsonResult.lastName,
linkedinLogin : linkedinJsonResult.id,
regDate : new Date()
}
);
//save user
newUser.save(function(err, user){
if(err){
throw err;
}
//login
console.log(user);
});
}
});
});
}
);
Let me explain the code there. After getting the data of the user I check the field "id" that is received. If this id matches one of my users' linkedinLogin field stored into the DB, I consider it already registered (the user has been found in the DB), thus I have to log him/her in. Otherwise I just create a new user using the data received from the GET request.
My question is, in both the cases - the user is found in my DB, or the user has to be created - how can I set req.user to be my user whenever it interacts with my website? Is it sufficient to just do req.user = userSearchResult (if the user is found, inside the if statement) or req.user = user (if the user has been created, inside the newUser.save() callback), or should I call some passport functions that will set it for me?
All the other passport functions related to the registration and login of users without using LinkedIn login are working fine. I am just worried about making this LinkedIn login work with passport.
Thank you.
passport.js will automatically set the req.user object to the object you will pass as the second argument to the done function of the strategy callback.
This means that you should do something like this:
function(req,accessToken, refreshToken, profile, done) {
console.log('authenticated');
console.log(accessToken);
req.session.code = accessToken;
process.nextTick(function () {
// retrieve your user here
getOrCreateUser(profile, function(err, user){
if(err) return done(err);
done(null, user);
})
});
}));
I hope this helps.

email field is optional in passportjs facebook strategy

I wrote the code for login with facebook, everything works and I'm getting the user's email address. But there is another option on facebook which lets the user select the data my application is going to have access to.
If user clicks on that, he'll see the name and everything marked as required, but email is optional and the user can remove it from the data that is going to be provided to the app.
On the application, email is required. So how I can mark the email as required on facebook?
This is the snippet I'm using in the code.
passport.use(new FacebookStrategy({
clientID: config.social.facebook.clientID,
clientSecret: config.social.facebook.clientSecret,
callbackURL: config.url+'auth/facebook/cb',
enableProof: false,
profileFields:['id', 'name', 'emails'],
scope: "email"
}, function(accessToken, refreshToken, profile, done) {
// doing the rest of the thing
}
));
// ...
app.get('/auth/facebook', passport.authenticate('facebook', {scope: ['email']}));
app.get('/auth/facebook/cb', passport.authenticate('facebook'), function(req, res, next) {
res.redirect("/");
});
I solved the problem by re-requesting the permission.
Turns out I can add authType: 'rerequest' to passport.authenticate('facebook', {scope: ['email'], authType: 'rerequest'}).
What I did is to check if the emails field is present in the result, if not, I call done with an error.
function(accessToken, refreshToken, profile, done) {
if (profile.emails === undefined) {
done('email-required')
return;
}
// doing the rest of the thing
}
Then to catch the error, I had to write a custom callback for passport.authenticate('facebook').
app.get('/auth/facebook/cb', function(req, res, next) {
passport.authenticate('facebook', function (err, user, info) {
if (err) {
if (err == 'email-required') res.redirect('/auth/facebook/rerequest');
// check for other kinds of errors and show proper messages
return;
}
req.user = user;
// do the rest of the thing
})(req, res, next)
});
As you see, I redirect the user to another route /auth/facebook/rerequest in case of error.
app.get('/auth/facebook/rerequest',
passport.authenticate('facebook', {
scope: ['email'],
authType: 'rerequest' // this is important
}
));
This will redirect the user to the same page on FB again and this time email field is required. I couldn't do this in the same route; apparently it was using the same generated code to communicate to fb which was not acceptable by fb.
And that's how I managed to solve the issue.
you need to specify email in Strategy in profileFields property
passport.use('facebook', new FacebookStrategy({
clientID: config.facebook.appId,
clientSecret: config.facebook.appSecret,
callbackURL: config.facebook.callbackURL,
profileFields: ['emails', 'first_name', 'last_name', 'locale', 'timezone']
}, function (token, refreshToken, profile, done) {
// you will get emails in profile.emails
}));

Google Oauth giving code redeemed error

Hi i am working on a project where a user logs in via google account.(localhost)
I have implemented the google signup.
As soon as I log in from my account I am getting the below error.
TokenError: Code was already redeemed.
at Strategy.OAuth2Strategy.parseErrorResponse (c:\Projects\Internship_rideshare\node_modules\passport-google-oauth\node_modules\passport-oauth\node_modules\passport-oauth2\lib\strategy.js:298:12)
at Strategy.OAuth2Strategy._createOAuthError (c:\Projects\Internship_rideshare\node_modules\passport-google-oauth\node_modules\passport-oauth\node_modules\passport-oauth2\lib\strategy.js:345:16)
at c:\Projects\Internship_rideshare\node_modules\passport-google-oauth\node_modules\passport-oauth\node_modules\passport-oauth2\lib\strategy.js:171:43
at c:\Projects\Internship_rideshare\node_modules\passport-google-oauth\node_modules\passport-oauth\node_modules\passport-oauth2\node_modules\oauth\lib\oauth2.js:176:18
at passBackControl (c:\Projects\Internship_rideshare\node_modules\passport-google-oauth\node_modules\passport-oauth\node_modules\passport-oauth2\node_modules\oauth\lib\oauth2.js:123:9)
at IncomingMessage.<anonymous> (c:\Projects\Internship_rideshare\node_modules\passport-google-oauth\node_modules\passport-oauth\node_modules\passport-oauth2\node_modules\oauth\lib\oauth2.js:142:7)
at IncomingMessage.emit (events.js:129:20)
at _stream_readable.js:908:16
at process._tickCallback (node.js:355:11)
My code is as follows(snippet for google login):-
passport.use(new GoogleStrategy(google, function(req, accessToken, refreshToken, profile, done) {
if (req.user) {
User.findOne({ google: profile.id }, function(err, existingUser) {
if (existingUser) {
console.log('There is already a Google+ account that belongs to you. Sign in with that account or delete it, then link it with your current account.' );
done(err);
} else {
User.findById(req.user.id, function(err, user) {
user.google = profile.id;
user.tokens.push({ kind: 'google', accessToken: accessToken });
user.profile.displayName = user.profile.displayName || profile.displayName;
user.profile.gender = user.profile.gender || profile._json.gender;
//user.profile.picture = user.profile.picture || 'https://graph.facebook.com/' + profile.id + '/picture?type=large';
user.save(function(err) {
console.log('Google account has been linked.');
done(err, user);
});
});
}
});
} else {
User.findOne({ google: profile.id }, function(err, existingUser) {
if (existingUser) return done(null, existingUser);
User.findOne({ email: profile._json.email }, function(err, existingEmailUser) {
if (existingEmailUser) {
console.log('There is already an account using this email address. Sign in to that account and link it with Google manually from Account Settings.' );
done(err);
} else {
var user = new User();
user.email = profile._json.email;
user.google = profile.id;
user.tokens.push({ kind: 'google', accessToken: accessToken });
user.profile.displayName = profile.displayName;
user.profile.gender = profile._json.gender;
//user.profile.picture = 'https://graph.facebook.com/' + profile.id + '/picture?type=large';
user.profile.location = (profile._json.location) ? profile._json.location.name : '';
user.save(function(err) {
done(err, user);
});
}
});
});
}
}));
I am stuck on it.Please help me out..thanks
The problem is not in your "snippet", look at the routes. It should be absolute path on redirect for google.
router.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '#/signIn' }),
function(req, res) {
// absolute path
res.redirect('http://localhost:8888/#/home');
});
It's known issue, follow this link to other workarounds
https://github.com/jaredhanson/passport-google-oauth/issues/82
I have come across this issue. The exact problem is your route.
app.get('/auth/google/callback', passport.authenticate('google'), (req, res) => {
res.send('get the data');
});
At this point app had got user permission and google send a code to this url. Now what passport does here it took that code and made a request to google for user details and got it from google. Now we have to do something with this details otherwise you will get the error that you have got.
Now we can use serialiseUser and deserialiseUser of passport to save details in cookie and edit one line of above code to go at some url like that.
app.get('/auth/google/callback', passport.authenticate('google'), (req, res) => {
res.redirect('/servey'); // just a url to go somewhere
});
I also had the same problem since few days. What I figured out is, you just need to complete the process. Until now you have only checked whether the user is present in the database or not. If not then you save the user to the database.
However, after this, when the google tries to redirect the user, the code that google+ API sent is already used or say it is no longer available. So when you check the user in your database, you need to serialize the user i.e store the code into your browser in a cookie so that when google redirects the user, it know who the user is. This can be done by adding the code given below.
//add this in current snippet
passport.serializeUser(function(user,done){
done(null,user.id);
});
To use this cookie, you need to deserialize the user. To deserialize, use the code given below.
//add this in current snippet
passport.deserializeUser(function(id,done){
User.findById(id).then(function(user){
done(null, user);
});
});
Also, you are required to start a cookie session and you can do this by adding the below code in your main app.js file.
const cookieSession = require('cookie-session');
app.use(cookieSession({
maxAge: 24*60*60*1000, // age of cookie, the value is always given in milliseconds
keys:[keys.session.cookiekey]
}));
//initialize passport
app.use(passport.initialize());
app.use(passport.session());
Note that you need to require the cookie-session package. Install it using
npm install cookie-session
Also, you require to write absolute URI in the callbackURL property in your google strategy.
I had the same problem.
Reseting client secret from google console solved the problem.

Resources