Chai test doesn't pass the cookie authentification middleware - node.js

I have a middleware that checks if a user is loggued-in before letting him access a route. It looks like this:
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
const authenticate = (req: Authenticate, res: Response, next: NextFunction) => {
const token = req.cookies;
if (token) {
jwt.verify(token, process.env.JWT_TOKEN_KEY, (error, res) => {
if (error) return res.sendStatus(403);
req.cookie = { _id: res._id, locale: res.locale };
return next();
});
}
return res.sendStatus(401);
};
export default authenticate;
The test is:
import chai from "chai";
import chaiHttp from "chai-http";
import { server } from "../index";
chai.use(chaiHttp);
const api = chai.request(server).keepOpen();
describe("GET /user/:id", () => {
it("return user information", () => {
api
.get("/user/123")
.set("Cookie", "_id=567;locale=en")
.end(function (err, res) {
chai.expect(res).to.have.status(200);
});
});
});
The test works fine if I remove the middleware from the route. So:
// OK
router.post("/user/:id", searchUser);
// NOT OK
router.post("/user/:id", authenticate, searchUser);
The error is:
TypeError: Cannot read property 'sendStatus' of undefined
at /Users/myname/Desktop/Code/myapp/server/src/middleware/authenticate.ts:21:29 at Object.module.exports [as verify] (/Users/myname/Desktop/Code/myapp/server/node_modules/jsonwebtoken/verify.js:57:12)

The res variable of the jwt.verify() callback function overwrites the res variable of the authenticate middleware, which is why you get this error. You should give it a different name to avoid conflicts.
I think you got the req.cookies from chai-http. If you didn't, you probably forgot to use cookie-parser middleware.
E.g.
index.ts:
import express from 'express';
import cookieParser from 'cookie-parser';
import authenticate from './mws/authenticate';
const server = express();
server.use(cookieParser());
server.get('/user/:id', authenticate, (req, res) => {
const { id } = req.params;
res.json({ id, name: 'teresa teng' });
});
export { server };
authenticate.ts:
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
const authenticate = (req: Request, res: Response, next: NextFunction) => {
const token = req.cookies;
console.log('cookies: ', req.cookies);
if (token) {
jwt.verify(token, process.env.JWT_TOKEN_KEY, (error, verifyResponse) => {
if (error) return res.sendStatus(403);
req.cookies = { _id: verifyResponse._id, locale: verifyResponse.locale };
return next();
});
}
return res.sendStatus(401);
};
export default authenticate;
index.test.ts:
import chai from 'chai';
import chaiHttp from 'chai-http';
import { server } from './';
chai.use(chaiHttp);
const api = chai.request(server).keepOpen();
describe('GET /user/:id', () => {
it('return user information', (done) => {
api
.get('/user/123')
.set('Cookie', '_id=567;locale=en')
.end(function (err, res) {
chai.expect(res).to.have.status(200);
done();
});
});
});
logs:
GET /user/:id
cookies: { _id: '567', locale: 'en' }
1) return user information

Related

Im trying to update a user in MongoDB but it's giving me an error "You are not authenticated" why is that?

Im creating a video sharing app that allows users to sign in and update their profiles. i've created an error handler to identify errors in the backend and when im submitting my put request to update a user its going off.
here are my routes that im using
import express from 'express';
import { update} from '../controllers/user.js'
import { verifyToken } from '../verifyToken.js';
const router = express.Router();
router.put("/:id", verifyToken, update);
here is the controller for updating the user
import { createError } from "../error.js";
import User from '../models/User.js'
export const update = async (req, res, next) => {
if(req.params.id === req.user.id) {
try {
const updatedUser = await User.findByIdAndUpdate(req.params.id, {
$set: req.body
}, {new: true})
res.status(200).json(updatedUser)
} catch (error) {
next(error)
}
} else {
return next(createError(403, 'You can only update this account'));
}
}
here is the custom error handling im using which is in the index.js below and error.js
app.use((err, req, res, next) => {
const status = err.status || 500
const message = err.message || "something went wrong"
return res.status(status).json({
success: false,
status,
message
})
})
export const createError = (status, message) => {
const err = new Error()
err.status=status
err.message=message
return err
}
here is the middleware for the token
import jwt from 'jsonwebtoken'
import { createError } from './error.js'
export const verifyToken = (req, res, next) => {
const token = req.cookies.acess_token
if(!token) return next(createError(401, 'You are not authenticated'))
jwt.verify(token, process.env.JWT, (err, user)=>{
if(err) return next(createError(403, "Token invalid"))
req.user = user
next()
})
}

having trouble getting user authorization variables working

