Google oAuth with passportjs + Vue - node.js

I am currently stuck to handle a google oAuth login in a vue app which is connecting to my own node express api server.
On the express api server i am using passport as a middleware to handle google oauth and after succesfully logged in through google i am generating a jwt in the callback on my backend.
passport.use(new GoogleStrategy({
clientID: config.get('google.clientID'),
clientSecret: config.get('google.clientSecret'),
callbackURL: config.get('google.callbackUrl'),
},
function(accessToken, refreshToken, profile, done) {
User.findOne(
{ socialID: profile.id },
function (err, user) {
if (err) {
return done(err);
}
//No user was found... so create a new user with values from Facebook (all the profile. stuff)
if (!user) {
user = new User({
name: profile.displayName,
email: profile.emails[0].value,
provider: profile.provider,
socialID: profile.id,
});
user.save(function(err) {
if (err) console.log(err);
});
}
// the information which shall be inside the jsonwebtoken
const payload = {
user: {
id: user.id
}
};
// create jsonwebtoken and return it
jwt.sign(
payload,
config.get('jwt.secret'), // get the secret from default.json to hash jsonwebtoken
{ expiresIn: config.get('jwt.lifetime') },
(err, token) => {
if(err) throw err; // if there is error, throw it and exit
return done(JSON.stringify(token)); // return jwt token
}
);
}
);
}
));
I have theses routes on my api server
// #route GET api/auth/google
// #desc Google auth route - get User From Google, store it if not exists yet
// #access Public
router.get('/google',
passport.authenticate('google', { scope: ['profile', 'email'], session: false })
);
// #route GET api/auth/google/callback
// #desc Google callback route
// #access Public
router.get('/google/callback',
passport.authenticate('google', { failureRedirect: '/', session: false }),
function (req, res) {
res.redirect('http://localhost:8080/?token=' + res);
}
);
When i call my backend api route at /auth/google i successfully get redirected to the google login page. But with my approach i am trying to redirect from the callback url back to my vue app with a get parameter "token" to recieve the token in the frontend. The redirect in my backend callback route is not working. How do i pass the token which is generated in the backend to my frontend?

I came across that the redirect wasn't working because the return done() function expects two parameters to work correctly.
I changed inside the google passport middleware the done function like this
jwt.sign(
payload,
config.get('jwt.secret'), // get the secret from default.json to hash jsonwebtoken
{ expiresIn: config.get('jwt.lifetime') },
(err, token) => {
if(err) throw err; // if there is error, throw it and exit
return done(null, token); // return jwt token
}
);
Now inside my route i can successfully redirect + add the token as a get parameter - so with this workaround i am recieving my jwt which is generated in my backend in my frontend.
// #route GET api/auth/google/callback
// #desc Google callback route
// #access Public
router.get('/google/callback',
passport.authenticate('google', { failureRedirect: '/', session: false }),
function (req, res) {
let token = res.req.user;
res.redirect('//localhost:8080/?token=' + token);
}
);

Related

node.js react Error [ERR_HTTP_HEADERS_SENT] when trying to send token using jwtToken with passport google oath2.0

