I finished wiring up my authentication flow by enabling cookies inside my application and then essentially telling passport to use cookies inside my authentication.
To test this out, I added a new route handler inside of my application whose sole purpose is to inspect this req.user property.
This is my services/passport.js file:
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const mongoose = require('mongoose');
const keys = require('../config/keys');
const User = mongoose.model('users');
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
User.findById(id).then(user => {
done(null, user);
});
});
// passport.use() is a generic register to make Passport
// aware of new strategy
// creates a new instance to authenticate users
passport.use(
new GoogleStrategy(
{
clientID: keys.googleClientID,
clientSecret: keys.googleClientSecret,
callbackURL: '/auth/google/callback'
},
(accessToken, refreshToken, profile, done) => {
User.findOne({ googleId: profile.id }).then(existingUser => {
if (existingUser) {
// we already have a record with given profile id
} else {
// we dont have a user record with this id, make a new record
done(null, existingUser);
new User({ googleId: profile.id })
.save()
.then(user => done(null, user));
}
});
}
)
);
The data is passed to passport which pulls out the id out of the cookie data. The id is then passed on to my deserializeUser function where I am taking that id and turning it into a user model instance and then the whole goal is that the user model instance returned from deserializeUser is added to the request object as req.user and so that new route handler mentioned above has a job of inspecting req.user.
So in my routes/authRoutes.js:
const passport = require('passport');
module.exports = app => {
app.get(
'/auth/google',
passport.authenticate('google', {
scope: ['profile', 'email']
})
);
app.get('/auth/google/callback', passport.authenticate('google'));
app.get('/api/current_user', (req, res) => {
res.send(req.user);
});
};
So this is supposed to test that someone who has already gone through the OAuth flow in theory, can now log back in and we can authenticate that is the same user.
So the expected behavior being that I would once again go through the OAuth flow by visiting localhost:5000/auth/google and then open a separate browser in localhost:5000/api/current_user and be able to see the MongoDB record of that user as json in the browser, but instead I got a blank page and no error in my command line terminal or anywhere else.
What could be the matter here?
You have a minor flaw i've noticed in your if statement:
if (existingUser) {
// we already have a record with given profile id
} else {
// we dont have a user record with this id, make a new record
done(null, existingUser);
new User({ googleId: profile.id })
.save()
.then(user => done(null, user));
}
should be:
if (existingUser) {
// we already have a record with given profile id
done(null, existingUser);
} else {
// we dont have a user record with this id, make a new record
new User({ googleId: profile.id })
.save()
.then(user => done(null, user));
}
Related
I've been trying to get the google oauth login system to work on multiple ports and found issues of saving the data (from backend to frontend) when redirecting (most tutorials do this on the same port which got me confused). I understand how to get data from backend on frontend (through axios) but am confused how to send data from backend to frontend, and so I found this tutorial similar to what I'm looking for. (I tried to adapt this to use google oauth instead of twitter)
However, in this tutorial during the google redirect, I'm getting an error:
Error: Failed to serialize user into session
On googling, most say it's related to the serializeUser and deserializeUser functions (which I however have). So I'm quite hard-stuck and am hoping to find a fix.
Here's my code:
passport.serializeUser((user, done) => {
done(null, user.id);
});
// deserialize the cookieUserId to user in the database
passport.deserializeUser((id, done) => {
User.findById(id)
.then(user => {
done(null, user);
})
.catch(e => {
done(new Error("Failed to deserialize an user"));
});
});
passport.use(
new GoogleStrategy(
{
clientID: keys.googleClientID,
clientSecret: keys.googleSecret,
callbackURL: "http://localhost:5000/auth/google/redirect"
},
(token, tokenSecret, profile, done) => {
// find current user in UserModel
console.log(profile);
const currentUser = User.findOne({
googleId: profile.id
});
// create new user if the database doesn't have this user
if (!currentUser) {
const newUser = new User({
name: profile.displayName,
screenName: profile.displayName,
googleId: profile.id,
profileImageUrl: ""
})
newUser.save()
.catch((err) => console.log(err));
if (newUser) {
done(null, newUser);
}
} else {
done(null, currentUser);
}
}
)
);
Thanks for reading this and helping!!
I am building an app using node.js and react. I have the need to authenticate users using Oauth v2 with Passport.js and MongoDB. I want to allow users to login using either Google, Facebook or LinkedIn. I have the Google authorization/authentication working great. The user fires up the app, gets a Landing page with Login options. When the Google option is selected an intermediary page is rendered, asking the user to choose their Google Account (I have 4). I select the Google account I want. The app moves on to the next page in the app. I can logout and go back to my landing page. All of that is just fine. Now if I select a login with LinkedIn (I have only 1 account), the app does not give me the intermediary page, asking me to allow (or disallow) the login. It simple and automatically gives me an authenticated user and moves on to the next page in the app. I can then logout and go back to my Landing page. So, the app is working sort of as it should be, but not entirely.
I have confirmed that when I start the login process with LinkedIn there is no item in the User Collection. I have confirmed that after the login I DO have an item in the user Collection for the Linkedin login. I have physically deleted the item (repeatedly) and retried. I get no opportunity to allow/disallow the login with Linkedin.
Here is my code:
services/passport.js
'use strict';
// node modules
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const FacebookStrategy = require('passport-facebook').Strategy
const LinkedinStrategy = require('passport-linkedin-oauth2').Strategy;
const mongoose = require('mongoose');
// local modules
const keys = require('../config/keys');
const User = mongoose.model('users');
passport.serializeUser((user, done) => {
done(null, user.id); // NOTE: user.id = mongoDB _id
});
passport.deserializeUser((id, done) => {
User.findById(id)
.then(user => {
done(null, user);
});
});
passport.use(
new GoogleStrategy({
clientID: keys.googleClientID,
clientSecret: keys.googleClientSecret,
callbackURL: '/auth/google/callback',
proxy: true
},
async (accessToken, refreshToken, profile, done) => {
const existingUser = await User.findOne({
provider: profile.provider,
providerID: profile.id
})
if (existingUser) {
return done(null, existingUser);
}
const user = await new User({
provider: profile.provider,
providerID: profile.id,
displayName: profile.displayName
}).save()
done(null, user);
})
)
passport.use(
new LinkedinStrategy({
clientID: keys.linkedinAppID,
clientSecret: keys.linkedinAppSecret,
callbackURL: '/auth/linkedin/callback',
proxy: true
},
async (accessToken, refreshToken, profile, done) => {
const existingUser = await User.findOne({
provider: profile.provider,
providerID: profile.id
})
if (existingUser) {
return done(null, existingUser);
}
const user = await new User({
provider: profile.provider,
providerID: profile.id,
displayName: profile.displayName
}).save()
done(null, user);
})
)
routes/auth.js
use strict';
// node modules
const passport = require('passport');
// local modules
const keys = require('../config/keys');
module.exports = (app) => {
// google routes
app.get('/auth/google',
passport.authenticate('google', {
scope: ['profile', 'email']
})
);
app.get('/auth/google/callback',
passport.authenticate('google'),
(req, res) => {
res.redirect('/selector');
}
);
// linkedin routes
app.get('/auth/linkedin',
passport.authenticate('linkedin', {
request_type: 'code',
state: keys.linkedinAppState,
scope: ['r_basicprofile', 'r_emailaddress']
})
);
app.get('/auth/linkedin/callback',
passport.authenticate('linkedin'),
(req, res) => {
res.redirect('/selector');
}
);
// common routes
app.get('/api/logout', (req, res) => {
req.logout();
res.redirect('/');
});
app.get('/api/current_user', (req, res) => {
res.send(req.user);
});
}
I don't know if there is anything more you need to see. I have confirmed the the hrefs in my Header component are pointing to the correct endpoints and that they match the routes in auth.js
Upon further investigation I found that there is a fair amount of interpolation that must be engaged in when using passport.js instead of manually writing the API interfaces. My problems were encountered because of incomplete setup of the various parameters for the APIs. By brute force trial and error I have solved the problems.
I was trying to comprehend how passport strategy is working.
Consider these api routes which I am using to authenticate.
router.get("/google", passport.authenticate('google', { scope: ['profile', 'email'] }));
router.get("/google/callback", passport.authenticate('google'), (req, res) => {
res.redirect("http://localhost:3000/")
})
And this is the passport strategy
const passport = require('passport')
const GoogleStratergy = require('passport-google-oauth20')
const keys = require("./key.js")
const User = require("../models/user-model.js")
passport.serializeUser((user, done) => {
done(null, user.id)
})
passport.deserializeUser((id, done) => {
User.findById(id).then((user) => {
done(null, user) //pass it in req of our routes
})
})
passport.use(
new GoogleStratergy({
//Options for the stratergy
callbackURL: "/auth/google/callback",
clientID: keys.google.clientID,
clientSecret: keys.google.clientSecret
}, (accessToken, refreshToken, profile, done) => {
User.findOne({userId: profile.id }).then((currentUser) => {
if (currentUser) {
done(null, currentUser)
} else {
//Changing Image String
let oldURL= profile.photos[0]["value"]
let newURL = oldURL.substr(0, oldURL.length-2);
newURL = newURL + "250"
//Creating Mongoose Database
new User({
username: profile.displayName,
userId: profile.id,
image: newURL,
email: profile.emails[0]["value"]
}).save().then((newUser) => {
console.log("new user created", newUser)
done(null, newUser)
})
}
})
})
)
Now, I think I understand what is happening here, but one thing I am unable to comprehend here is..
How is
passport.use(
new GoogleStratergy({
//Options for the stratergy
being called here? I mean I don't see any export statements, so how is it linked with out Node App? or how does passport knows behind the scene about the location of our google strategy **
Also, Just to confirm, after we pass done from our passport.use? it goes to serialize?
When you require passport, you get a singleton instance i.e. it's constructed the first time you require passport, and is reused every time and everywhere it's required subsequently.
So you don't need to share the instance between modules i.e. no need for export. Any configuration you do on the instance is visible everywhere you require it.
There are other objects in NodeJS that work the same way, one prominent example is the express app instance.
Here is the source code for passport where you can verify this.
I'm writing authentication Nodejs API using passport, passport-google-oauth20
Everything is work but the problem is now I want to verify email of the user via domain. My system just allows email with domain #framgia.com can log in to. If not, send the user back a message.
My code here:
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const mongoose = require('mongoose');
const keys = require('../config/keys');
const User = mongoose.model('users');
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
User.findById(id).then(user => {
done(null, user);
})
});
passport.use(
new GoogleStrategy(
{
clientID: keys.googleClientID,
clientSecret: keys.googleClientSecret,
callbackURL: '/auth/google/callback',
},
async (accessToken, refreshToken, profile, done) => {
const existingUser = await User.findOne({ googleId: profile.id });
if (existingUser) {
return done(null, existingUser);
}
if (!profile._json.domain || profile._json.domain !== 'framgia.com') {
return done(null, {error: 'Not allow access!'});
}
const user = await new User({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
avatar: profile.photos[0].value,
}).save();
done(null, user);
},
),
);
And I'm writing logic code like that:
if (!profile._json.domain || profile._json.domain !== 'framgia.com') {
return done(null, {error: 'Not allow access!'});
}
But I think it won't work, but I don't know how to handle the error and send the message back to user.
My routes:
const passport = require('passport');
module.exports = (app) => {
app.get(
'/auth/google',
passport.authenticate('google', {
scope: ['profile', 'email'],
}),
);
app.get(
'/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
// Successful authentication, redirect home.
res.redirect('/');
},
);
};
How to handle the error and redirect to route /error with some message?
Any ideas would be greatly appreciated, thanks.
First of all, if you want to return the user only if an email has a certain domain, you need to put your domain check logic before findOne(). With current logic, if you found a user it will simply return it without checking the email domain
//check email domain before finding the user
if (!profile._json.domain || profile._json.domain !== 'framgia.com') {
return done(null, {error: 'Not allow access!'});
}
const existingUser = await User.findOne({ googleId: profile.id });
if (existingUser) {
return done(null, existingUser);
}
According to passport js documentation, http://www.passportjs.org/docs/configure/ (check verify callback section)
An additional info message can be supplied to indicate the reason for
the failure. This is useful for displaying a flash message prompting
the user to try again.
so if the domain does not match, you should return an error like this
return done(null, false, { message: 'Not allow access!' });
I have an express app which manages authentication via Passport, initially with a local strategy. To this I have just added Google sign in / account creation and almost everything works as per the docs.
The problem I have is that a user can create an account using the Google Strategy but I cannot quite get it so that an authenticated user (via the local strategy) can simply add additional Google details to their account so that they can use either the local or Google strategy.
In 'index.js' where I define my routes I define const passportGoogle = require('../handlers/google'); which has the details of my Google Strategy.
Further down in index.js I have my authenticate and authorise routes:
/* GOOGLE ROUTES FOR AUTHENTICATION*/
router.get('/google',
passportGoogle.authenticate('google',
{ scope: ['profile', 'email'] }));
router.get('/google/callback',
passportGoogle.authenticate('google',
{
failureRedirect: '/',
failureFlash: 'Failed Login!',
successRedirect: '/account',
successFlash: 'You are logged in!'
}
));
/* GOOGLE ROUTES FOR AUTHORISATION - IE A USER IS ALREADY LOGGED IN AND WANTS TO CONNECT THEIR GOOGLE ACCOUNT*/
// send to google to do the authentication
router.get('/connect/google',
passportGoogle.authorize('google',
{ scope : ['profile', 'email'] }
));
// the callback after google has authorized the user
router.get('/connect/google/callback',
passportGoogle.authorize('google', {
successRedirect : '/profile',
failureRedirect : '/'
})
);
As above my Google strategy is defined in google.js:
var passport = require('passport');
var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;
var User = require('../models/User');
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENTID,
clientSecret: process.env.GOOGLE_CLIENTSECRET,
callbackURL: "http://127.0.0.1:7777/google/callback"
},
// google will send back the token and profile
function(req, token, refreshToken, profile, done) {
// console.log('here is res.locals.user'+ res.locals.user);
console.log('here is req.user'+ req.user);
// asynchronous
process.nextTick(function() {
// check if the user is already logged in
if (!req.user) {
console.log('THERE IS NO REQ.USR');
// find the user in the database based on their facebook id
User.findOne({ 'google.id': profile.id }, function(err, user) {
// if there is an error, stop everything and return that
// ie an error connecting to the database
if (err)
return done(err);
// if the user is found, then log them in
if (user) {
return done(null, user); // user found, return that user
} else {
// if there is no user found with that google id, create them
var newUser = new User();
// set all of the facebook information in our user model
newUser.google.id = profile.id;
newUser.google.token = token;
newUser.name = profile.displayName;
newUser.email = profile.emails[0].value;
// save our user to the database
newUser.save(function(err) {
if (err)
throw err;
// if successful, return the new user
return done(null, newUser);
});
}
});
} else {
const user = User.findOneAndUpdate(
{ _id: req.user._id },
{ $set: {"user.google.id":profile.id,
"user.google.token":accessToken,
"user.google.name":profile.displayName,
"user.google.email":profile.emails[0].value
}
},
{ new: true, runValidators: true, context: 'query' }
)
.exec();
return done(null, user);
req.flash('success', 'Google details have been added to your account');
res.redirect(`back`);
}
});
}));
module.exports = passport;
However when a user is signed in and follows the link to /connect/google a new user is always created rather than their details updated. My logging shows that if (!req.user) condition in the Google stratgy is always firing but I'm not sure why that is since the user is definitely logged in.
Any help much appreciated!
In order to access the req in your callback, you need a passReqToCallback: true flag in your GoogleStrategy config object:
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENTID,
clientSecret: process.env.GOOGLE_CLIENTSECRET,
callbackURL: "http://127.0.0.1:7777/google/callback",
passReqToCallback: true
},
// google will send back the token and profile
function(req, token, refreshToken, profile, done) {
// console.log('here is res.locals.user'+ res.locals.user);
console.log('here is req.user'+ req.user);
....
})
If this flag is omitted, the expected callback form is
function(accessToken, refreshToken, profile, done){...}
So your code is looking for a user property on the accessToken that Google sends back, which should always fail. I also bring this up because, if I'm right, other parts of your function should also be misbehaving. (Like User.findOne({'google.id': profile.id}) should always fail, because the function is called with done as its fourth argument rather than profile.)