In am having trouble passing the token information to my controller for authorization. In the below code, I have console logged the relevant information and I am able to get the token correctly, and the decoded information, but not the req.user information. When I console.log for that information I receive null, and when I console.log for decoded.id, I get undefined. I believe this is what is hanging up my authorization, however I'm not sure what to look at to fix it? Any thoughts very helpful!
Here's a github link:https://github.com/roxanneweber/projectmanager
const jwt = require('jsonwebtoken');
const asyncHandler = require('express-async-handler');
const User = require('../models/userModel');
const protect = asyncHandler(async (req, res, next) => {
let token;
if (
req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')
) {
try {
// Get token from header
token = req.headers.authorization.split(' ')[1];
console.log(token);
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log(decoded);
// Get user from token
req.user = await User.findById(decoded.id).select('-password');
console.log(req.user);
console.log(decoded.id);
next();
} catch (error) {
console.log(error);
res.status(401);
throw new Error('Not authorized');
}
}
if (!token) {
res.status(401);
throw new Error('Not authorized');
}
});
module.exports = { protect };
hi i am handling the token like this in my auth.middleware.ts
import jwt from 'jsonwebtoken';
export default function (req, res, next) {
try {
const token = req.headers.authorization.split(' ')[1];
const decodedToken = jwt.verify(token, process.env.JWT_SECRET);
// add userData object to request
req.userData = {
email: decodedToken.email,
userId: decodedToken.userId,
username: decodedToken.username,
role: decodedToken.role,
};
next();
} catch (error) {
return res.status(401).json({
message: 'not authenticated',
});
}
}
then my frontend handles setting the token like this:
import { HttpInterceptor, HttpRequest, HttpHandler, HttpHeaders } from '#angular/common/http';
import { Injectable } from '#angular/core';
import { AuthenticationService } from '../services/authentication.service';
export interface HttpConfig {
body?: any;
headers?: HttpHeaders;
observe?: any;
}
#Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthenticationService) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
const authToken = this.authService.getToken();
const authRequest = req.clone({
headers: req.headers.set('Authorization', 'Bearer ' + authToken),
});
return next.handle(authRequest);
}
}
then i am using the auth.middleware.ts in my backend route files like this:
import express from 'express';
import authMiddleware from '../middleware/auth.middleware';
import FooController from './foo.controller';
class FooRoutes {
router = express.Router();
fooController = FooController;
constructor() {
this.configureRoutes();
}
configureRoutes() {
this.router.post('/foo/start', authMiddleware, this.fooController.start);
this.router.put('/foo/stop/:id', authMiddleware, this.fooController.stop);
this.router.get('/foo/:userId', authMiddleware, this.fooController.getAll);
this.router.delete('/foo/delete/:id', authMiddleware, this.fooController.delete);
}
}
export default new FooRoutes().router;

How to set a jwt cookie with mocha/chai?

