How to test authentication with jwt inside a cookie with supertest, passport, and JEST - node.js

Hey guys I am currently am trying to do something similar to what is posted here:
How to authenticate Supertest requests with Passport?
as I would like to test other endpoints that require authentication but in addition need to pass in a jwt. Right now, I tested it on POSTMAN and on the browser and it seems like it's working fine, but my test cases keep on breaking. I have a login POST route that is setup like so:
AccountService.js
// Login POST route
router.post('/account_service/login', (req, res, next) => {
passport.authenticate('local-login', (err, user, info) => {
try {
if (err) {
const error = new Error('An Error occurred: Cannot find user');
return next(error);
} else if (!user) {
return res.redirect('/account_service/login');
}
req.login(user, { session: false }, (error) => {
if (error) {
return next(error);
}
const email = req.body.email;
const role = req.user[0].role;
const id = req.user[0].id;
const user = {
email: email,
role: role,
id: id
};
const accessToken = jwt.sign(user, config.ACCESS_TOKEN_SECRET, {
expiresIn: 28800 // expires in 8 hours
});
const cookie = req.cookies.cookieName;
if (cookie === undefined) {
// set a new cookie
console.log('setting new cookie');
res.cookie('jwt', accessToken, { maxAge: 900000, httpOnly: true });
res.send({ token: accessToken });
} else {
// cookie was already present
console.log('cookie exists', cookie);
}
res.redirect('/account_service/profile');
});
} catch (error) {
return next(error);
}
})(req, res, next);
});
After the user is authenticated, I assign a JSON web token to the user and place it in the cookie so it gets stored within the headers for authorized requests. Here is an example:
AccountService.js
// Get all users
router.get('/account_service/all_users', passport.authenticate('jwt', { session: false }), (req, res, next) => {
const sql = 'select * from user';
const params = [];
db.all(sql, params, (err, rows) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
res.json({
message: 'success',
data: rows
});
});
});
I use passport.authenticate to ensure that the jwt is valid. This GET request only works after I login with admin user account.
Within my passport file I have it setup like so:
passport.js
const LocalStrategy = require('passport-local').Strategy;
const db = require('../database.js');
const bcrypt = require('bcrypt');
const config = require('../config/config.js');
const JwtStrategy = require('passport-jwt').Strategy;
const cookieExtractor = function (req) {
var token = null;
if (req && req.cookies) token = req.cookies.jwt;
return token;
};
module.exports = function (passport) {
passport.serializeUser(function (user, done) {
done(null, user);
});
passport.deserializeUser(function (user, done) {
done(null, user);
});
passport.use('local-login', new LocalStrategy({
usernameField: 'email',
passwordField: 'password',
passReqToCallback: true
}, (req, email, password, done) => {
try {
const sql = `select * from user WHERE email = "${email}"`;
const params = [];
db.all(sql, params, (err, row) => {
if (err) {
return done(err);
}
if (!row.length || !bcrypt.compareSync(password, row[0].password)) {
return done(null, false, req.flash('loginMessage', 'Inavalid username/password combination. Please try again.'));
}
return done(null, row);
});
} catch (error) {
return done(error);
}
}));
const opts = {};
opts.jwtFromRequest = cookieExtractor; // check token in cookie
opts.secretOrKey = config.ACCESS_TOKEN_SECRET;
// eslint-disable-next-line camelcase
passport.use(new JwtStrategy(opts, function (jwtPayload, done) {
try {
const sql = `select * from user WHERE email = "${jwtPayload.email}"`;
const params = [];
db.all(sql, params, (err, row) => {
if (err) {
return done(err);
}
if (!row.length || !bcrypt.compareSync('admin', jwtPayload.role)) {
return done(null, false, { message: '403 Forbidden' });
}
return done(null, row);
});
} catch (error) {
return done(error);
}
}));
};
Here's where I get confused as my test cases break. I am trying to login before my test cases to allow my other test cases to run but I end up getting a 401 error. Here are my test cases:
accountservice.test.js
const app = require('../../app');
const supertest = require('supertest');
const http = require('http');
describe('Account Service', () => {
let server;
let request;
beforeAll((done) => {
server = http.createServer(app);
server.listen(done);
request = supertest.agent(server);
request.post('/account_service/login')
.send({ email: 'admin#example.com', password: 'admin' })
.end(function (err, res) {
if (err) {
return done(err);
}
console.log(res);
done();
});
});
afterAll((done) => {
server.close(done);
});
it('Test request all users endpoint | GET request', async done => {
const response = await request.get('/account_service/all_users');
expect(response.status).toBe(200);
expect(response.body.message).toBe('success');
expect(response.body.data.length).toBe(3);
done();
});
});
But my test cases fail as I get a 401 error when it expects a 200 success code.
I tried thinking of a way to extract the jwt from a cookie after the login call so that I can set up the headers for the /account_service/all_users GET request code but was unable to find a way using Supertest. I saw this post: Testing authenticated routes with JWT fails using Mocha + supertest + passport but saw that it gets the token from the body.

