Passport authentication in Chai tests - node.js

This is my first project using passport and I am new to Node in general. I am having trouble getting an endpoint recognized as authorized when running integration tests using Chai. The endpoint is
protected by token authentication using the passport and jsonwebtoken libraries. Here are the other files imported to the tests;
const chai = require('chai');
const chaiHttp = require('chai-http');
const mongoose = require('mongoose');
const should = chai.should();
const { Session } = require('../models/practiceSession' );
const {User} = require('../models/user');
const { app, runServer, closeServer } = require('../app');
const { TEST_DATABASE_URL } = require('../config/mainConfig');
const {secret} = require('../config/mainConfig');
const jwt = require('jsonwebtoken');
chai.use( chaiHttp );
Here is the test for the endpoint;
//test the session endpoints
it( 'should return a session with proper request', function(){
const user = new User({
name: 'random2',
email: 'random2#random.com',
password: 'Password2'
});
user.save( (err) => {
if (err){
console.log( err.message);
}
});
const token = jwt.sign({
id: user._id,
userName: user.name,
level: 0
}, secret, {
expiresIn: 60 * 60
});
console.log( "user id: ", user._id);
console.log( 'token is: ', token );
const practiceRequest = {
operation: "+",
number: "10", ///this will possible need to be sent as a number
min: "1",
max: "200"
};
return chai.request(app)
.post('/api/session')
.set('Authorization', 'Bearer ' + token)
.send( practiceRequest )
.then( (res) => {
res.should.have.status(201);
})
});
});
The two console.log statements confirm that the user is being created and has an _id property, and that a valid token is being
created (this was confirmed on jwt.io).
This is the code for the endpoint;
router.route("/session")
.post(passport.authenticate('jwt', { session: false }), jsonParser, ( req, res ) => {
console.log( req );
let practiceSession = [];
for ( let i = 0; i < req.body.number; i++ ){
let firstTerm = generateTerm( req.body.min, req.body.max );
let secondTerm = generateTerm( req.body.min, req.body.max );
const problem = {
operator: req.body.operation,
firstTerm,
secondTerm,
problem: `${ firstTerm } ${ req.body.operation } ${ secondTerm }`,
correctResponse: generateCorrectResponse( firstTerm, secondTerm, req.body.operation )
};
practiceSession.push( problem );
}
// save session into db sessions collection for use in the training-logic.js
Session
.create( {
userId: req.user._id,
problems: practiceSession
} )
.then(
session => res.status( 201 ).json( session) )
.catch( err => {
console.error( err );
res.status( 500 ).json( { message: 'Internal Server Error' } );
});
} );
The test of this endpoint fails with a 401 response and a simple Error: Unauthorized That is not a lot of information
and I don't know where to continue trouble shooting. My understanding is that this means the initial passport.authenticate
at the endpoint is not recognizing the token as valid, even though it has been confirmed as valid on the jwt.io site. Is there
additional information I should be including when generating the token? If so how?
I have looked at several other examples using passport; one,
two,
three but I did
not see how to apply any of these insights to my implementation.
Any help with this would be greatly appreciated. Thank you for your time.
UPDATE:
Based on the fact that a valid token is going up to the endpoint but it is still not being accepted I tried building a promise chain with an initial call to the user authentication endpoint and then used the token derived from there to test the session generation endpoint. This was the effort;
it( 'should return a session with proper request', function(){
let token;
return chai.request(app)
.post('/api/user/authenticate')
.send( {
email: 'random#random.com',
password: 'password2'
} )
.then( (res) => {
res.should.have.status(200);
res.should.be.json;
console.log( res.body );
token = res.body.token;
return token;
})
.catch( (err) => {
console.log( 'failed at user authentication' );
})
.then( ( err, data ) => {
console.log( 'token value is ', token );
chai.request( app )
.post( '/api/session' )
.set( 'Authorization', `${ token }` )
.send( {
operation: "+",
number: "10",
min: "1",
max: "200"
})
.then( res => {
console.log( 'second response is ', res );
res.should.have.status(201);
} )
.catch( err => {
console.log( 'second promise ', err.message );
})
})
});
It is still rejected as Unauthorized. Something is missing from the request being sent to the session endpoint from the test. The console.log( req ) in the session endpoint confirms that it is receiving an object that has a great deal of information added to it, compared to what is being sent from the promise chain in the test. I had to use the token variable in the outer scope to pass the value between the chai.request(app) commands. This is not the same data flow present in the working app. I am not sure how to test this; my understanding is that what I have tried so far does not give the endpoint enough data to validate the request.
Any help would be greatly appreciated. Thank you for your time.

