koa-router doesn't work with passport-local strategy - passport.js

passport.authenticate from koa-passport doesn't work with local strategy. I use no sessions, no serializing. It's just simple passport example, but the server response is always 404.
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const passport = require('koa-passport');
const Router = require('koa-router');
const LocalStrategy = require('passport-local').Strategy;
const app = new Koa();
app.use(bodyParser());
app.use(passport.initialize());
passport.use(new LocalStrategy({
session: false,
}, (email, password, done) => {
if(email && password){
return done(null, [email, password]);
}
else {
return done(null, false);
}
}
));
const router = new Router();
router.post('/auth', async (ctx) => {
passport.authenticate('local', { session: false }, (err, user) => {
if(!user)
return ctx.body = 'Failed!';
return ctx.body = { msg: 'Success' };
});
});
app.use(router.routes());
app.listen(3000, console.log('The server is started'));

The answer is in a depth of an issues on github repo of koa-router:
We should return passport.authenticate with passed context object:
router.post('/auth', async (ctx) => {
return passport.authenticate('local', { session: false }, (err, user) => {
if(!user)
return ctx.body = 'Failed!';
return ctx.body = { msg: 'Success' };
})(ctx, next);
});

I had the same issue and understood that the context object must be returned back for it to parse the response.
router.post('/auth', async (ctx) => {
return passport.authenticate('local', { session: false }, (err, user) => {
if(!user)
return ctx.body = 'Failed!';
return ctx.body = { msg: 'Success' };
})(ctx);
});

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.

Where did I go wrong with passport-jwt setup?

I tried to add passport-jwt to my MEVN-stack application, login is successful, but when app tries to redirect to home page after login, I get 401 Unauthorized error in console. I pass token as value of Authorization header in get request on the home page, but it did not help.
This is the code of server.js (the entrypoint of server side):
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const cors = require('cors');
const morgan = require('morgan');
const fs = require('fs');
const jwt = require('jsonwebtoken');
const passport = require('passport');
const passportJWT = require('passport-jwt');
const ExtractJWT = passportJWT.ExtractJwt;
const JWTStrategy = passportJWT.Strategy;
const jwtOptions = {};
jwtOptions.jwtFromRequest = ExtractJWT.fromAuthHeaderWithScheme('jwt');
jwtOptions.secretOrKey = 'movieratingapplicationsecretkey';
const app = express();
const router = express.Router();
const User = require('./models/User');
app.use(morgan('combined'));
app.use(bodyParser.json());
app.use(cors());
app.use(passport.initialize());
passport.use(new JWTStrategy(jwtOptions, (jwt_payload, done) => {
User.findOne({ id: jwt_payload.id }, (err, user) => {
if (err) {
return done(err, false);
}
if (user) {
return done(null, user);
} else {
return done(null, false);
}
});
}));
mongoose.connect('mongodb://localhost/movie_rating_app', {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log('Connection is established');
})
.catch((err) => {
console.error(`App starting error: ${err.stack}`);
});
// include controllers
fs.readdirSync('controllers').forEach(file => {
if (file.substr(-3) === '.js') {
const route = require(`./controllers/${file}`)
route.controller(app)
}
})
router.get('/', (req, res) => {
res.json({ message: 'API was initialized!' });
});
const port = process.env.API_PORT || 8081;
app.use('/', router);
app.listen(port, () => {
console.log(`api running on port ${port}`);
});
This is the movies.js controllerm, which contains passport.authenticate() method in get request:
const Movie = require('../models/Movie')
const Rating = require('../models/Rating')
const passport = require('passport')
module.exports.controller = app => {
// fetch all movies
app.get(
'/movies',
passport.authenticate('jwt', { session: false }),
(req, res) => {
Movie.find({}, 'name description release_year genre', (error, movies) => {
if (error) console.error(error)
res.send(movies);
})
})
// fetch a single movie
app.get('/movies/:id', (req, res) => {
Movie.findById(req.params.id, 'name description release_year genre', (error, movie) => {
if (error) console.error(error)
res.send(movie);
})
})
}
This is the users.js controller:
const User = require('../models/User');
const passportJWT = require('passport-jwt');
const jwt = require('jsonwebtoken');
const ExtractJwt = passportJWT.ExtractJwt;
const jwtOptions = {};
jwtOptions.jwtFromRequest = ExtractJwt.fromAuthHeaderWithScheme('jwt');
jwtOptions.secretOrKey = 'thisisthesecretkey';
module.exports.controller = app => {
// register a user
app.post('/users/register', (req, res) => {
const newUser = new User({
name: req.body.name,
email: req.body.email,
password: req.body.password,
})
User.createUser(newUser, (error, user) => {
if (error) {
res.status(422).json({
message: 'Something went wrong. Please try again after some time'
})
}
res.send({ user });
})
})
// login user
app.post('/users/login', (req, res) => {
if (req.body.email && req.body.password) {
const email = req.body.email,
password = req.body.password;
User.getUserByEmail(email, (err, user) => {
if (!user) {
res.status(404).json({ message: 'The user does not exist' })
} else {
User.comparePassword(password, user.password, (error, isMatch) => {
if (error) throw error;
if (isMatch) {
const payload = { id: user.id };
const token = jwt.sign(payload, jwtOptions.secretOrKey);
res.json({ message: 'ok', token })
} else {
res.status(401).json({ message: 'The password is incorrect' })
}
})
}
})
}
})
}
This is the script part of Home.vue, when I try to receive response:
<script>
import axios from 'axios';
import MovieCard from '#/components/MovieCard.vue';
export default {
name: 'Home',
components: {
MovieCard,
},
data: () => ({
movies: [],
}),
mounted() {
this.fetchMovies();
},
methods: {
async fetchMovies() {
const token = window.localStorage.getItem('auth');
return axios({
method: 'get',
url: 'http://localhost:8081/movies',
headers: {
Authorization: `JWT ${token}`,
'Content-Type': 'application/json',
},
})
.then((response) => {
console.log(response);
});
// return axios.get('http://localhost:8081/movies')
// .then(({ data }) => {
// this.movies = data;
// })
// .catch((error) => {
// console.error(error);
// });
},
},
};
</script>
Please don't mark my question as duplicate, because I have already tried some advices such as change ExtractJWT.fromAuthHeaderWithScheme('jwt') to ExtractJWT.fromAuthHeaderWithScheme('bearer'), but it did not help me.
How to fix 401 error?
I replaced all ExtractJwt.fromAuthHeaderWithScheme('jwt') with ExtractJwt.fromAuthHeaderAsBearerToken(), and set all jwtOptions.secretOrKey values as 'thisisthesecretkey'. 401 error does not exist now

