[ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client in MERN Stack Application - node.js

How to make user redirect after authentication based on user.role ?
I'm getting the following error: UnhandledPromiseRejectionWarning: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
const jwt = require('jsonwebtoken')
const { COOKIE_NAME, SECRET } = require('../config/config')
module.exports = function() {
return (req, res, next) => {
let token = req.cookies[COOKIE_NAME]
if(token) {
jwt.verify(token, SECRET, function(err, decoded){
if (err) {
res.clearCookie(COOKIE_NAME)
} else {
if(decoded.user.role === 'admin') {
res.redirect('http://localhost:4000')
}
req.user = decoded;
}
})
}
next();
}
}
Login Fetch:
fetch(`${API}/auth/login`,{
method: 'POST',
credentials: 'include',
withCredentials: true,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(user)
})
.then((response) => {
if(response.status === 302) {
window.location = 'http://localhost:4000'
}
else if(response.status === 200) {
onSuccess()
setTimeout(() => {
window.location = '/'
}, 1000)
} else if (response.status === 401) {
onError()
}
})
.catch((error) => {
console.log(error)
})
}
Here is my authService:
const jwt = require('jsonwebtoken')
const User = require('../models/User');
const bcrypt = require('bcrypt')
const { SALT_ROUNDS, SECRET } = require('../config/config');
const register = async ({name, username, email, password, cart}) => {
let salt = await bcrypt.genSalt(SALT_ROUNDS);
let hash = await bcrypt.hash(password, salt);
const user = new User({
name,
username,
email,
password: hash,
cart
});
return await user.save()
}
const login = async ({email, password}) => {
let user = await User.findOne({email})
if (!user) {
throw {message: 'User not found!'}
}
let isMatch = await bcrypt.compare(password, user.password)
if (!isMatch) {
throw {message: 'Password does not match!'}
}
let token = jwt.sign({user}, SECRET)
return token;
}
And my authController:
const { Router } = require('express');
const authService = require('../services/authService');
const { COOKIE_NAME } = require('../config/config');
const router = Router();
router.post('/login', async (req, res) => {
const {email, password} = req.body
try {
let token = await authService.login({email, password})
res.cookie(COOKIE_NAME, token)
res.status(200).json(token)
} catch (error) {
res.status(401).json({ error: error })
}
})
Here is my server if this will help:
app.use((req, res, next) => {
const allowedOrigins = ['http://localhost:3000', 'http://localhost:4000'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', true)
}
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
next();
});

