Switching Profile routes based on user role in registration (Express.js) - node.js

A bit of a long read
I am currently building the backend for a MERN project, with quite an interesting structure (I would be changing the specifics because it is a private project).
Database: There are 4 database schemas at the moment, 1 user schema with 3 different roles: student, teacher, and sponsor.
const mongoose = require('mongoose')
const UserSchema = new mongoose.Schema({
username : {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
role: {
type: String,
enum: ['student', 'teacher', 'sponsor'],
required: true,
},
dateCreated: {
type: Date,
default: Date.now
}
})
module.exports = User = mongoose.model('user', UserSchema)
**The 3 types of user roles have their own unique but quite similar profile schema (TeacherProfile, StudentProfile e.t.c) which all reference the user shcema by ID **.
const mongoose = require('mongoose');
const studentProfileSchema = new mongoose.Schema({
user: {
// create a reference to the user schema
type: mongoose.Schema.Types.ObjectId,
ref: 'user'
},.........
I have an authentication middleware that takes care of the jwt logic.
Now things get interesting at the routes
I have a user route that takes care of user registration
An auth route for login and authentication,
And 3 routes for the profiles
What I desire to build is a middleware logic that would switch between the 3 project routes once a user registers, so he/she would be returned the profile that desribes choosen role during registration.
keep in mind that there are calls to the database, which i have to wrap inside of an async block
This is an example of one of such routes:
const express = require('express');
const router = express.Router();
const config = require('config');
// validator
const {check, validationResult} = require('express-validator');
// auth middleware
const auth = require('../../middleware/authMiddleware');
// db collections
const SponsorProfile = require('../../models/SponsorProfile');
const User = require('../../models/User');
// #route GET api/SponsorProfile/me
// #desc GET current user profile
// #access Private
router.get('/me', auth, async (req, res) => {
try {
// fetch profile object
const sponsorProfile = await SponsorProfile.findOne({user:req.user.id});
// check if profile exists
if(!sponsorProfile) {
return res.status(400).json({msg: 'Hello Sponsor, You have not created a profile'})
}
res.json(sponsorProfile)
} catch (error) {
console.error(error.message);
res.status(500).json({msg:'This is our fault not yours'})
}
})
module.exports= router;
Now this is what I tried:
I built a master router that uses all the profile routers as sub-routers starting from the student to the sponsor.
const express = require('express');
const profilesRouter = express.Router();
profilesRouter.use('/', require('./studentProfile'));
profilesRouter.use('/', require('./teacherProfile'));
profilesRouter.use('/', require('./sponsorProfile'));
module.exports = profilesRouter;
It is then called in server.js like this:
app.use('/api/profilesRouter', require('./routes/api/profilesRouter'));
The appraoch was to place a middleware function in the first 2 routers and leave the third one empty so there will be a switch, if the criterias in the first two passes.
async function shouldRouterChange(req, res, next) {
let userRole = await User.findOne({user:req.role}).select('-password');
console.log(userRole)
if ( userRole === 'mentor') {
return next('router');
}
return next();
}
function shouldRouterChange(req, res, next) {
if (req.user.role === 'teacher') {
return next('router');
}
return next();
}
// #route GET api/studentProfile/me
// #desc GET current user profile
// #access Private
router.get('/me', [auth, shouldRouterChange], async (req, res) => {
try {
// check if profile exists
const studentProfile = await StudentProfile.findOne({user:req.user.id}).populate('user', ['role']);
if(!studentProfile) {
return res.status(400).json({msg: 'Hello student, You have not created a profile'})
}
res.json(studentProfile)
} catch (error) {
console.error(error.message);
res.status(500).json({msg:'This is our fault not yours'})
}
})
module.exports= router;
Obviously that did not work
Then I tried the middleware function like this
async function shouldRouterChange(req, res, next) {
let userRole = await User.findOne({user:req.role}).select('-password');
console.log(userRole)
if ( userRole === 'mentor') {
return next('router');
}
return next();
}
No Way
Then this:
async function shouldRouterChange(req, res, next) {
try {
let userRole = await studentProfile.findOne({user:req.user.id}).populate('user', ['role']);
// conditional
console.log(userRole)
if (userRole.role==='mentor') {
return next('router')
}
return next()
} catch (error) {
console.error(error.message)
res.status(500).json({msg: 'Server Error'})
}
}
I debugged as best as I could and realized that:
The whole switching structure actually works nicely
The problem lies in the middleware structure
The first middleware structure might be corerct, apart from the conditional.
The conditional equates to null, or undefined (it does not get the user role properly).
The whole middleware logic might have to be called inside the router.get() logic that returns the profile.
My question is how can i make the conditional correct, and consequently structure the middleware to work properly (maybe without doing too much change on my app structure, won't mind anyways)

I have solved the error, and it works like a charm
To use router level middleware for switching routers (can also switch single routes)
Build a master router for all those routers (now sub-routers):
const express = require('express');
const profilesRouter = express.Router();
profilesRouter.use('/', require('./studentProfile'));
profilesRouter.use('/', require('./teacherProfile'));
profilesRouter.use('/', require('./sponsorProfile'));
module.exports = profilesRouter;
Add the master router path to server.js:
app.use('/api/profileRouter', require('./routes/api/profileRouter'));
Build the middleware logic, to switch the routers conditionally:
Middleware to switch from student to teacher router file:
const jwt = require('jsonwebtoken');
const config = require('config');
// Check user profile
function teacherSwitch(req, res, next) {
const token = req.header('x-auth-token');
// if no token is returned
if (!token) {
return res.status(401).json({ msg: 'No token, permission denied' });
}
try {
// decode token
const decoded = jwt.verify(token, config.get('jwtSecret'));
// set role to the same value as in the request
req.user.role = decoded.user.role;
// check if role is teacher
if (decoded.user.role !== 'student') {
return next('router');
}
return next();
} catch (error) {
res.status(401).json({ msg: 'Wrong token, authentication failed' });
}
}
module.exports = teacherSwitch;
Our logic here is to switch based on the role chosen by the user when registering. This means that role is a DB field in the user schema.
All we have to do is to add the role in our authentication payload.
A token is created on user registration, and that token uses the user id as payload, so if we add the role to that payload, we can now access it in the "req" object and build our conditional around it.
This payload is located in my auth route using the auth middleware:
// create jwt payload
const payload = await {
user: {
id: user.id,
role: user.role,
},
};
// if user login is successful, return token
jwt.sign(payload, config.get('jwtSecret'),
{ expiresIn: 36000 }, (error, token) => {
if (error) throw error;
return res.json({ token });
});
} catch (error) {
console.error(error.message);
res.status(500).send('Server Error');
}
Also, the "next(router)" in the middleware function is the magic that switches to the next router in the router stack found in profilesRouter.
It initiates a switch which is then called when we finally return next.
return next().
Call the teacherSwitch middleware in the first sub-router (student router)
router.get('/me', [auth, teacherSwitch], async (req, res) => {
try {
// check if profile exists
const studentProfile = await StudentProfile.findOne({ user: req.user.id });
if (!teacherProfile) {
return res.status(400).json({ msg: 'Hello student, You have not created a profile' });
}
return res.json(studentProfile);
} catch (error) {
console.error(error.message);
return res.status(500).json({ msg: 'This is our fault not yours' });
}
});
Create another middleware to switch to the 3rd sub-router conditionally
function sponsorSwitch(req, res, next) {
const token = req.header('x-auth-token');
// if no token is returned
if (!token) {
return res.status(401).json({ msg: 'No token, permission denied' });
}
try {
// decode token
const decoded = jwt.verify(token, config.get('jwtSecret'));
// set role to the same value as in the request
req.user.role = decoded.user.role;
// check if role is partner
if (decoded.user.role !== 'teacher') {
return next('router');
}
return next();
} catch (error) {
res.status(401).json({ msg: 'Wrong token, authentication failed' });
}
}
6 Call it in the second sub-router.
router.get('/me', [auth, partnerSwitch], async (req, res) => ...
And that is it, as long as the condition is not met, it will switch and switch until the condition is passed.
Works just like a regular switch statement.
What I was missing here was that to be able to use the role to switch, I had to call it in as part of the auth payload, this way it becomes part of the request object in the req, res cycle, and hence can be manipulated as you like.
We never need to call another middleware in the last router, because logically the switching only gets there when everything fails, just like the default in a regular switch statement.
N.B: It is actually important to call the middleware function in the last router, as this would make this work perfectly in the frontend (I used react), not doing this caused bug where react could not return the last route if it had to, which is not what we want.
Got some very valuable help from John
An used this site to manually decode my JWT tokens, to know exactly what was going on.

Related

how to protect a route from being used until after they verified?

Assume the user wants to go to their settings page where they could change their personal info (e.g. first name). I want to trigger an OTP sent to their phone to verify it is actually the user going to this page before they could actually update their info. We use a third party service that handles the sms part (expiration, error message etc). I have the whole sending + updating part figured out, I'm just unsure on how to protect the update route only after they're verified.
Does anyone know what the best practice with this use case? i would be grateful for any help.
/* verifies if the access token (which user gets upon login) is valid */
const auth = (req, res, next) => {
const token = req.header('access-token');
if (!token) return res.status(401).json('Not Authorized');
try {
const payload = jwt.verify(token, 'secret');
req.userId = payload.userId
next();
} catch (ex) {
res.status(400).json('Invalid.');
}
};
/* sends one time password to user */
router.post(
'/update/user-check/send',
auth,
async (req, res) => {
try {
// gets user from db
const curUser = await getUser({
userId: req.userId,
});
// sends otp
await SMSService.sendOneTimePasswordThroughPhone(curUser.phoneNumber)
res.status(200).json("sent one time password")
} catch (e) {
console.log(e);
return res.status(401).json(e);
}
},
);
/* verifies one time password to user */
router.post(
'/update/user-check/verify',
auth,
async (req, res) => {
try {
const oneTimePassword = req.body.oneTimePassword
// gets user from db
const curUser = await getUser({
userId: req.userId,
});
// checks otp
const result = await SMSService.checkOTP(oneTimePassword, curUser.phoneNumber);
res.status(200).json("verified")
} catch (e) {
return res.status(401).json(e);
}
},
);
/* updates user's first name */
router.put(
'/update/first-name',
auth,
async (req, res) => {
try {
// updates first name
const curUser = await updateFirstName({
userId: req.userId,
});
res.status(200).json("updated first name")
} catch (e) {
return res.status(401).json(e);
}
},
);
There is one big flaw with my implementation. I have no way to ensure '/update/first-name' route is protected only after they are validated. How do I go about this ? For example, currently if they use the put route and they haven't verified otp yet, then it would still allow them to use that route

api call getting affected by another api call's validation

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);
});

How can i allow specific user categories to access specific routes of application?

My application consists of two type of users. Let's say A and B. Both of them first needs to authenticated. Authentication is done. But now i want A to access specific routes and if it tries to access B routes i want to give A error like access denied to this route and same for B. A is type=0 and B is type=1.
For authetication i am using this middleware which uses token:
auth.js:
const authenticate = (req, res, next) => {
var token = req.cookies['x-auth'];
User.findByToken(token).then(user => {
if(!user){
return Promise.reject();
}
req.user = user;
next();
}).catch(err => {
console.log(err);
var response = {
status:'failure',
message: err.message
};
res.status(401).send(response);
})
};
How should i proceed to achieve this?
Here's your solution, where roles is an array of strings containing the allowed roles for that specific route. This also implies that the users (in your MongoDB model) have a role field.
const authenticateWithRole = (roles = []) => {
return async (req, res next) => {
const token = req.cookies['x-auth'];
try {
const user = await User.findByToken(token);
if (roles.includes(user.role)) {
// Store the informations for the middleware
req.user = user;
next();
} else {
res.status(403).send({
// Your error-response here...
});
}
}
catch (e) {
res.status(401).send({
// Your error-response here...
});
}
}
}
// Later in your APIs
app.use('/limited-route', authenticateWithRole([
'admin',
'student'
]), (req, res) => {
});

user authentication and authorization. Displaying only looged user profile

I am having an issue with user authentication aftre sign in. This is part of expanding my knowledge and now I am stuck with this.
My code looks like this:
Route:
const express = require('express');
const router = express.Router();
/* import controllers */
const {
findById,
isAuth,
isAdmin,
} = require('../controllers/user.controller.js');
const { requierSignin } = require('../controllers/auth.controller.js');
router.param('userId', findById);
router.get('/test/:userId', requierSignin, isAuth, findById, (req, res) => {
res.json({ user: req.profile });
});
Here i have sign in, find user by id and my authorization method. This is a test route.
As i goes for my controllers:
User:
const User = require('../models/user.models');
//user middleware
exports.findById = async (req, res, next, id) => {
try {
let user = await User.findById(id).exec();
if (!user) {
return res.status(401).json({
errors: [
{
msg: 'User not found',
},
],
});
}
req.profile = user; // this will get user profile based on User
next();
} catch (err) {
console.error(err.message);
res.status(500).send('Server error');
}
};
/* check if user is authenticated */
exports.isAuth = (req, res, next) => {
/* id we have user that will send id and is auth */
let user = req.profile && req.auth && req.profile._id == req.auth._id;
console.log(req.profile);
console.log('auth', req.auth);
console.log('id', req.profile._id);
console.log('user', user);
if (!user) {
return res.status(403).json({
errors: [
{
msg: 'Access Denied',
},
],
});
}
next();
};
and my sign in method from auth.controller
const config = require('config');
const jwt = require('jsonwebtoken'); //generate token
const expressJwt = require('express-jwt'); //auth check
const User = require('../models/user.models');
const secret = config.get('jwtSecret');
exports.requierSignin = expressJwt({
secret,
userProperpty: 'auth',
});
This is only part of that code but if rest will be neede I will update it
As far i get. User sign in and the getting a profile is working. Bu I want to protect other user profiles with isAuth. This is getting me "Access denied". From console log i got
auth undefined
id 5eaac004200b95869cc76531
user undefined
GET /api/test/5eaac004200b95869cc76531 403 88.591 ms - 36
But when i change in my isAuth method user to be only:
let user = req.profile
user is defined.
Not sure what I am missing here :/ Not sure why i don't get in my 'auth' ._id and this is causing issues
github repo
Got this working
Issue was in my user sign in method:
const payload = {
user: {
id: user._id,
email: user.email,
name: user.name,
role: user.role,
},
};
return res.json({
token,
payload,
});
In my payload i sending
id:user._id
But when i generted a token i was doing sending id
// generate a token with id and secret
const token = jwt.sign({ _id: _id }, secretJwt);
So every time when myt prtected rout was checking id from token and user
let user = req.profile && req.auth && req.profile._id == req.auth._id;
This was false as there was no id to compare.
Setting _id to id solved my issue
// generate a token with id and secret
const token = jwt.sign({ _id: _id }, secretJwt);
That was a brain cracker for me.
Even I'm facing the same issue using "postman", as "req.profile" property has to be set from frontend only if the user is "signedIn"
req.profile = user
so we need to populate the user first
exports.setUserInfo = function setUserInfo(request) {
const getUserInfo = {
_id: request._id,
firstName: request.profile.firstName,
lastName: request.profile.lastName,
email: request.email,
role: request.role
};
return getUserInfo;
};
git repo https://github.com/NRSingh007/mern-starter/blob/master/server/helpers.js

How to create a secret string with JWT_KEY in Node.JS app

I'm developing a Node.JS & MongoDB app inserting articles and categories, with a signup and login users system. I need to add/fix a secret string on order to make Jsonwebtoken (JWT_KEY) work properly.
My authentication or authorization fails when I try to add an article with details (title, attached picture ect.) threw Postman, probably because I made a mistake installing or using the jsonwebtoken library. It maybe a mistake in the nodemon.json file that is should be hidden at the end (user, password, JWT_KEY), but maybe in another part of my code.
The Postman process connects with the article.js routes file, that seems to be fine. The relevant part is the createArticle POST, since the rest work fine so far:
const express = require('express');
const router = express.Router();
const upload = require('../middlewares/upload');
const checkAuth = require('../middlewares/checkAuth');
const {
getAllArticles,
createArticle,
getArticle,
updateArticle,
deleteArticle
} = require('../controllers/articles');
router.get('/', getAllArticles);
router.get('/:articleId', getArticle);
router.post('/', checkAuth, upload.single('image'), createArticle);
router.patch('/:articleId', checkAuth, updateArticle);
router.delete('/:articleId', checkAuth, deleteArticle);
module.exports = router;
Here is the authChek.js middleware that is responsible of the authorization process:
const jwt = require('jsonwebtoken');
const checkAuth = (req, res, next) => {
try {
const token = req.headers.authorization.split('')[1];
jwt.verify(token, process.env.JWT_KEY);
next();
} catch(error) {
res.status(401).json({
message: 'Auth failed'
})
}
}
module.exports = checkAuth;
The verify seems ok and should work fine connecting to nodemon. If it's all fine, Postman should return back a message that the authorization succeeded - but it returns failed auth. Here, in the article.js controller, the POST method seems fine to and should not catch an error of 500, 401 or 409:
const mongoose = require('mongoose');
const Article = require('../models/article');
const Category = require('../models/category');
module.exports = {
createArticle: (req, res) => {
const { path: image } = req.file;
const { title, description, content, categoryId } = req.body;
Category.findById(categoryId).then((category) => {
if (!category) {
return res.status(404).json({
message: 'Category not found'
})
}
const article = new Article({
_id: new mongoose.Types.ObjectId(),
title,
description,
content,
categoryId,
image: image.replace('\\','/')
});
return article.save();
}).then(() => {
res.status(200).json({
message: 'Created article'
})
}).catch(error => {
res.status(500).json({
error
})
});
}
}
Another file using the JWT_KEY is the users.js controller, in the login part. Look at the area of the if & result. It may fail to connect properly to the .env part of the nodemon.json file. See here "process.env.JWT_KEY":
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const User = require('../models/user');
module.exports = {
login: (req, res) => {
const { email, password } = req.body;
User.find({ email }).then((users) => {
if (users.length === 0) {
return res.status(401).json ({
message: 'Authentication failed'
});
}
const [ user ] = users;
bcrypt.compare(password, user.password, (error, result) => {
if (error) {
return res.status(401).json({
message: 'Authentication failed'
});
}
if (result) {
const token = jwt.sign({
id: user._id,
email: user.email,
},
process.env.JWT_KEY,
{
expiresIn: "1H"
});
return res.status(200).json({
message: 'Authentication successful',
token
})
}
res.status(401).json({
message: 'Authentication failed'
});
})
})
}
}
Is there something to fix here? Or how can I check if my JWT_KEY in nodemon.json is written properly or wrong? If the string is generated by a library or taken from somewhere else, I don't know where to search for it in my app or around the web.

Resources