SOLUTION
To follow up, a friend helped with this and gave me a solution that passed additional information back to the endpoint when
building the token. The final working model had a beforeEach() function that adds a dummy user to the test database, and then
assigns this user to a testUser variable in the outer scope, so it is accessible in the later test. In the actual test of the
endpoint this testUser variable is mapped to a full user schema and is used to build the token and test the endpoint. It works well since the endpoint has all the properties needed. Here is the code;
describe( 'End-point for practice session resources', function() {
let testUser;
let modelSession;
before( function() {
return runServer( TEST_DATABASE_URL );
});
beforeEach( function(done){
addUser()
.then( user => {
testUser = user;
done();
});
});
and the test;
it( 'should generate a session with the proper request (POST)', function(){
const token = jwt.sign({
id: testUser._id
}, secret, { expiresIn: 60 * 60 });
return chai.request( app )
.post( '/api/session' )
.set( 'Authorization', `Bearer ${ token }` )
.send( {
operation: "+",
number: "10",
min: "1",
max: "200"
})
.then( res => {
res.should.have.status(201);
} )
})
});
Hope this helps someone else down the road.

Related

Not Able to Login Without giving auth token in the header

This is a todo list web app, I have used nodejs and reactjs in it
I am not able to use the login feature , It shows me the error : invalid token
I have tried hard coding the token (which generates on the sign up) and that way it worked. But with the below code it doesnt work.
Using JWT for Authentication token generation
Funtion that handles the Login Click (user puts email and password)
const handleSubmit = async (e) => {
e.preventDefault();
const response = await fetch('http://localhost:5000/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email: credentials.email, password: credentials.password })
});
const json = await response.json();
if (json.success) {
localStorage.setItem('token', JSON.stringify(json.authToken));
showAlert('Successfully Logged in');
navigate("/");
} else {
alert("Invalid credentials");
}
}
Backend Api Call (Using Nodejs and Express)
router.post("/login", fetchUser,
[
body("email", "Enter a valid email").isEmail(),
body("password", "Password cannot be blank").exists(),
], async (req, res) => {
let success = false;
// if there are errors, handle them with bad requests
const errors = validationResult(req);
if (errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
try {
const { email, password } = req.body;
// Check if the user with requested email exists in the database
let user = await User.findOne({ email });
if (!user) {
success = false;
return res.status(400).json({ success, error: "Please enter the correct credentials" });
}
// Check if the user with requested passwork exists in the database
const comparePassword = await bcrypt.compare(password, user.password);
if (!comparePassword) {
success = false;
return res.status(400).json({ success, error: "Please enter the correct credentials" });
}
// Auth Token Generation using jwtToken
const data = {
user: {
id: user.id,
},
};
success = true;
let authToken = jwt.sign(data, JWT_Secret);
res.json({ success, authToken });
} catch (error) {
res.status(500).send("Internal error occured");
}
});
When I tried Hardcording the auth-token it worked
By clicking on login the new auth-token should be generated and set as 'token' in the local storage. Through which data will be accessed using different end points.
At this line json.authToken is a string already. You don't need to stringify it again.
localStorage.setItem('token', JSON.stringify(json.authToken))
Just remove the function and it'll be fine.
localStorage.setItem('token', json.authToken)

How to render errors from node.js in react using fetch

My application has a login page that works fine when the user enters the correct login credentials but while test casing for eventual wrong entries of either usernames or passwords I've realized that my catch block isn't able to correctly format the error object from the backend thus nothing is being rendered to the frontend.
I've tried using res.send(401).json({"message":"Unauthorized"}); in the backend instead of res.sendStatus(401); but the former method doesn't trigger an error response and rather returns as a response in the fetch.
While using res.sendStatus(401);, although the error is triggered my catch block isn't able to render it's response.
The backend:
const User = require('../model/User');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const handleLogin = async (req,res) => {
const user = req.body.user.toLowerCase();
const pwd = req.body.pwd;
if(!user || !pwd) return res.sendStatus(400);
const foundUser = await User.findOne({username: user}).exec();
if(!foundUser) return res.sendStatus(401);
const match = await bcrypt.compare(pwd, foundUser.password);
console.log(match);
if(match){
const roles = Object.values(foundUser.roles);
const accessToken = jwt.sign(
{"userInfo": {
"username": foundUser.username,
"roles": roles
}},
process.env.ACCESS_TOKEN,
{expiresIn: "300s"}
);
const refreshToken = jwt.sign(
{"username": foundUser.username},
process.env.REFRESH_TOKEN,
{expiresIn: "1d"}
);
foundUser.refreshToken = refreshToken;
const result = await foundUser.save();
if(!result) return res.status(500);
res.cookie("jwt",refreshToken,{httpOnly: true, sameSite: "None", maxAge: 24*60*60*1000});
res.json({user, roles, accessToken});
}
else{
res.sendStatus(401);
}
}
module.exports = {handleLogin};
The fetch:
fetch(BASE_URL + "/login", {
method: "POST",
headers: {
"Content-Type":"application/json"
},
body: JSON.stringify({user: username,pwd})
})
.then(res => res.json())
.then(data => {
setUser(data);
console.log(data);
})
.then(() => {
setSuccess(true);
setTimeout(() => {
navigate("/");
}, 1000);
})
.catch((err)=>{
console.log(err);
if(err.status == "401"){
setErrMsg("Wrong username or password.")
}
else{
setErrMsg("Login failed, try again.")
}
errRef.current.focus();
})
Once the error is triggered the console displays the following error SyntaxError: Unexpected token 'U', "Unauthorized" is not valid JSON and in addition to that the error is not rendered to the frontend.
How can I correctly format the response from the backend or handle the error response from the front end to be able to correctly render it to the view?
Your first then assumes that the response has valid json. Instead it should check if the response status is ok and if not, throw an error that will be caught by the catch.
.then(res => {
if (res.ok) {
return res.json();
}
throw new Error(res.status);
})

(ReactJS) Getting error in Login Component, while Register Compo. works perfectly

I can't seem to get why my Login component is getting an error when my Registration component works well while using the same POST request to the backend server. The only thing that they differ is the method of retrieving data from MongoDB in their backend script partner, which is what I am thinking is the problem, but anything I do doesn't seem to work.
Edit > * The error in the Login Component is AxiosError: Network Error. Both the Login and Register backend have been tested in Postman and works well, and responds a status. So it seems that the problem is in the Login React Component's Axios post request. It send data to the backend okay, but it catches an error after that.*
The login script of the backend server is working well and validating the credentials perfectly. But then, React gets an error.
in Login React Component (AxiosError):
async postReq() {
const loginData = JSON.stringify(
{
'email': this.state.email,
'password': this.state.password,
},
);
console.log(loginData)
let validation = await axios.post(
'http://localhost:5000/login',
loginData,
{ headers: {'Content-Type':'application/json'}
})
.then((res) => {
console.log(`Login successful. ${res}`);
let response = res;
this.props.redirect('/session');
})
.catch((error) => {
console.log(error);
console.log(`Cannot login. ${error.message}`)
console.log(error.request);
let response = error;
alert("Damn.")
});
}
in Register React Component (works smoothly):
handleSubmit() {
// POST to server
const regData = JSON.stringify(
{
'firstname': this.state.fname,
'lastname': this.state.lname,
'email': this.state.email,
'birthday': this.state.birthday,
'password': this.state.password,
'country': this.state.country,
'city': this.state.city,
'provstate': this.state.provstate,
'contactnum': this.state.contactnum,
'formpicture': this.state.img,
'disclcond': this.state.cond,
},
);
console.log(regData)
axios.post(
'http://localhost:5000/register',
regData,
{ headers: {'Content-Type':'application/json'}
})
.then((res) => {
console.log(`Registered successfully. ${res}`);
setTimeout(() => this.props.redirect('/login'), 2000)
})
.catch((res) => {
console.log(`Not registered. ${res}`)
alert("Damn.")
});
}
NodeJS, Mongoose || Login backend:
const router = require('express').Router();
let User = require('../db_models/user.model');
router.route('/').get((req, res) => {
res.sendStatus(200);
res.end();
})
// If user submits login credentials, check database
router.route('/').post((req, res) => {
const email = req.body.email;
console.log(email)
const password = req.body.password;
let accountMatched = null;
async function checkPassword() {
await User.findOne({ 'email' : email })
.then(user => {
if (user.password === password) {
console.log(`true ${user.email} :: ${user.password}`);
accountMatched = true;
res.sendStatus(200);
} else {
console.log(`damn!! ${err}`)
res.sendStatus(404);
throw err
}
})
.catch(err => console.log(err))
accountMatched === true ? console.log('Passed') : res.send('Failed');
res.end()
}
checkPassword();
})
module.exports = router;
Register backend:
const router = require('express').Router();
let User = require('../db_models/user.model');
router.route('/').get((req, res) => {
res.send('hello hello');
res.end();
})
// If user submits registration credentials, submit to database
router.route('/').post((req, res) => {
console.log(req.body)
const firstname = req.body.firstname;
const lastname = req.body.lastname;
const email = req.body.email;
const birthday = Date.parse(req.body.birthday);
const password = req.body.password;
const contactnum = req.body.contactnum;
const country = req.body.country;
const city = req.body.city;
const provstate = req.body.provstate;
// below only pass links
const formpicture = req.body.formpicture;
const disclcond = req.body.disclcond;
const newUser = new User({
firstname,
lastname,
email,
birthday,
password,
country,
city,
provstate,
contactnum,
formpicture,
disclcond,
});
newUser.save()
.then(() => {
console.log('User added.');
res.sendStatus(200);
res.end();
})
.catch(err => {
console.log(`Damn, user not added. ${err}`);
res.end();
})
});
module.exports = router;
I would really appreciate some help.
Try exchange the Login Component part to something like this if you wanna use async/await.
async postReq() {
const loginData = JSON.stringify(
{
'email': this.state.email,
'password': this.state.password,
},
);
console.log(loginData)
try {
let res = await axios.post(
'http://localhost:5000/login',
loginData,
{ headers: {'Content-Type':'application/json'}
})
console.log(`Login successful. ${res}`);
let response = res;
this.props.redirect('/session');
} catch (error) {
console.log(error);
console.log(`Cannot login. ${error.message}`)
console.log(error.request);
let response = error;
alert("Damn.")
}
}

How to verify cookies from Nextjs UI to an external server Nodejs Server

I have been struggling for days with no success with my web application. The kind of similar questions on stake overflow are not addressing my issue. I have a standalone Nodejs server which is basically an API provider and a standalone Nextjs web app which serves as the UI. The idea is to have them different Servers in production. On localhost the Next app is running on port 3000 and the Node App is running on port 5000. I am trying to do authentication but how can I verify cookies from the browser (Nextjs App) on the Server. When a user logs in the server sends the user data and a token in a cookie. The cookie is then saved on the browser, from here how can I send back that cookie to the server for verification from Nextjs.
Here is my backend code (Nodejs App running at port 5000)
const signToken = (id) =>
jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN,
});
const createSendToken = (user, statusCode, res) => {
const token = signToken(user._id);
const cookieOptions = {
expires: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
httpOnly: true,
};
if (process.env.NODE_ENV === 'production') cookieOptions.secure = true;
res.cookie('jwt', token, cookieOptions);
// Remove password from output
user.password = undefined;
user.passwordChangedAt = undefined;
res.status(statusCode).json({
status: 'success',
token,
data: {
user,
},
});
};
exports.Login = catchAsync(async (req, res, next) => {
const { email, password } = req.body;
//1) Check if email and password exists
if (!email || !password) {
return next(new AppError('Please provide email and password!', 400));
}
//2) Check if user exists && password is correct
const user = await User.findOne({ email }).select('+password');
if (!user || !(await user.correctPassword(password, user.password))) {
return next(new AppError('Incorrect email or password', 401));
}
//3) If everything is ok , send token to client
createSendToken(user, 200, res);
});
Frontend Code (Nextjs App running on port 3000)
const postData = async (url, post, token) => {
try {
const res = await fetch(`${serverUrl}${url}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: token,
},
body: JSON.stringify(post),
});
const data = await res.json();
return data;
} catch (error) {
return error;
}
};
const handleSubmit = async(e) => {
e.preventDefault();
const res = await postData('users/login', userData);
console.log(res);
if(res.status === 'fail') return errorAlert(res.message)
successAlert('Login Successful')
dispatch({ type: 'AUTH', payload: {
token: res.token,
user: res.user,
cookie: cookieData
}})
Cookie.set('token', res.token)
localStorage.setItem('firstLogin', true)
};
The cookie which you set after authentication in the browser is included by the browser for subsequent requests to Nextjs server automatically.
You can verify this by checking the req/res in Network Tab.
In Nextjs, we can have a function for validation:
import { NextRequest } from 'next/server'
const TOKEN = '<token-name>';
const isRequestValid = async (req: NextRequest) => {
const cookie = req.cookies[TOKEN]
if (!cookie) {
return false;
}
// validate cookie here by sending request to Node server (running on 5000)
// and return true or false
return ...
}
Then create a _middleware.tsx under pages. Here we will define our _middleware function.
This would run on every req (since it's defined at the root level. More on here)
import { NextRequest, NextResponse } from 'next/server'
export function middleware(req: NextRequest) {
const { pathname } = req.nextUrl;
// check for authenticated routes
if (pathname === '<path-for-which-you-want-to-authenticate>') {
if (!isRequestValid(req)) {
return NextResponse.redirect('<url-for-redirection-if-not-authenticated>')
}
}
// if user is authenticated this will be called which continues the
// normal flow of application
return NextResponse.next()
}

JWT expired, unexpected response in nodejs API

i have a problem when i try to use a private api in my node.js server, This is the process that i am following
SignUp (if the user doesn't have an account) or logIn (already have an account).
It generates the token and i pass it in the header res.header('access_token': token)
I copy this token and paste it in my private api in the header section (i'm using postman to test it for now) in order to verify if the user is logged in.
In my route, i use a middleware to turn it private, validating if the user can use the resource jwt.verify(authorization[1], process.env.SEED_AUTH) (SEED_AUTH is my token secret stored in my server)
Here is when i have the error, the middleware is failling to verify the user and throw me this error jwt expired
This is my route
const UsersController = require('../controllers/users.controller')
const AuthorizationMiddleware = require('../middlewares/authorization.middleware')
exports.routesConfig = (app) => {
app.post('/api/users',[
AuthorizationMiddleware.verifyValidJWT
], UsersController.insert)
}
This is my middleware
const jwt = require('jsonwebtoken')
require('../config/env.config')
exports.verifyValidJWT = (req, res, next) => {
if (req.headers['access-token']) {
try {
let authorization = req.headers['access-token'].split(' ');
if (authorization[0] !== 'Bearer') {
return res.status(401).json({
ok: false,
err: "Unauthorized, Need a valid token"
});
} else {
console.log(authorization[1]);
req.jwt = jwt.verify(authorization[1], process.env.SEED_AUTH);
return next();
}
} catch (error) {
return res.status(403).json({
ok: false,
err: "Forbidden, need a valid token -> " + error.message
});
}
} else {
return res.status(401).json({
ok: false,
err: "Need to recieve a valid token"
});
}
}
And finally the API UsersController.insert
What i'm trying to do with this api is to create a new user.
For a better understanding this is my LOGIN API
const User = require('../models/users.model')
const { signUpValidation, logInValidation } = require('../middlewares/auth.validation.data')
const bcrypt = require('bcrypt')
const jwt = require('jsonwebtoken')
exports.logIn = async (req, res) => {
let body = req.body;
//Validation
const { error } = logInValidation(body)
if (error) {
return res.status(400).json({
ok: false,
err: error.details[0].message
})
}
//Check if the email already exists in the database
const user = await User.findOne({ email: body.email })
if (!user) {
return res.status(400).json({
ok: false,
err: "Invalid Email or password!"
})
}
const validPass = await bcrypt.compareSync(body.password, user.password)
if (!validPass) {
return res.status(400).json({
ok: false,
err: "Invalid Email or password!"
})
}
const token = jwt.sign(
{
_id: user._id,
email: user.email
},
process.env.SEED_AUTH,
{
expiresIn: process.env.TOKEN_EXPIRY
}
)
res.header('access-token', token).json({
ok: true,
user: {
id: user._id,
email: user.email
}
});
}
SignUp and LogIn validation
I use these middleware to verify if it is a valid email, and the name with a minimum number of letters...
My process.env.TOKEN_EXPIRY is set to 300 (i understand that, it is in seconds), i've tried with bigger number though
(The API works without the middleware).
What would be the problem that i am not seeing. Thanks for your help.
process.env variables are as string and not as a number. According to the jsonwebtoken documentation, string is considered as milliseconds by default and number is counted as seconds by default. So change TOKEN_EXPIRY to 300000 from 300

Resources