Since you're using jwt.verify with a callback, it is being executed asynchronously. Due to this, immediately after calling verify but before getting the decoded token, your next() function is called which passes the control to the next middleware (which probably would be synchronous) which then returns the request.
The flow of events would be something like this:
if(token) { ... starts
jwt.verify(token, ... is called asynchronously. It registers the callback function(err, decoded) { ... but doesn't execute it yet.
You exit the if(token) { ... } block and call next().
The next middleware in line starts executing and probably returns the request if it is the last middleware in chain. So the client has already been sent the response by this time.
jwt.verify(token ... succeeds and calls your registered callback.
It sees that there is no error at line if (err) ... so it moves to the else block.
It decodes the user role and tries to redirect (which internally would try to insert a header on the response). But this fails because the user was already sent the response (and hence your error message).
So the simple solution to this is to not call next() UNTIL jwt verifies and decodes your token and you know the role. In the code below, I've moved the next() function call a few lines upwards.
const jwt = require('jsonwebtoken')
const { COOKIE_NAME, SECRET } = require('../config/config')
module.exports = function() {
return (req, res, next) => {
let token = req.cookies[COOKIE_NAME]
if(token) {
jwt.verify(token, SECRET, function(err, decoded){
if (err) {
res.clearCookie(COOKIE_NAME)
} else {
if(decoded.user.role === 'admin') {
res.redirect('http://localhost:4000')
}
req.user = decoded;
}
next();
})
}
}
}

Related

What would be the error with this token verification code?

When making the middleware request in my route, I always fall into the else of "verifyAdmin" (error 403). The big problem is that I can't send a throw or catch of this error, it just doesn't return any error in the terminal, but when testing in postman it always goes to else
const jwt = require('jsonwebtoken');
const verifyToken = (req, res, next) => {
const { authorization } = req.headers;
if (!authorization) {
return res.status(401).json('Invalid Authorization')
};
const token = authorization.replace('Bearer', ' ').trim();
try {
const secret = process.env.JWT_SECRET;
const data = jwt.verify(token, secret);
req.users = data;
const { id } = data;
req.userId = id;
return next();
} catch (err) {
return res.status(400).json(err);
}
};
const verifyAdmin = (req, res, next) => {
if (req.users.isAdmin === true) {
next();
} else {
return res.status(403).json("You are not alowed to do that!");
}
};
module.exports = {
verifyToken,
verifyAdmin,
};
in route
const { verifyToken, verifyAdmin } = require('../middlewares/verifyToken');
router.get('/', verifyToken, verifyAdmin, FindAllUsersController.index);
construction token
const db = require('../../models/index');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
exports.store = async (req, res) => {
const { email, password } = req.body;
const secret = process.env.JWT_SECRET;
try {
const user = await db.User.findOne({ where: { email } });
if (!user) {
return res.status(401).json('User does not exist');
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json('Password is not valid');
}
const token = jwt.sign({ id: user.id }, secret, {
expiresIn: process.env.EXPIRES_TOKEN,
})
return res.status(200).json({
user,
token,
});
} catch (err) {
console.log(err);
}
}
The isAdmin flag is not contained in your token, because you include only the id when constructing it:
const token = jwt.sign({ id: user.id }, ...)
You need (at least):
const token = jwt.sign({ id: user.id, isAdmin: user.isAdmin }, ...)

Cannot post Token from Client

Frontend Code
<script>
const formDOM = document.querySelector(".form");
const btnSubmitDOM = document.querySelector(".btn-submit");
// get token form server
formDOM.addEventListener("submit", async (e) => {
e.preventDefault();
const email = document.querySelector("#email").value;
const password = document.querySelector("#password").value;
try {
const { data } = await axios.post(
"http://localhost:3000/api/v1/auth/login",
{ email, password }
);
console.log("token", data.token);
localStorage.setItem("token", data.token);
} catch (error) {
console.log(error);
}
});
// after submit, if authenticated, goes to protective route
btnSubmitDOM.addEventListener("click", async () => {
const token = localStorage.getItem("token");
console.log("toekn when press", token);
try {
const { data } = await axios.get("http://localhost:3000/api/v1/inventories", {
headers: {
Authorization: `Bearer ${token}`,
},
});
location.assign("/inventories");
} catch (error) {
localStorage.removeItem("token");
console.log(error);
}
});
</script>
Here is the backend code
const jwt = require("jsonwebtoken");
const auth = async (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer")) {
res.send("Auth route - Unauthenticated Error");
}
const token = authHeader.split(" ")[1];
console.log("token is ", token);
try {
const payload = jwt.verify(token, 'secret');
// console.log(payload);
// attached to the inventories route
req.user = {
userId: payload.userId,
email: payload.email,
};
// console.log(req.user)
next();
} catch (error) {
console.log(error);
}
};
module.exports = auth;
Here is the error from server side
token is eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI2MzY4ZjFiM2U1ODJlYmQ1MjJkODI4MGEiLCJlbWFpbCI6Imt5YXdAZ21haWwuY29tIiwiaWF0IjoxNjY3ODI3NzIzLCJleHAiOjE2NzA0MTk3MjN9.rptgRrYB4TrLQCxbB18JqMHd05LSox-LQuiLJS0L2Gw
/Users/kyawmyo/Desktop/software-development/nodejs-&-vanilajs-projects/inventories/middleware/authentication.js:8
const token = authHeader.split(" ")[1];
^
TypeError: Cannot read properties of undefined (reading 'split')
I try to test with postman, all working fine.
But I send request from client side, it is error, I cannot figure it out. Please help.

Handling Login Authorization with Node.js

Here I'm carrying out the GET method to list the data by authenticating the login credentials, but when I pass the token in value in the header it directly catch the error message. is anything I'm doing wrong?
Authentication Middleware - authentication.js
const jwt = require("jsonwebtoken");
const authenticate = (req, res, next) => {
const access_token = req.headers["authorization"];
if (!access_token) return res.status(401).send("Access denied! no token provided.");
try {
const decoded = jwt.verify(access_token, "SECRET_JWT_CODE");
req.receive = decoded;
next();
} catch (error) {
res.status(400).send("invalid token.");
}
};
module.exports = authenticate;
console.log(req.headers)
GET method
const authenticate = require("./authentication.js");
router.get("/admin", authenticate, async (req, res) => {
try {
const receive = await SomeModel.find();
res.json(receive);
} catch (err) {
res.send(err);
}
});
login
router.post("/admin/sign_in", (req, res) => {
if (!req.body.email || !req.body.password) {
res.json({ error: "email and password is required" });
return;
}
login
.findOne({ email: req.body.email })
.then((admin) => {
if (!admin) {
res.json({ err: "user does not exist" });
} else {
if (!bcrypt.compareSync(req.body.password, admin.password)){
res.json({ err: "password does not match" });
} else {
const token = jwt.sign(
{
id: admin._id,
email: admin.email,
},
SECRET_JWT_CODE
);
res.json({
responseMessage: "Everything worked as expected",
access_token: token,
});
}
}
})
.catch((err) => {
err.message;
});
});
The token is in the form Bearer <token> so you need to split it before verifying it:
const jwt = require("jsonwebtoken");
const authenticate = (req, res, next) => {
const access_token = req.headers["Authorization"];
if (!access_token) return res.status(401).send("Access denied! no token provided.");
const splitToken = access_token.split(' ');
if (splitToken.length !== 2) return res.status(401).send("Access denied! invalid token.");
const token = splitToken[1];
try {
const decoded = jwt.verify(token, "SECRET_JWT_CODE");
req.receive = decoded;
next();
} catch (error) {
res.status(400).send("invalid token.");
}
};
module.exports = authenticate;
Also, make sure that you pass the token via the Authorization tab in Postman.
It should be available in req.headers["Authorization"] (capitalized) in the expected Bearer <token> format.

Trying To Incorporate Tokens Into Sign Up

I am trying to create a token from JWT when a user signs up for my application but I get the error, Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client. I think there is an issue with the structure of my code. How can I fix this issue?
//sign up request
exports.signup = async (req, res, next)=> {
const {email} = req.body;
const userExist = await User.findOne({email});
if (userExist) {
return next(new ErrorResponse(`Email already exists`, 404))
}
try {
const user = await User.create(req.body);
res.status(201).json({
success: true,
user
})
generateToken(user, 201, res);
} catch (error) {
console.log(error);
next(error);
}
}
//generate token method
const generateToken = async (user, statusCode, res) => {
const token = await user.jwtGenerateToken();
var hour = 3600000;
const options = {
httpOnly: true,
expires: new Date(Date.now() + hour)
};
res.status(statusCode)
.cookie('token', token, options)
.json({success: true, token})
}
First generate the token and next send the response
generateToken(user, 201, res);
res.status(201).json({
success: true,
user
})
You can return value from the token and then send it using response or you can just send it from the generateToken function. Here you are trying to use the response again though you have sent it once hence resulting in error.
Updated
//sign up request
exports.signup = async (req, res, next)=> {
const {email} = req.body;
const userExist = await User.findOne({email});
if (userExist) {
return next(new ErrorResponse(`Email already exists`, 404))
}
try {
const user = await User.create(req.body);
generateToken(user, 201, res);
} catch (error) {
console.log(error);
next(error);
}
}
//generate token method
const generateToken = async (user, statusCode, res) => {
const token = await user.jwtGenerateToken();
var hour = 3600000;
const options = {
httpOnly: true,
expires: new Date(Date.now() + hour)
};
res.status(statusCode)
.cookie('token', token, options)
.json({success: true, token: token, user: user})
}
you are trying to send the response twice. and then in generateToken method. Keep it at one place, the error will go away.

How can I check currently logged user in user authentication on Mean Stack (Node.js + Mongodb)?

I have a login system with tokens. When logging in, it checks whether such a user exists, doesn't have information about current user during the login session. What's the easiest way to check it and send response to frontend?
Routes:
function verifyToken(req, res, next) {
if (!req.headers.authorization) {
return res.status(401).send('Unauthorized request');
}
let token = req.headers.authorization.split(' ')[1];
if (token === 'null') {
return res.status(401).send('Unauthorized request');
}
let payload = jwt.verify(token, 'secretKey');
if (!payload) {
return res.status(401).send('Unauthorized request');
}
req.userId = payload.subject;
next();
}
router.post('/register', (req, res) => {
let userData = req.body;
let user = new User(userData);
user.save((error, registeredUser) => {
if (error) {
console.log(error);
} else {
let payload = { subject: registeredUser._id };
let token = jwt.sign(payload, 'secretKey');
res.status(200).send({ token });
}
})
})
router.post('/login', (req, res) => {
let userData = req.body;
User.findOne({ email: userData.email }, (error, user) => {
if (error) {
console.log(error);
} else {
if (!user) {
res.status(401).send('Invalid email');
} else
if (user.password !== userData.password) {
res.status(401).send('Invalid password')
} else {
let payload = { subject: user._id };
let token = jwt.sign(payload, 'secretKey');
res.status(200).send({ token });
}
}
})
})
You could try using a middleware to retrieve the token from the Authorization header and retrieve the userId from there, the middleware could look something like this:
const decodeToken = (token) => {
return jwt.verify(token, 'secretKey', (err, decoded) => {
if (err) {
return undefined;
}
return decoded;
});
};
const authorize = (req, res, next) => {
if (!req.headers.authorization) {
return res.status(401).send({message: 'UNAUTHORIZED'});
}
const token = req.headers.authorization.split(' ')[1];
if (!token) {
return res.status(401).send({message: 'UNAUTHORIZED'});
}
const decodedToken = decodeToken(token);
if (!decodedToken) {
return res.status(401).send({message: 'UNAUTHORIZED'});
}
req.userId = decodedToken.subject;
next();
};
module.exports = authorize;
Hope this helps you, if not, I hope you find your answer :)
EDIT
To use the middleware you only need to add it to your route, I´ll leave you an example with a get request:
const authorize = require('../middleware/authorize');
router.get('/someroute', authorize, (req, res) => {
// authorize will verify the token and attach the userId to your request
// so to use it you'll only need to call req.userId
// something like this
console.log('current logged user id', req.userId);
// YOUR CODE HERE
});

Resources