I am currently trying to implement a google authentication using passport google oath2.0 on my node.js react web application with mongodb as database. I am struggling on passing token from google passport to jwtToken.
I tried a remedy by inserting the sendToken(use, 200, res) on the route parameter. I apologize for not so clear explanation but to make it easier to understand, here is my code.
backend/utils/sendToken.js
const sendToken = (user, statusCode, res) => {
// Create Jwt token
const token = user.getJwtToken();
// Options for cookie
const options = {
expires: new Date(
Date.now() + process.env.COOKIE_EXPIRES_TIME * 24 * 60 * 60 * 1000
),
httpOnly: true
}
res.status(statusCode).cookie('token', token, options).json({
success: true,
token,
user
})
}
module.exports = sendToken;
backend/passport.js
...
passport.use(new GoogleStrategy({
clientID: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
callbackURL: "/api/v1/google/callback",
scope: ["profile", "email"],
},
function (accessToken, refreshToken, profile, cb) {
const googleEmail = profile._json.email;
const googlePassword = profile._json.family_name;
User.findOne({
'google.google_id': profile.id
}, function(err, user) {
if (err) {
return cb(err);
}
if (!user) {
user = new User({
google: {
google_id: profile.id,
google_mail: googleEmail,
} ,
name: profile.displayName ,
email: googleEmail ,
password: googlePassword ,
avatar: { url: profile._json.picture }
});
user.save(function(err) {
if (err) console.log(err);
console.log(profile, "Register Successful, 'Access Token: '", accessToken, 'Refresh Token: ',refreshToken);
return cb(err, user);
});
} else {
console.log(profile, "Login Successful, 'Access Token: '", accessToken, 'Refresh Token: ',refreshToken);
return cb( err, user);
}
});
}
));
...
backend/routes/auth.js
const express = require('express');
const router = express.Router();
const passport = require("passport");
const sendToken = require('../utils/jwtToken');
...
router.get("/google", passport.authenticate("google", { scope: ["profile", "email"] }));
router.get(
"/google/callback",
passport.authenticate('google', { failureRedirect: '/' }),
function( req, res) {
// Successful authentication
//send user object to sendToken via jwtToken
sendToken(req.user, 200, res)
//this is where I trying to redirect after sending token to jwtToken
redirect(`http://localhost:3000/dashboard`);
});
...
You cannot redirect after you have already send a response
// you are sending a response inside sendToken function
sendToken(req.user, 200, res); // (res.status(statusCode).cookie(...).json(...)
// so you cannot redirect after response was sent
res.redirect(`http://localhost:3000/dashboard`);
If you want to redirect, you can do something like:
res.cookie('token', token, options).redirect(...);
have you ever tried using res.redirect(http://localhost:3000/dashboard) instead?

Encrypt Nodejs JWT Token

I have been following the tutorial on creating a test API application from this article. At the end of the article i see a mention its best to encrypt the jwt token for added security so i wen searching for a way to do that as well. I ran into this article and it gives examples of how to encrypt the jwt token with RSA private/public keys.
THIS is where im getting stuck. After i have successfully signed up using the /signup route, i can then use the /login route to get my token. So im assuming this is where i use my private key to encrypt the token before sending it back to the user?
**Made repo public for testing - you will need only to provide a mongoDB connection string in app.js
Im stuck at the encrypt/decrypt portion of the process, any help appreciated.
router.post("/login", async (req, res, next) => {
passport.authenticate("token", async (err, user, info) => {
try {
if (err || !user) {
const error = new Error("An Error occurred");
return next(error);
}
req.login(user, { session: false }, async error => {
if (error) return next(error);
//We don't want to store the sensitive information such as the
//user password in the token so we pick only the email and id
const body = { _id: user._id, email: user.email };
//Sign the JWT token and populate the payload with the user email and id
const token = jwt.sign({ user: body }, PRIV_KEY, { algorithm: 'RS256' });
//Send back the token to the user
return res.json({ token });
});
} catch (error) {
return next(error);
}
})(req, res, next);
});
And then when making calls to the "secure" routes i need to decrypt the token and verify it against the public key?
router.get("/profile", (req, res, next) => {
//We'll just send back the user details and the token
jwt.verify(req.query.token, PUB_KEY, { algorithms: ['RS256'] }, function(err, decoded) {
if (err.name === "TokenExpiredError") {
console.log("Whoops, your token has expired!");
}
if (err.name === "JsonWebTokenError") {
console.log("That JWT is malformed!", err); <------ GET ERROR HERE
}
if (err === null) {
console.log("Your JWT was successfully validated!");
}
// Both should be the same
console.log(decoded);
res.json({
message: "You made it to the secure route",
user: req.user
});
});
});
I don’t have the time to reproduce this. Your login part seems correct. However, you should try to setup protected routes like this, copied and tailored to your needs from your first article:
Setting up middleware to handle jwt decryption, make sure to require it in your app.js or wherever you need to, if you set it up in a separate file. This can be used as a middleware later on in your controllers:
const JWTstrategy = require('passport-jwt').Strategy;
//We use this to extract the JWT sent by the user
const ExtractJWT = require('passport-jwt').ExtractJwt;
//This verifies that the token sent by the user is valid
passport.use(new JWTstrategy({
//secret we used to sign our JWT
secretOrKey : PUB_KEY,
algorithms: ['HS256'],
//we expect the user to send the token as a query parameter with the name 'token'
jwtFromRequest : ExtractJWT.fromUrlQueryParameter('token')
}, async (token, done) => {
try {
//Pass the user details to the next middleware
return done(null, token.user);
} catch (error) {
done(error);
}
}));
Setup protected route, note that you don’t need to manually call jwt.verify, middleware handles it and populates req.user:
const express = require('express');
const router = express.Router();
//Let's say the route below is very sensitive and we want only authorized users to have access
//Displays information tailored according to the logged in user
router.get('/profile', passport.authenticate('jwt', { session: false }), (req, res, next) => {
//We'll just send back the user details and the token
res.json({
message : 'You made it to the secure route',
user : req.user,
token : req.query.token
})
});
module.exports = router;
**Update based on your comment:
I cloned your repo and it is working for me, although I changed some things:
I added
app.use(bodyParser.json()); to app.js so that I could send the request bodies as json - this is not necessary if you prefer urlencoded
the problem is that secureRoute that you export is another router and you try to use it as a controller in app.js:
...
const secureRoute = require('./routes/secure-routes');
...
app.use('/user', passport.authenticate('jwt', { session: false }), secureRoute);`
*note that it will be /user route, if you want /profile please change it in like app.use('/profile', ...)
so instead of
router.get("/profile", (req, res, next) => {
//We'll just send back the user details and the token
res.json({
message: "You made it to the secure route",
user: req.user,
token: req.query.secret_token
});
});
it should be just a controller function:
...
module.exports = (req, res, next) => {
//We'll just send back the user details and the token
res.json({
message: 'You made it to the secure route',
user: req.user,
token: req.query.token // note that you don't use secret_token but token as a name
});
};
the third thing is to not forget to add token to the query params, when you call the API, like http://localhost:3000/user?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7Il9pZCI6IjVlODc2Yjc1YTVlNTk3MTRlOGFjMmI4NyIsImVtYWlsIjoiYUBiLmNvbSJ9LCJpYXQiOjE1ODU5MzMyNjR9.lcLuQeCMRy7Ef9zNkIt_rn4S22t2cm7YLRE7Jgp1Mpw
you should get the response:
{
"message": "You made it to the secure route",
"user": {
"_id": "5e876b75a5e59714e8ac2b87",
"email": "a#b.com"
}
}

Getting a 401 status when trying to reach protected route using passport-jwt

I am using JWT working with passport.js for authentication. I have a local strategy that signs up and another that logs in a user. Currently, I have it return the token by doing:
req.login(user, {session: false}, (err) => {
if (err) {
res.send(err);
}
var jwtUser = {
_id: user.id,
email: user.email
};
const token = jwt.sign(jwtUser, config.passport.jwtSecret);
return res.json({jwtUser, token});
});
This returns an object like:
{"jwtUser":
{
"_id":"5c55f0be9ddcf71a704d92b2",
"email":"john.doe#example.com"
},
"token":"<token>" // redacted this because it contains my personal email
}
So I am getting a token back that is correct (I've checked using an online decoder, it gives the correct user ID and the correct email).
When I use this route to try and test whether the JWT strategy is working or not I get a 401 unauthorized access message.
Here is my JWT strategy
var opts = {};
opts.jwtFromRequest = ExtractJWT.fromAuthHeaderAsBearerToken();
opts.secretOrKey = config.passport.jwtSecret;
passport.use('jwt', new JWTstrategy(opts, function(jwt_payload, done) {
console.log('payload received: ', jwt_payload);
User.findOne({id: jwt_payload.id}, function(err, user) {
console.log('User: ', user);
if (user) {
console.log(user);
return done(null, user);
}
if (err) {
return done(err, false);
}
});
}));
My route
router.get('/auth/test', passport.authenticate('jwt', { session: false }), (req, res) => {
res.send('You reached a private route.');
});
I have my local strategy working using the same method of calling passport.
I think I have included everything of value, if something is missing please say and I will update the OP with it.
I should note I am using postman to test this and using the authorization method of bearer token.
Thanks in advance!

Express Passport Google Strategy - using res.redirect

I have an express app which manages authentication via Passport with support for both local and Google strategies.
At the moment I support account creation via Google auth but actually a user's Google account doesn't contain all the info that my application requires. Because of this I would like to redirect uses who don't have a local account to my own account creation and only support account linking (i.e. their local account can be linked to Google account for authorization).
The problem is that I would normally do this via res.redirect but res is not available in the Google Strategy - what is the best way to handle this?
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",
passReqToCallback: true
},
// google will send back the token and profile
function(req, accessToken, refreshToken, profile, done) {
// asynchronous
process.nextTick(function() {
// check if the user is already logged in
if (!req.user) {
// 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 { <--here is where I want to redirect
//currently I create a new user but i really want to redirect them to create a local account
// 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;
Any help much appreciated!

Express Passport Session not working

I'm building a Node application in which the users must register or login, then when they drag and drop some elements (the front end is all working) I store on the database their action with their corresponding userId.
My understanding is that once they are registered/logged in, I can use the req.user to access their id and correctly store their actions, however it isn't working.
Here is the section of my server.js file that deals with Passport. Also, I'm using Sequelize as an ORM, but everything dealing with the database works perfect without the req.user part.
app.use(cookieParser());
app.use(bodyParser.json());
app.use(passport.initialize());
app.use(passport.session());
/****** Passport functions ******/
passport.serializeUser(function (user, done) {
console.log('serialized');
done(null, user.idUser);
});
passport.deserializeUser(function (id, done) {
console.log("start of deserialize");
db.user.findOne( { where : { idUser : id } } ).success(function (user) {
console.log("deserialize");
console.log(user);
done(null, user);
}).error(function (err) {
done(err, null);
});
});
//Facebook
passport.use(new FacebookStrategy({
//Information stored on config/auth.js
clientID: configAuth.facebookAuth.clientID,
clientSecret: configAuth.facebookAuth.clientSecret,
callbackURL: configAuth.facebookAuth.callbackURL,
profileFields: ['id', 'emails', 'displayName', 'name', 'gender']
}, function (accessToken, refreshToken, profile, done) {
//Using next tick to take advantage of async properties
process.nextTick(function () {
db.user.findOne( { where : { idUser : profile.id } }).then(function (user, err) {
if(err) {
return done(err);
}
if(user) {
return done(null, user);
} else {
//Create the user
db.user.create({
idUser : profile.id,
token : accessToken,
nameUser : profile.displayName,
email : profile.emails[0].value,
sex : profile.gender
});
//Find the user (therefore checking if it was indeed created) and return it
db.user.findOne( { where : { idUser : profile.id } }).then(function (user, err) {
if(user) {
return done(null, user);
} else {
return done(err);
}
});
}
});
});
}));
/* FACEBOOK STRATEGY */
// Redirect the user to Facebook for authentication. When complete,
// Facebook will redirect the user back to the application at
// /auth/facebook/callback//
app.get('/auth/facebook', passport.authenticate('facebook', { scope : ['email']}));
/* FACEBOOK STRATEGY */
// Facebook will redirect the user to this URL after approval. Finish the
// authentication process by attempting to obtain an access token. If
// access was granted, the user will be logged in. Otherwise,
// authentication has failed.
app.get('/auth/facebook/callback',
passport.authenticate('facebook', { failureRedirect: '/' }),
function (req, res) {
// Successful authentication, redirect home.
res.redirect('../../app.html');
});
app.get('/', function (req, res) {
res.redirect('/');
});
app.get('/app', isLoggedIn, function (req, res) {
res.redirect('app.html');
});
app.post('/meal', function (req, res) {
//Testing Logs
/*console.log(req.body.foodId);
console.log(req.body.quantity);
console.log(req.body.period);
console.log(req.body);
*/
//Check whether or not this is the first food a user drops on the diet
var dietId = -1;
db.diet.findOne( { where : { userIdUser : req.user.idUser } } ).then(function (diet, err) {
if(err) {
return done(err);
}
if(diet) {
dietId = diet.idDiet;
} else {
db.diet.create( { userIdUser : req.user.idUser }).then(function (diet) {
dietId = diet.idDiet;
});
}
});
db.meal.create({
foodId : req.body.foodId,
quantity : req.body.quantity,
period : req.body.period
}).then(function (meal) {
console.log(meal.mealId);
res.json({ mealId : meal.mealId});
});
});
From what I read on the documentation for Passport, the deserializeUser function that I implemented should be called whenever I use req.user, however, with my console.logs(), I found out that serializeUser is called after logging in, therefore it is storing my session, but deserializeUser is never called! Ever.
Any idea on how to get around this? Any help is appreciated, thank you!
You need the express session middleware before calling passport.session(). Read the passportjs configuration section on documentation for more info.
Make sure to set cookieParser and express-session middlewares, before setting passport.session middleware:
const cookieParser = require('cookie-parser')
const session = require('express-session')
app.use(cookieParser());
app.use(session({ secret: 'secret' }));
app.use(passport.initialize());
app.use(passport.session());
To test if passport session is working or not, use:
console.log(req.session.passport.user)
(put in on a middleware for example)
In my case, i was using LocalStrategy and i was thinking i can protect and endpoint with simple username and password as form parameters, and i though passport will only use form parameters when it can't find user in session. but it was wrong assumption. in passport localStrategy, you should have separate endpoints for login and protected endpoint.
So Make sure you're using right middlewares for each endpoints. in my case:
wrong:
Protected endpoint:
app.get('/onlyformembers', passport.authenticate('local'), (req, res) => {
res.send({"res": "private content here!"})
})
correct :
Login:
app.post('/login', passport.authenticate('local'), (req, res) => {
res.send('ok')
})
Protected endpoint:
var auth = function (req, res, next) {
if (req.isAuthenticated())
return next();
res.status(401).json("not authenticated!");
}
app.get('/onlyformembers', auth, (req, res) => {
res.send({"res": "private content here!"})
})

Resources