I need to test protected routes with mocha and chai. The authentication middleware crashes all the time with an error 500 because jwt must be a string.
When the user logs-in, he receives an http-only cookie whose data is a jwt token. The cookie is automatically sent to the server on each request thanks to axios withCredentials: true property.
So, I've tried to set a cookie or an auth bearer to my test, but without any success so far. How to fix this?
Here is what I've written:
import chai from "chai";
import chaiHttp from "chai-http";
import { app } from "../index";
import jwt from "jsonwebtoken";
chai.use(chaiHttp);
const api = chai.request(app).keepOpen();
const token = jwt.sign(
{ _id: "123", locale: "en" },
process.env.JWT_TOKEN_KEY,
{
expiresIn: "14d",
}
);
describe("GET /user/:id", () => {
it("return user information", (done) => {
api
.get("/user/123")
.set("Cookie", token)
.end((err, res) => {
chai.expect(res).to.have.status(200);
done();
});
});
});
The middleware is:
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
import { Cookie } from "../models/Cookie";
interface Authenticate extends Request {
cookie: Cookie;
cookies: any;
}
const authenticate = (req: Authenticate, res: Response, next: NextFunction) => {
const token = req.cookies;
if (token) {
jwt.verify(token, process.env.JWT_TOKEN_KEY, (error, res) => {
if (error) {
return res.sendStatus(403);
}
req.cookie = { _id: res._id, locale: res.locale };
return next();
});
}
return res.sendStatus(401);
};
export default authenticate;
I've been stuck on testing my api routes since 48 hours. Any help would be highly appreciated.
req.cookies is an object, from the express docs:
When using cookie-parser middleware, this property is an object that contains cookies sent by the request. If the request contains no cookies, it defaults to {}.
So you need to set the jwt inside the cookie in this way:
.set('Cookie', `jwtToken=${token}`)
And get it in your middleware:
const { jwtToken } = req.cookies;
jwt.verify(jwtToken, ...

How can I pass a JWT from the client side to my API to be properly authenticated?

I need to pass a JWT with GET, POST, PUT, DELETE methods from my UI to my API.
For this I tried to create a front-end using ReactJS. I get:
POST http://localhost:4000/book/ 401 (Unauthorized)
when I try to send a request.
I know I have to pass my token, which I did via postman when I send data to mongo DB.
I'm new to MERN stack, so I am still trying to understand.
This is the book controller
import mongoose from 'mongoose';
import { book_schema } from '../model/book_model';
const Book = new mongoose.model('Book', book_schema);
export const add_new_book = (req, res) => {
let new_book = new Book(req.body);
new_book.save((err, book) => {
if (err) {
res.send(err);
}
res.json(book);
});
};
These are the book routes
import {add_new_book} from '../controller/book_controller';
import {authorization} from '../controller/user_controller';
const book_routes = (app) => {
//GET
app.route('/book')
//POST
.post(authorization,add_new_book);
}
This is the user controller
import mongoose from 'mongoose';
import bycrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { user_schema } from '../model/user_model';
const User = mongoose.model('User', user_schema);
export const register = (req, res) => {
const new_user = new User(req.body);
new_user.hash_password = bycrypt.hashSync(req.body.password, 10);
new_user.save((err, user) => {
if (err) {
return res.status(400).send({
message: err
});
} else {
user.hash_password = undefined;
return res.json(user);
}
});
};
export const authenticate = (req, res) => {
User.findOne({
user_name: req.body.user_name
}, (err, user) => {
if (err) throw err;
if (!user) {
res.status(401).json({ message: 'Authentication Failed ! - No User.' })
} else if (user) {
if (!user.comparePassword(req.body.password, user.hash_password)) {
res.status(401).json({ message: 'Authentication Failed ! - Wrong Password.' })
} else {
var token = res.json({
token: jwt.sign({ user_name: user.user_name }, 'RESTFULAPIs', { expiresIn: '24h' })
});
//$window.sessionStorage.accessToken = response.body.access_token;
return token;
}
}
});
};
export const authorization = (req, res, next) => {
if (req.user) {
next();
} else {
return res.status(401).json({ message: 'Unauthorized User !' })
}
};
export const de_authenticate = (req, res) => {
};
This is the server
import express from 'express';
import cors from 'cors';
import bodyParser from 'body-parser';
import mongoose from 'mongoose';
import jsonwebtoken from 'jsonwebtoken';
import db_config from './config_db.js';
const app = express();
const PORT = 4000;
//import routes here
import book_routes from './api/route/book_route';
import user_routes from './api/route/user_route';
import item_routes from './api/route/item_route';
//mongo DB connection
mongoose.Promise = global.Promise;
mongoose.connect(db_config.DB, { useNewUrlParser: true }).then(
() => { console.log('Database is connected') },
err => { console.log('Can not connect to the database' + err) }
);
app.use(cors());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
//JWT setup
app.use((req, res, next) => {
if (req.headers && req.headers.authorization && req.headers.authorization.split(' ')[0] === 'JWT') {
jsonwebtoken.verify(req.headers.authorization.split(''), [1], 'RESTFULAPIs', (err, decode) => {
if (err) req.user = undefined;
req.user = decode;
//console.log(req.user );
//console.log(decode);
next();
});
} else {
req.user = undefined;
next();
}
});
//to app
book_routes(app);
user_routes(app);
item_routes(app);
app.get('/', (req, res) =>
res.send(`Node Server and Express Server Running on Port : ${PORT}`)
);
app.listen(PORT, function () {
console.log(`Server is running on Port: ${PORT}`);
});
I developed front end using ReactJS and import axios for access API via URL. In insert_book.js file my form submission function is look like this,
onSubmit(e) {
e.preventDefault();
const book_obj = {
book_no:this.unique_id_generator(),
isbn_no: this.state.isbn_no,
author: this.state.author,
published_year: this.state.published_year,
publisher: this.state.publisher,
category:this.state.category
};
axios.post('http://localhost:4000/book/', book_obj)
.then(res => console.log(res.data));
this.setState({
isbn_no: '',
author:'',
published_year: '',
publisher: '',
category:''
})
}
Lastly I would like to provide user routes code,
import {register,
authenticate,
authorization
} from '../controller/user_controller';
const user_routes = (app) => {
//SIGNUP
app.route('/auth/signup')
//POST A USER
.post(register);
//SIGNIN
app.route('/auth/signin')
//AUTHENTICATE USER
.post(authenticate);
}
export default user_routes;
Here are the areas where I am stuck:
How could I store these tokens in sessions to send with future requests?
How to attach and send token value with GET, POST, PUT, DELETE methods?
so to store the token in local storage:
const user: User = getUserFromBackend()
// assuming user is an object
localStorage.setItem('token', user.token);
// get token
const token = localStorage.getItem('token');
to make a request with token header:
const token = localStorage.getItem('token');
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify(data), // body data type must match "Content-Type" header
})
.then(response => response.json())
Although late to answer. I'd suggest storing JWT in cookie if possible. Then you just read the cookie and pass JWT in headers for the request where you need authentication. I prefer to call the header key x-api-token and set its value to the JWT string.
Moreover, you just need to authenticate the user once from your backend by decrypting this JWT and verifying. Once JWT is verified, you can create a session for authenticated user. This session expires in half hour or whatever you've configured in your App.
This way you don't need to send JWT again as the session is valid. I'm not saying that sending JWT is not good and sometimes required in applications where you don't create a session for authenticated user. This happens when performing operations requiring one time authentications like payment processing.
Implementing this logic shouldn't be a problem using node's JWT-decode package (https://www.npmjs.com/package/jwt-decode).
Hope this helps.

Simple Mock Function of Passport using Jest

I am currently unit testing all my routes, including some that are using a custom passport authentication function. I am trying to mock the passport function to test error handling, but I keep getting the error:
TypeError: _passport.default.authenticate(...) is not a function
Here is the actual code that runs in /controllers/users.js:
export const persistentLogin = (req, res, next) => {
// Authenicate the cookie sent on the req object.
passport.authenticate('jwt', { session: false }, async (authErr, user) => {
// If there is an system error, send 500 error
if (authErr) return res.sendStatus(500);
// If no user is returned, send response showing failure.
if (!user) {
return res.status(200).json({
success: 'false',
});
}
})(req, res, next);
};
Here is the testing code in /tests/controllers/users.js:
import passport from 'passport';
import { persistentLogin } from '../../controllers/users';
beforeEach(() => {
mockResponse = () => {
const response = {};
response.status = jest.fn().mockReturnValue(response);
response.json = jest.fn().mockReturnValue(response);
response.sendStatus = jest.fn().mockReturnValue(response);
response.clearCookie = jest.fn().mockReturnValue(response);
response.cookie = jest.fn().mockReturnValue(response);
return response;
};
});
/**
* persistentLogin Tests
*/
describe('Persistent Login Controller', () => {
beforeEach(() => {
req = {};
res = mockResponse();
validateLoginForm.mockClear();
bcrypt.compare.mockClear();
});
// Passport authenication error
test('Should show passport authenication error', async () => {
passport.authenticate = jest.fn((authType, options, callback) => callback('This is an error', null));
await persistentLogin(req, res);
expect(passport.authenticate).toHaveBeenCalledTimes(1);
expect(res.sendStatus).toHaveBeenCalledWith(500);
});
});
If I had to guess, I would say it has something to do with how the (req, res, next) objects are passed into the live function after the fact. But since we are just mocking the function, I am not sure if it actually needs access to those objects.
EDIT #1:
Per the comment from #jakemingolla, I am now thinking it may be because Jest is not running my app.js file which defines my custom JWT strategy.
Here is the code from the /app.js file:
import passport from 'passport';
import passportJWTStrategy from './utils/auth/passport';
app.use(passport.initialize());
passportJWTStrategy(passport);
And the code from the /utils/auth/passport.js file:
import { Strategy } from 'passport-jwt';
/**
* Verifies JWT payload
*
* #param passport The instance of passport module.
*/
export default (passport) => {
const JWTStrategy = Strategy;
// Setup Options Object
const opts = {};
opts.jwtFromRequest = req => req.cookies.jwt;
opts.secretOrKey = process.env.PASSPORT_SECRET;
passport.use(
new JWTStrategy(opts, (jwtPayload, done) => {
if (Date.now() > jwtPayload.expire_date) {
return done('jwt expired');
}
return done(null, jwtPayload);
}),
);
};
You just need a small change:
Your mock for passport.authenticate just needs to return a function:
passport.authenticate = jest.fn((authType, options, callback) => () => { callback('This is an error', null); });
In the question you mock passport.authenticate, but in this case verify function of your strategy is not called. If you want to run this function as well or mock specific strategy then try something like this:
sinon
.stub(passport._strategies.google, 'authenticate')
.callsFake(function verified() {
const self = this;
this._verify(
null,
null,
{
_json: { email: faker.internet.email() },
name: {
givenName: faker.name.firstName(),
familyName: faker.name.lastName(),
},
},
(err, user, info) => {
if (err) {
return self.error(err);
}
if (!user) {
return self.fail(info);
}
return self.success(user, info);
}
);
});
const response = await supertest(app)
.get('/google/callback?code=123');

Resources