I'm having a hard time connecting the last dots building a role based access control api in Express.
Following this tutorial and implementing onto my existing program, but I think I am missing the last step and after countless tutorials analysis paralysis has set in. I have since scaled back all my necessary code to what I think is the bare minimum.
Currently I am able to create a new user and save them to the mongoose database. I can see the hash by bcrypt is doing its thing and I can see the token being generated in the response after signing up. However as soon as I navigate to a new page after signup or login, for eg the users own id page/user/:userId as per tutorial, I keep getting You need to be logged in. I know I need to check for a token on every request but my question is, why doesn't it seem like the middleware is checking for the token or something is holding it back?
Since the token is shown in the json reponse surely I should be able to check for the tokens existence with the next get request at for eg the /user/:userId page? Isn't that the idea? Or is the browser just showing the response but I still need to actually store it? I don't understand where it goes to so to speak..
Any advice? Or is this a session thing? I know its a bit hard without all the code but if anyone could spot anything relevant so that I could research my next steps I would much appreciate it!
First this middleware in app.js
app.use(express.json());
app.use(express.urlencoded({extended: true}));
app.use('/', async (req, res, next) => {
if (req.headers['x-access-token']) {
try {
const accessToken = req.headers['x-access-token'];
const {userId, exp} = await jwt.verify(accessToken, process.env.JWT_SECRET);
console.log('token verified'); // not printing to console
// If token has expired
if (exp < Date.now().valueOf() / 1000) {
return res.status(401).json({
error: 'JWT token has expired, please login to obtain a new one',
});
}
res.locals.loggedInUser = await User.findById(userId);
next();
} catch (error) {
next(error);
}
} else {
next();
}
});
app.use('/', userRoutes);
I have built the roles using the module access-control which is required
const AccessControl = require('accesscontrol');
const ac = new AccessControl();
exports.roles = (function() {
ac.grant('basic')
.readOwn('profile')
.updateOwn('profile');
ac.grant('supervisor')
.extend('basic')
.readAny('profile');
ac.grant('admin')
.extend('basic')
.extend('supervisor')
.updateAny('profile')
.deleteAny('profile');
return ac;
})();
routes examples as per tutorial.
router.get('/signup', (req, res, next) => {
res.render('signup', {
viewTitle: 'User SignUp',
});
});
router.post('/signup', userController.signup);
router.get('/login', (req, res, next) => {
res.render('login', {
viewTitle: 'User Login - WTCT OPS',
});
});
router.post('/login', userController.login );
router.get('/add', userController.allowIfLoggedin, userController.grantAccess('readAny', 'profile'), userController.add);
router.get('/users', userController.allowIfLoggedin, userController.grantAccess('readAny', 'profile'), userController.getUsers);
router.get('/user/:userId', userController.allowIfLoggedin, userController.getUser);
router.put('/user/:userId', userController.allowIfLoggedin, userController.grantAccess('updateAny', 'profile'), userController.updateUser);
router.delete('/user/:userId', userController.allowIfLoggedin, userController.grantAccess('deleteAny', 'profile'), userController.deleteUser);
relevant part of controller
async function hashPassword(password) {
return await bcrypt.hash(password, 10);
}
async function validatePassword(plainPassword, hashedPassword) {
return await bcrypt.compare(plainPassword, hashedPassword);
}
// grant access depending on useraccess role
exports.grantAccess = function(action, resource) {
return async (req, res, next) => {
try {
const permission = roles.can(req.user.role)[action](resource);
if (!permission.granted) {
return res.status(401).json({
error: 'You don\'t have enough permission to perform this action',
});
}
next();
} catch (error) {
next(error);
}
};
};
// allow actions if logged in
exports.allowIfLoggedin = async (req, res, next) => {
try {
const user = res.locals.loggedInUser;
if (!user) {
return res.status(401).json({
error: 'You need to be logged in to access this route',
});
}
req.user = user;
next();
} catch (error) {
next(error);
}
};
// sign up
exports.signup = async (req, res, next) => {
try {
const {role, email, password} = req.body;
const hashedPassword = await hashPassword(password);
const newUser = new User({email, password: hashedPassword, role: role || 'basic'});
const accessToken = jwt.sign({userId: newUser._id}, process.env.JWT_SECRET, {
expiresIn: '1d',
});
newUser.accessToken = accessToken;
await newUser.save();
res.send({
data: newUser,
message: 'You have signed up successfully',
});
} catch (error) {
next(error);
}
};
exports.login = async (req, res, next) => {
try {
const {email, password} = req.body;
const user = await User.findOne({email});
if (!user) return next(new Error('Email does not exist'));
const validPassword = await validatePassword(password, user.password);
if (!validPassword) return next(new Error('Password is not correct'));
const accessToken = jwt.sign({userId: user._id}, process.env.JWT_SECRET, {
expiresIn: '1d',
});
await User.findByIdAndUpdate(user._id, {accessToken});
res.status(200).json({
data: {email: user.email, role: user.role},
accessToken,
});
} catch (error) {
next(error);
}
};
// get one user
exports.getUser = async (req, res, next) => {
try {
const userId = req.params.userId;
const user = await User.findById(userId);
if (!user) return next(new Error('User does not exist'));
// console.log(req.params);
res.send(200).json({
data: user,
});
} catch (error) {
next(error);
}
};
Why when trying to post to the endpoint /user/:userId is the middleware not checking for the token?
Thank you for any advice!
Update:
So far I have tried to removed the / from app.use. I saw I made that mistake now, but also tried removing it from the app.use(userRoutes); middleware to make it apply to all http requests but no luck.
app.use(async (req, res, next) => {
if (req.headers['x-access-token']) {
try {
const accessToken = req.headers['x-access-token'];
const {userId, exp} = await jwt.verify(accessToken, process.env.JWT_SECRET);
// If token has expired
if (exp < Date.now().valueOf() / 1000) {
return res.status(401).json({
error: 'JWT token has expired, please login to obtain a new one',
});
}
res.locals.loggedInUser = await User.findById(userId);
// console.log('Time:', Date.now());
next();
} catch (error) {
next(error);
}
} else {
next();
}
});
app.use(userRoutes);
I also thought that maybe because my server makes http requests in the backend maybe that was causing a problem in setting the x-access-token header? So I tried to change the x-access-token mw to use router.use on all routes but still nothing. I don't understand what I am missing. And just to be sure I'm not missing something fundamental, since I am using the JWT I do not need to use local storage or cookies to allow for browsing between pages while logged in since I can use the token set in the header, correct?
Thanks again for any advice!
That's because your middleware is only tied to the / route. Remove it if you want it to be used for every route. Take a look at the ExpressJS Docs regarding middleware.
Related
In the users table, I have two collections, one of which is admin and the other which is not.
Now I only want admin user to post data.
Here is the post request:
router.post("/bus/add", auth, async (req, res) => {
const bus = new Bus(req.body);
const user = await User.find({ admin: true });
try {
if (user) {
await bus.save();
res.status(201).send(bus);
} else {
return res.status(401).send("You are not allowed to perform this action");
}
} catch (e) {
res.status(500).json({
message: "Please enter the valid data",
});
}
});
I'm using JWT to determine whether or not the user is an admin. I've set one of the users' admin roles to 'true' in the user schema.
Authentication middleware:
const authentication = async (req, res, next) => {
try {
const token = req.header("Authorization").replace("Bearer ", "");
const decoded = jwt.verify(token, process.env.JWT_SECRET_KEY);
const user = await User.findOne({ _id: decoded._id, "tokens.token": token });
if (!user) {
throw new error();
}
req.token = token
req.user = user
next();
} catch (e) {
res.status(401).send(e);
}
};
However, even non-admin users can post data, which is then saved to the database.
I want to restrict this.
I'm not sure how I can prevent non-admin users from posting data.
You need to check if the user is admin in the Auth middleware.
const authentication = async (req, res, next) => {
try {
const token = req.header('Authorization').replace('Bearer ', '');
const decoded = jwt.verify(token, process.env.JWT_SECRET_KEY);
const user = await User.findOne({
_id: decoded._id,
'tokens.token': token,
admin: true
});
if (!user) {
throw new error();
}
req.token = token;
req.user = user;
next();
} catch (e) {
res.status(401).send(e);
}
};
And remove the line const user = await User.find({ admin: true }); and related if check in the route.
router.post("/bus/add", auth, async (req, res) => {
const bus = new Bus(req.body);
try {
await bus.save();
res.status(201).send(bus);
} catch (e) {
res.status(500).json({
message: "Please enter the valid data",
});
}
});
not really sure if my title is correct but my problem is that I have this reset password token checker in my api that seems to get affected by another api that finds a specific user, this api has user validation.
Here is what they look like:
//get specific user
router.get('/:id', validateToken, async (req, res) => {
const id = req.params.id
const user = await User.findByPk(id);
res.json(user);
});
//reset-password token check
router.get('/reset-pass', async (req, res) => {
await User.findOne({
where: {
resetPasswordToken: req.body.resetPasswordToken,
resetPasswordExpires: {
[Op.gt]: Date.now()
}
}
}).then(user => {
if(!user) {
res.status(401).json({ error: 'Password reset link is invalid or has expired.'})
} else {
res.status(200).send({
username: user.username,
message: 'Password reset link Ok!'
});
}
});
});
then here is the validateToken
const validateToken = (req, res, next) => {
const accessToken = req.cookies['access-token'];
if (!accessToken)
return res.status(401).json({error: 'User not authenticated!'});
try {
const validToken = verify(accessToken, JWT_SECRET)
req.user = validToken;
if(validToken) {
req.authenticated = true;
return next();
}
} catch(err) {
res.clearCookie('access-token')
return res.status(400).json({error: err}).redirect('/');
}
};
when I comment out the get specific user api the reset password token check works. If I remove validateToken it returns null instead of giving me the username and message.
One of the things I notice is the route param "/:id", that means that literally everything would be processed by get specific user because all routes start with "/", only use params in routes with a prefix like "/user/:id" that way only the routes that starts with "/user" will execute that code.
Change your code to:
//get specific user
router.get('/user/:id', validateToken, async (req, res) => {
const id = req.params.id
const user = await User.findByPk(id);
res.json(user);
});
I am quite new to Node.js / Express and development of web apps. I try to do a simple user registration where I hash the password with bcrypt before saving the hash to mongodb. The login form, which should allow a user to login, does subsequently lookup a user in the db and then compares the two passwords.
Certain routes in my web app I do want to protect so that only authenticated user have access to them. So when successfully login in I do send a Json Web Token (jwt) along the response header which should then be used - when redirected to the protected '/lobby' route - to authenticate the user and allow him / her to proceed to that route.
However, I always get the following error:
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
So it looks like it already sends back a response to the client before trying to set the header which of course is then not possible anymore.
I would highly appreciate your help here!
I do use the following code:
Register function
async function register(req, res) {
//Check with user already exists
const emailExists = await User.findOne({email: req.body.email});
if(emailExists) {
return res.status(400).send('User already exists!');
};
//Hash the password and create new user from request data
bcrypt.hash(req.body.password, 10, async function (err, hashedPass){
if(err){
res.json({
error: err
});
}
let user = new User({
name: req.body.name,
email: req.body.email,
username: req.body.username,
password: hashedPass,
password2: hashedPass
});
try {
await user.save();
}catch (err) {
res.status(400).send(err);
};
});
res.render('index');
};
Login function
async function login(req, res) {
const user = await User.findOne({email: req.body.email});
if(!user) {
return res.status(400).json({message: 'User not found!'}).render('index');
};
bcrypt.compare(req.body.password, user.password).then((result)=> {
if(result){
const token = jwt.sign({_id: user._id}, process.env.TOKEN_SECRET);
res.setHeader('auth-token', token.toString());
res.redirect('/lobby');
}else {
return res.status(400).json({message: 'Passwords do not match!'}).render('index');
}
}).catch((err)=> {
console.log(err);
});
};
As a middleware to the '/lobby' route (i.e. when someone does a get request to '/lobby') I use a "verifyToken" function which should ensure correct authentication of the user via jwt.
verifyToken function
const jwt = require('jsonwebtoken');
module.exports = function(req, res, next) {
console.log('verify function started');
const token = req.header('auth-token');
console.log(token);
if(!token) {
res.status(401).json({
message: 'Access denied!'
});
};
try {
const verified = jwt.verify(token, process.env.TOKEN_SECRET);
req.user = verified;
next();
}catch (err) {
res.status(400).json({
message: 'Invalid token!'
});
};
};
As said, I would very much appreciate your help here! I assume the problem is much simpler than I think it is :-).
Cheers
You forgot to return the response in few cases. So it continues to execute other code aswell, that's where server trying to send the response again, which is why you're getting that error.
Change your response like the following.
verifyToken function
const jwt = require('jsonwebtoken');
module.exports = function(req, res, next) {
console.log('verify function started');
const token = req.header('auth-token');
console.log(token);
if(!token) {
return res.status(401).json({ // <-- here you need to `return`
message: 'Access denied!'
});
};
try {
const verified = jwt.verify(token, process.env.TOKEN_SECRET);
req.user = verified;
next();
}catch (err) {
return res.status(400).json({
message: 'Invalid token!'
});
};
};
Register function
async function register(req, res) {
//Check with user already exists
const emailExists = await User.findOne({email: req.body.email});
if(emailExists) {
return res.status(400).send('User already exists!');
};
//Hash the password and create new user from request data
bcrypt.hash(req.body.password, 10, async function (err, hashedPass){
if(err) {
return res.json({ // <-- here as well
error: err
});
}
let user = new User({
name: req.body.name,
email: req.body.email,
username: req.body.username,
password: hashedPass,
password2: hashedPass
});
try {
await user.save();
return res.render('index'); // <-- assuming this is your success response
}catch (err) {
return res.status(400).send(err); <-- here too
};
});
};
Looks like in the Login function the header gets set. I can see this via console.log(res.header('auth-token'));. Subsequently the redirect to "/lobby" gets called because the verifyToken function does start.
However, in the verifyToken function the respective header is then undefined. Because I always also get a 'Access denied!' message.
As said, I do call the verifyToken function as middleware when doing a get request to the /lobby route. The route for '/lobby' looks as follows:
const express = require('express');
const router = express.Router();
const lobbyCtrl = require('../controllers/lobby');
const verify = require('./verifyToken');
router.get('/', verify, lobbyCtrl.display);
module.exports = router;
I have a get request that should return all of the logged-in user's project they created, I believe the code was written well. when I run it on postman I consistently get a 401 unauthorised error, but when I change the request to patch for example, and I also run a patch request on the postman, it works properly. what could be the issue?
// get all logged in user's projects
router.get('/api/project/mine', auth, async (req, res) => {
try {
const id = req.user._id
const projects = await Project.find({owner: id})
res.status(200).send(projects)
} catch (e) {
res.status(401).send()
}
})
the auth middleware
const jwt = require('jsonwebtoken')
const User = require('../models/user')
const auth = async (req, res, next) => {
try {
const token = req.header('Authorization').replace('Bearer ', '')
const decoded = jwt.verify(token, 'creativetoken')
const user = await User.findOne({ _id: decoded._id, 'tokens.token': token })
if (!user) {
throw new Error()
}
req.token = token
req.user = user
next()
} catch (e) {
res.status(401).send({ error: 'Please authenticate' })
}
}
module.exports = auth
Note: the auth makes sure the objectId of the logged-in user is returned through req.user.id
You have 2 try-catch that return 401 but you don't log the error or return the error to frontend.
You need to add console.log(e) in 2 catch block in your auth middleware and your get request.
try {
// some of your code
} catch (e) {
console.log(e); //Add this line
res.status(401).send({ error: 'Please authenticate' })
}
I'm new to NodeJS and ExpressJS. I created an AuthController which handles the login post.
exports.login = async (req, res, next) => {
try {
const email = req.body.email;
const password = req.body.password;
const user = await User.findOne({ email }).select("+password");
if (!user) {
const error = new Error("Wrong Credentials");
error.statusCode = 401;
throw error;
}
const validPassword = await user.validPassword(password);
if (!validPassword) {
const error = new Error("Wrong Credentials");
error.statusCode = 401;
throw error;
}
const token = jwt.encode({id: user.id}, config.jwtSecret);
res.send({user, token});
} catch(err) {
next(err);
}
}
I would like to use this method in my web application to let the user login. If the user succesfully logs in, I get a json response in my browser with the user and the token. That's not what I want. The url for the api login is: /api/auth/login. I created a function in my web application:
router.post('/login', async (req, res, next) => {
var apiResponse = await authController.login(req, res, next);
res.render('profile.ejs', {user: apiResponse.user});
});
I keep getting the error that authController.login is not a function. My question is how can I use the method from my API and use the response in my webapplication? Can someone help me out?
You can do it like this code below: 👇
router.post('/login', authController.login, async (req, res, next) => {
res.render('profile.ejs', {user: apiResponse.user});
});
👨🏫 Please read the documentation about Express Middleware.
Updated: Middleware
Make sure your authController looks like this code below:
exports.login = async (req, res, next) => {
try {
const email = req.body.email;
const password = req.body.password;
const user = await User.findOne({ email }).select("+password");
if (!user) {
const error = new Error("Wrong Credentials");
error.statusCode = 401;
throw error;
}
const validPassword = await user.validPassword(password);
if (!validPassword) {
const error = new Error("Wrong Credentials");
error.statusCode = 401;
throw error;
}
const token = jwt.encode({id: user.id}, config.jwtSecret);
// passing your data to next function
// you can change exampleData with the 'variable' you want
req.exampleData = { user, token }
next();
} catch(err) {
next(err);
}
}
If your authController is same root ("folder") with the your routes, than make it's look like this:
const { login } = require('./authController');
So, now, you can call the authController like this:
router.post('/login', login, async (req, res, next) => {
// your data: "user" and "token" from middleware
const { exampleData } = req;
console.log(exampleData);
res.render('profile.ejs', {user: exampleData});
});
I hope it's can help you 🙏.