After messing around with my code, I ended up having issues with in-memory storage and running asynchronous db.run functions that would call every time I ran my server. So I used a file to store my data and ran my tests again and it ended up working!
Here was the faulty code:
const sqlite3 = require('sqlite3').verbose();
const md5 = require('md5');
const DBSOURCE = ':memory:';
const db = new sqlite3.Database(DBSOURCE, (err) => {
if (err) {
// Cannot open database
console.error(err.message);
throw err;
} else {
db.run(`CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name text,
email text UNIQUE,
password text,
status text,
comments text,
photos text,
CONSTRAINT email_unique UNIQUE (email)
)`,
(err) => {
if (err) {
// Table already created
console.log('Table already created');
} else {
// Table just created, creating some rows
const insert = 'INSERT INTO user (name, email, password, status, comments, photos) VALUES (?,?,?,?,?,?)';
db.run(insert, ['user_delete', 'user_delete#example.com', md5('admin123456'), 'pending_deleted', 'comment1,comment2', 'https://giphy.com/gifs/9jumpin-wow-nice-well-done-xT77XWum9yH7zNkFW0']);
db.run(insert, ['user_no_delete', 'user#example.com', md5('user123456'), 'active', 'comment1', 'https://giphy.com/gifs/cartoon-we-bare-bears-wbb-NeijdlusjcduU']);
db.run(insert, ['mikey', 'mikey#example.com', md5('mikey123'), 'pending_deleted', 'comment1', 'https://giphy.com/gifs/wwe-shocked-vince-mcmahon-gdKAVlnm3bmKI']);
}
});
}
});
module.exports = db;
I simply stored this data within a file and used this code instead:
const sqlite3 = require('sqlite3').verbose();
const DBSOURCE = 'mockdb.sqlite';
// Data inserted inside file
/*
db.run(insert, ['user_delete', 'user_delete#example.com', bcrypt.hashSync('admin123456', saltRounds), 'pending_deleted', 'comment1,comment2', 'https://giphy.com/gifs/9jumpin-wow-nice-well-done-xT77XWum9yH7zNkFW0', bcrypt.hashSync('user', saltRounds)]);
db.run(insert, ['user_no_delete', 'user#example.com', bcrypt.hashSync('user123456', saltRounds), 'active', 'comment1', 'https://giphy.com/gifs/cartoon-we-bare-bears-wbb-NeijdlusjcduU', bcrypt.hashSync('user', saltRounds)]);
db.run(insert, ['mikey', 'mikey#example.com', bcrypt.hashSync('mikey123', saltRounds), 'pending_deleted', 'comment1', 'https://giphy.com/gifs/wwe-shocked-vince-mcmahon-gdKAVlnm3bmKI', bcrypt.hashSync('user', saltRounds)]);
db.run(insert, ['admin', 'admin#example.com', bcrypt.hashSync('admin', saltRounds), 'active', 'admincomments', 'adminphoto', bcrypt.hashSync('admin', saltRounds)]);
console.log('last hit in database');
});
*/
const db = new sqlite3.Database(DBSOURCE, (err) => {
if (err) {
// Cannot open database
console.error(err.message);
throw err;
}
console.log('Connection successful!');
});
module.exports = db;
I also ended up using supertest.agent.
const app = require('../../app');
const supertest = require('supertest');
const http = require('http');
const db = require('../../database/database.js');
describe('Account Service', () => {
let server;
let request;
// Find cookie management option.
beforeAll(async (done) => {
server = http.createServer(app);
server.listen(done);
request = supertest.agent(server);
done();
});
And it ended up working and successfully solving my issue!

Related

How to add user in req.body using passport js?

Without passport, I have added simple middleware to authorize user as below.
const jwt = require('jsonwebtoken');
module.exports = (req, res, next) => {
const authHeader = req.get('Authorization');
const token = authHeader.split(' ')[1];
let jwttoken;
try {
jwttoken = jwt.verify(token, 'secret');
} catch (err) {
err.statusCode = 500;
throw err;
}
if (!jwttoken) {
const error = new Error('Not authenticated.');
error.statusCode = 401;
throw error;
}
req.userId = jwttoken.userId;
next();
};
with passport, I have added middleware as below
const options = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'SECRET'
}
module.exports = (passport) => {
passport.use(new Strategy(options, async (payload, done) => {
await user.findByPk(payload.userId).then(user => {
if (user) {
return done(null, user);
}
return done(null, false);
}).catch(error => {
return done(null, error);
});
}));
}
Question: like I was adding user to request without passport as req.userId = jwttoken.userId, how can we do it with passport middleware?
At passport this is accomplished with that line of code on your module.exports:
return done(null, user);
This tells passport that the information should be sent to the next function callback at req.user.
So for example, if you "user.findByPk(payload.userId)" response is something like:
{
"name": <NAME>
"profile": <profile>
}
At your protected endpoint's callback, you should see it on req.user.
For example:
app.post('/profile', passport.authenticate('jwt', { session: false }),
function(req, res) {
res.send(req.user.profile);
}
);
With req.user.profile being equal to of the "user.findByPk(payload.userId)" response.

Issue in executing callback and saving data to mysql db

I am trying to authenticate using google plus api and trying to save the google user details in the callback function from google plus api but due to some reason i am unable to pass values from google plus api response to callback request.
Snippet from Router.js
router.get("/auth/google", passport.authenticate('google', { scope: ['profile','email'] }));
router.get("/auth/google/callback" , passport.authenticate('google') , googleInsert);
Snippet from User.controller.js
const {
getGoogleUserByEmail,
createGoogleUser,
} = require("./user.service.js");
module.exports = {
googleInsert: (req, res) => {
body = req.body;
body.googleId = req.body.profile.id;
body.firstName = req.body.profile.name.givenName;
body.lastName = req.body.profile.name.familyName;
body.email = req.body.profile.emails[0].value;
body.photoUrl = req.body.profile.photos[0].value;
//const salt = genSaltSync(10);
//body.password = hashSync(body.password, salt);
//verify if email id exists
getGoogleUserByEmail(body.email, (err, results) => {
if (err) {
console.log(err);
}
//Email id already registered and exists in db
if (results) {
console.log("Google Email already exists");
return res.status(409).json({
success: 0,
data: "Google Email already exist",
});
}
console.log(
"Google Email id is not registered, proceed with Google User Insert"
);
if (!results) {
createGoogleUser(body, (err, createResults) => {
console.log(body);
if (err) {
console.log(err);
return res.status(500).json({
success: 0,
message: "Database connection error",
});
}
if (createResults.affectedRows == 1) {
console.log("inside succcess is 1");
//Insert into UserRole Table
createUserRole(
createResults.insertId,
(body.role_id = 2),
(err, results) => {
console.log(results);
if (err) {
console.log(err);
return res.status(500).json({
success: 0,
message: "DB error",
});
}
if (!err) {
console.log("Google User created successfully");
return res.status(200).json({
success: 1,
data: createResults,
});
}
}
);
}
});
}
});
},
};
Passport.js
const passport = require("passport");
const GoogleStrategy = require("passport-google-oauth20").Strategy;
const keys = require('../config/keys');
passport.use(
new GoogleStrategy(
{
clientID: process.env.clientID,
clientSecret: process.env.clientSecret,
callbackURL: "http://localhost:1111/api/users/auth/google/callback",
//passReqToCallback: true
},
(accessToken, refreshToken, profile, callback) => {
console.log("access token", accessToken);
console.log("refresh token", refreshToken);
console.log("profile", profile);
console.log("callback", callback);
}
)
);
The issue i am getting is i am not sure how to get the value from google authentication to be used in callback request and am not sure if callback is working or not. After i select the email id from google plus api client screen, it just goes into infinite loop and no data is getting inserted into db.
body = req.body;
body.googleId = req.body.profile.id;
body.firstName = req.body.profile.name.givenName;
body.lastName = req.body.profile.name.familyName;
body.email = req.body.profile.emails[0].value;
body.photoUrl = req.body.profile.photos[0].value;

PUT method not allowed with http-proxy

When calling my PUT method, I got this error:
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html>
<head>
<title>403 Forbidden</title>
</head>
<body>
<h1>Forbidden</h1>
<p>You don't have permission to access this resource.</p>
</body>
</html>
I have some jwt authentification, so I put my complete code here:
It works with GET and POST methods
const express = require('express');
const logger = require('morgan');
var fs = require('fs');
var https = require('https');
var privateKey = fs.readFileSync('/etc/letsencrypt/live/hidden/privkey.pem', 'utf8');
var certificate = fs.readFileSync('/etc/letsencrypt/live/hidden/fullchain.pem', 'utf8');
const util = require('util')
var credentials = {key: privateKey, cert: certificate};
var queryString = require('querystring');
const bodyParser = require('body-parser');
const app = express();
const jwt = require('jsonwebtoken');
// import passport and passport-jwt modules
const passport = require('passport');
const passportJWT = require('passport-jwt');
const bcrypt = require('bcryptjs');
const Sequelize = require('sequelize');
var httpProxy = require('http-proxy');
var apiProxy = httpProxy.createProxyServer();
var backend = 'http://localhost:8484';
// initialize an instance of Sequelize
const sequelize = new Sequelize({
//hiden///
});
// ExtractJwt to help extract the token
let ExtractJwt = passportJWT.ExtractJwt;
// JwtStrategy which is the strategy for the authentication
let JwtStrategy = passportJWT.Strategy;
let jwtOptions = {};
jwtOptions.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
jwtOptions.secretOrKey = 'hidden';
// lets create our strategy for web token
let strategy = new JwtStrategy(jwtOptions, function(jwt_payload, next) {
console.log('payload received', jwt_payload);
let user = getUser({ id: jwt_payload.id });
if (user) {
next(null, user);
} else {
next(null, false);
}
});
// use the strategy
passport.use(strategy);
app.use(passport.initialize());
app.use(logger(':date[iso] :method :url :status :response-time ms - :res[content-length]'));
// parse application/json
app.use(bodyParser.json());
//parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: true }));
// check the databse connection
sequelize
.authenticate()
.then(() => console.log('Connection has been established successfully.'))
.catch(err => console.error('Unable to connect to the database:', err));
// create user model
const User = sequelize.define('user', {
name: {
type: Sequelize.STRING,
allowNull: false
},
code_event: {
type: Sequelize.INTEGER,
allowNull: false
},
password: {
type: Sequelize.STRING,
allowNull: false
},
});
// create table with user model
User.sync()
.then(() => console.log('User table created successfully'))
.catch(err => console.log('did you enter wrong database credentials?'));
// create some helper functions to work on the database
const createUser = async ({ name, password, code_event }) => {
try{
return await User.create({ name, password, code_event });
}
catch (error) {
// your catch block code goes here
}
};
const getAllUsers = async () => {
try{
return await User.findAll();
}
catch (error) {
// your catch block code goes here
}
};
const getUser = async obj => {
try{
return await User.findOne({where: obj,});
}
catch (error) {
// your catch block code goes here
}
};
// get all users
app.get('/users', function(req, res) {
getAllUsers().then(user => res.json(user));
});
// register route
app.post('/register', function(req, res, next) {
console.log(req.body);
let { name, password, code_event } = req.body;
bcrypt.genSalt(10, (err, salt) => {
if(err) throw err;
bcrypt.hash(password, salt,
(err, hash) => {
if(err) throw err;
password = hash;
createUser({ name, password, code_event }).then(user =>
res.json({ user, msg: 'account created successfully' })
)
.catch(err => res.status(400).json(err));
});
});
});
// login route
app.post('/login', async function(req, res, next) {
console.log(req.body);
const { name, password , code_event} = req.body;
if (name && password) {
// we get the user with the name and save the resolved promise returned
let user = await getUser({ name, code_event});
if (!user) {
res.status(401).json({ msg: 'No such user found', user });
}
bcrypt.compare(password, user.password)
.then(isMatch => {
if (isMatch) {
const payload = {id: user._id};
// let token = jwt.sign(payload, jwtOptions.secretOrKey, { expiresIn: 36000 }, (err, token) => {
// if (err) res.status(500).json({ error: "Error signing token", raw: err });
// res.json({ msg: 'ok', token: token });
// });
let token = jwt.sign(payload, jwtOptions.secretOrKey);
res.json({ msg: 'ok', token: token });
} else {
res.status(401).json("Password is incorrect");
}
});
}
});
apiProxy.on( 'proxyReq', ( proxyReq, req, res, options ) => {
console.log("body " +util.inspect(req.body, false, null, true /* enable colors */));
if ( !req.body || !Object.keys( req.body ).length ) {
return;
}
let contentType = proxyReq.getHeader( 'Content-Type' );
let bodyData;
if ( contentType.includes( 'application/json' ) ) {
bodyData = JSON.stringify( req.body );
}
if ( contentType.includes( 'application/x-www-form-urlencoded' ) ) {
bodyData = queryString.stringify( req.body );
}
if ( bodyData ) {
proxyReq.setHeader( 'Content-Length', Buffer.byteLength( bodyData ) );
proxyReq.write( bodyData );
}
});
app.all("/*", passport.authenticate('jwt', { session: false }), function(req, res) {
console.log("req all" + req);
apiProxy.web(req, res, {target: backend});
});
app.on('upgrade', function (req, socket, head) {
console.log("req on" + req);
apiProxy.ws(req, socket, head, {target: backend});
});
var httpsServer = https.createServer(credentials, app);
// start the app
httpsServer.listen(8383, function() {
console.log("Express is running on port 3000");
});
It works with my POST methods.
I don't know where it goes because, when calling a method, I should have a trace in my log files. I can see POST ot GET calls but not PUT ones.
Any ideas?

why my koa-passport authenticate parameter user is always undefined?

I'm trying to use koa-passport for koa2, and followed the examples of the author, but i always get "Unauthorized". I used the console.log and found that it even not hit the serializeUser.
var UserLogin = async (ctx, next) =>{
return passport.authenticate('local', function(err, user, info, status) {
if (user === false) {
ctx.body = { success: false }
} else {
ctx.body = { success: true }
return ctx.login(user)
}
})(ctx, next);
};
And then I searched on the web and found another writing of router, it goes to the serializeUser but the done(null, user.id) threw error that "cannot get id from undefined".
let middleware = passport.authenticate('local', async(user, info) => {
if (user === false) {
ctx.status = 401;
} else {
await ctx.login(ctx.user, function(err){
console.log("Error:\n- " + err);
})
ctx.body = { user: user }
}
});
await middleware.call(this, ctx, next)
The auth.js are showed below. Also I followed koa-passport example from the author here and tried to use session, but every request i sent will get a TypeError said "Cannot read property 'message' of undefined". But I think this is not the core problem of authentication, but for reference if that really is.
const passport = require('koa-passport')
const fetchUser = (() => {
const user = { id: 1, username: 'name', password: 'pass', isAdmin: 'false' };
return async function() {
return user
}
})()
const LocalStrategy = require('passport-local').Strategy
passport.use(new LocalStrategy(function(username, password, done) {
fetchUser()
.then(user => {
if (username === user.username && password === user.password) {
done(null, user)
} else {
done(null, false)
}
})
.catch(err => done(err))
}))
passport.serializeUser(function(user, done) {
done(null, user.id)
})
passport.deserializeUser(async function(id, done) {
try {
const user = await fetchUser();
done(null, user)
} catch(err) {
done(err)
}
})
module.exports = passport;
By the way when I use the simple default one, it will just give me a "Not found". But through console.log I can see it actually got into the loginPass.
var loginPass = async (ctx, next) =>{
passport.authenticate('local', {
successRedirect: '/myApp',
failureRedirect: '/'
});
};
In server.js:
// Sessions
const convert = require('koa-convert');
const session = require('koa-generic-session');
app.keys = ['mySecret'];
app.use(convert(session()));
// authentication
passport = require('./auth');
app.use(passport.initialize());
app.use(passport.session());
Thanks a lot for any help!!! :D

Passport.authenticate not sending a response

I'm using Passport for authentication, specifically with a JWT strategy. I'm able to create a new token when a user is created, however, when I use that token in the header of a request to a route that requires authentication, my request just hangs up. I'm using Postman to test these POST/GET requests.
Here's my initial configuration for signing up a user:
const User = require('../db/models/User');
const jwt = require('jsonwebtoken');
function userToken(user) {
return jwt.sign({
id: user.id,
}, process.env.JWT_SECRET);
}
exports.signup = function(req, res, next) {
const email = req.body.email.toLowerCase();
const password = req.body.password.toLowerCase();
User.findOne({
where: { email },
}).then(function(user) {
if (!user) {
User.create({
email,
password,
})
.then(function(user) {
return res.send({ token: userToken(user) });
});
}
if (user) {
return res.send({ message: 'That user is in use' });
}
});
};
Here's my passport configuration:
const passport = require('passport');
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const User = require('../db/models/User');
const jwtOptions = {
jwtFromRequest: ExtractJwt.fromHeader('authorization'),
secretOrKey: process.env.JWT_SECRET,
};
const jwtLogin = new JwtStrategy(jwtOptions, function(payload, done) {
User.findOne({
where: { id: payload.id },
}, function(err, user) {
if (err) { return done(err, false); }
if (user) { return done(null, user); }
return done(null, false);
});
});
passport.use(jwtLogin);
Here's what my protected route looks like:
const passport = require('passport');
const requireAuth = passport.authenticate('jwt', { session: false });
module.exports = function router(app) {
app.get('/', requireAuth, function(req, res) {
res.send({ 'hi': 'there' });
});
};
Here's what I see in my terminal:
Executing (default): SELECT "id", "username", "email", "password", "photo", "createdAt", "updatedAt" FROM "users" AS "user" WHERE "user"."id" = 15;
So I know that it's correctly querying for a user id and searching for it, however, it just hangs up at this point, rather than serving me a response.
Not sure what the issue is, so any and all suggestions are welcomed and appreciated. Thank you!
Realized that because I am using Sequelize, it handles errors with a catch like so:
...
const jwtLogin = new JwtStrategy(jwtOptions, function(payload, done) {
User.findOne({
where: { id: payload.id }
})
.then(user => {
if (user) {
done(null, user);
} else {
done(null, false);
}
})
.catch(err => {
if (err) { return done(err, false); }
});
});
...
This solved my issue and is returning my response.

Resources