How to test authentication with jwt inside a cookie with supertest, passport, and JEST

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!

NodeJs JWT Passport

I use nodejs with passport Auth JWT. I can create the JWT Token but If I secure my route with passport.authenticate('jwt') it's not work and i have an errors.
My Error :
TypeError: Cannot read property '_id' of undefined
at JwtStrategy._verify (D:\Programes\nodejs\node\CRUD_NodeAngular5\NodeServer\config\passport.js:15:39)
at D:\Programes\nodejs\node\CRUD_NodeAngular5\NodeServer\node_modules\passport-jwt\lib\strategy.js:110:26
at D:\Programes\nodejs\node\CRUD_NodeAngular5\NodeServer\node_modules\passport-jwt\node_modules\jsonwebtoken\verify.js:27:18
at _combinedTickCallback (internal/process/next_tick.js:131:7)
at process._tickCallback (internal/process/next_tick.js:180:9)
Passport.js
var JwtStrategy = require('passport-jwt').Strategy;
var ExtractJwt = require('passport-jwt').ExtractJwt;
var User = require('../models/User');
var config = require('./database');
module.exports = function(passport) {
var opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeader();
opts.secretOrKey = config.secret;
passport.use(new JwtStrategy(opts, function(jwt_payload, done) {
User.findById(jwt_payload.$__._id, function(err, user) {
if (err) {
return done(err, false);
}
if (user) {
done(null, user);
} else {
done(null, false);
}
});
}));
};
Login
router.post('/login', function(req, res) {
User.findOne({ email: req.body.email }, function(err, user) {
if (err) throw err;
if (!user) {
res.send({ success: false, message: 'Authentication failed. User not found.' });
console.log('User not found');
} else {
user.comparePassword(req.body.password, function(err, isMatch) {
if (isMatch && !err) {
var token = jwt.sign(user.toJSON(), config.secret, {expiresIn: 1000 });
var decoded = jwt.decode(token);
console.log(decoded);
res.json({ success: true, token:'JWT ' + token, decoded: decoded });
console.log('Connected : ' + token);
} else {
res.send({ success: false, message: 'Authentication failed. Passwords did not match.' });
console.log('Password is wrong');
}
});
}
});
});
Route Dashboard Not work
router.get('/dashboard', passport.authenticate('jwt', { session: false }), function(req, res) {
res.send('It worked! User id is: ' );
});
lets clean your code it's simple :-
Step1: create config file and load where you keep your routes
var checkAuth = require('../config/check-auth');
Step2: copy paste it in check-auth
const jwt = require('jsonwebtoken');
module.exports = (req,res,next)=>{
try{
const token = req.headers.authorization.split(" ")[1];
const decoded = jwt.verify(token,"secret");
req.userData = decoded;
next();
}catch(error){
return res.status(401).json({message:'Auth failed'});
}
}
step 3 :protect your route
router.get('/dashboard',checkAuth , { session: false }),
function(req, res) {
res.send('It worked! User id is: ' );
});
protect all your routes by passing checkAuth as parameter

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

Resources