Learning about the concept of microservices in Nodejs, I have set up two microservices auth and users, both standalone and running on different ports.
The auth service handles user creation and log-in users using a username and password. This works as expected. I've used jwt to generate the tokens. I can easily create a user, create a session token and verify its validity.
My second service, users, I intend to use to show greetings to users and fetch a user's detail. I need to use the auth service to know when a user is logged in in this setting.
However, with my current workings, when I try to go to an endpoint, /users/:id/sayhello with a valid user id and a valid token passed in the headers, I get the following errors:
TypeError: Cannot read properties of undefined (reading 'id') at /path/auth/verifyToken.js:23:21 .
And then this; from jwt:
{
"name": "JsonWebTokenError",
"message": "secret or public key must be provided"
}
Let's look at my setup now.
Here's my verifyToken.js file from the auth service:
const verifyToken = (req, res, next)=>{
const authHeader = req.headers.token
// split the header, and get the token "Bearer token"
const token = authHeader.split(" ")[1];
if (authHeader) {
jwt.verify(token, process.env.JWT_SEC, (err, user)=>{
if (err) res.status(403).json(err);
req.user = user
next();
})
} else {
return res.status(401).json("You are not authenticated")
}
}
const verifyTokenAndAuthorization = (req, res, next) =>{
verifyToken(req, res, ()=>{
if(req.user.id === req.params.id){ // error traced back to this line
next();
}else{
res.status(403).json("Permission denied!");
}
})
}
From my users service, here's the code that uses the auth service to know when the user is logged in then say hello.
app.get("/users/:id/sayhello", verifyTokenAndAuthorization, async (req, res) => {
try {
const user = await User.findById(req.params.id);
console.log(req.params.id) // undefined
res.status(200).json(`Hello ${user.username}`);
} catch (error) {
res.status(500).json(error);
}
});
I've with no success sought any leads from similar posts like A,B and C
I'm not sure of what's not right. I'll appreciate possible suggestions and leads towards a fix.
console.log(process.env.JWT_SEC)
The authentication process got failed, so the user property was unset on the req object, so req.user is null.
Ensure the integrity of your inputs.
I think in the Headers convention of using Authorization or authorization key will not dissappoint as its the most preferred way of doing this have something like
I have done a rolebased approach in tackling this so check the implementation as of the question rolebased structure.
What to check for
Check if the Authorization header is available
If it does not exist just throw an exception or a response with some error
Check if the token is present in the Bearer token
If the token is not there just throw an exception to temnate the excecution
If the AuthHeader and token are present then now you can be certain that you have the token, thus you can just return the jwt.verify(...args:[])
Depending on validity everything here is on check
If the jwt is valid then the JWT payload is there thn we can pass it to the request object to carry it through to the other middlewares
If we want to now have Athorization then we override the next parameter with a function to execute on it`s behalf
From here now you can check on the user Roles and return next based on what permissions they have.
import RoleModel from "../models/RoleModel"
import UserModel from "../models/UserModel"
class AuthMiddleware {
constructor(role:typeof Model, user:typeof Model) {
this.role=role
this.user=user
}
verifyJwt = async (req, res, next) => {
try {
const AuthHeader = req.headers["authorization"]
if (!AuthHeader) {
return res.status(401).json("Please provide an auth token")
}
const token = AuthHeader.split(" ")[1]
if (!token) {
return res.status(401).json("Please provide an auth token")
}
return jwt.verify(token, SECRET_KEY, async (error, payload) => {
if (error) {
return res.status(401).redirect("/auth/login")
}
const decodedPayload = payload as JWTPayloadType
req.user = decodedPayload
return next()
})
} catch (error) {
return next(error)
}
}
loginRequired = async (req, res, next) => {
try {
this.verifyJwt(req, res, async () => {
const user = await this.user.findById(req.user.userId)
const role = await this.role.findById(user.role)
const permitted = await role.hasPermission(Permissions.USER)
if (!permitted) {
return res.status(403).json("Forbidden")
}
return next()
})
} catch (error) {
return next(error)
}
}
adminRequired = async (req, res, next) => {
try {
this.verifyJwt(req, res, async () => {
const user = await this.user.findById(req.user.userId)
const role = await this.role.findById(user.role)
const permitted = await role.hasPermission(Permissions.ADMIN)
if (!permitted) {
return res.status(403).json("Forbidden")
}
return next()
})
} catch (error) {
return next(error)
}
}
}
export default new AuthMiddleware(RoleModel, UserModel)
Applying this to a middleware
import auth from "../middlewares/AuthMiddleware"
/**
* ************* UPDATE USER PROFILE ********
*/
router
.route("/update/profile/:id")
.put(
auth.loginRequired,
imageUpload.single("profile"),
uController.updateUserDetails,
uMiddleware.uploadProfilePic,
)
Assuming you supply the middlewares to the given route its easy to abstract away the verify jwt and have a login_required based on the roles you want achieved.
Full implementation of this I have on this Github repo Github link
Related
I am trying to set up viewpages to show the authenticated user's info such as user's name or email on the page when they are logged in.
To do so, I am using the res.locals function to set the user data at the global level for the pages to access.
const jwt = require("jsonwebtoken")
const User = require("../models/User")
const checkUser = (req, res, next) => {
const token = req.cookies.jwt
if (token) {
jwt.verify(token, "Company_Special_Code", async (err, decodedToken) => {
if (err) {
res.locals.user = null // Set it to null if the user does not exist
next();
} else {
let user = await User.findById(decodedToken.id)
res.locals.user = user
next();
}
})
} else {
res.locals.user = null
next();
}
}
module.exports = {
checkUser
}
The first code, where I call next() function every time the code reaches an endpoint, allows the pages to access the user info without any errors.
However, if I call the next() function only once at the very bottom of the checkUser() function, it causes error claiming that the user is not defined at the view page level. The code is as follows:
const jwt = require("jsonwebtoken")
const User = require("../models/User")
const checkUser = (req, res, next) => {
const token = req.cookies.jwt
if (token) {
jwt.verify(token, "Company_Special_Code", async (err, decodedToken) => {
if (err) {
res.locals.user = null // Set it to null if the user does not exist
} else {
let user = await User.findById(decodedToken.id)
res.locals.user = user
}
})
} else {
res.locals.user = null
}
next();
}
module.exports = {
checkUser
}
If I coded the function correctly, the checkUser() function should get to the next() function at the bottom regardless of the status of jwt token or if there was an error during the token verification process. I would really appreciate your help if you can tell me what I am getting it wrong here...
Your jwt.verify is has an asynchronous callback and next() at the bottom is being called before that returns. So you either need to put next() into that callback, or use jsonwebtoken synchronously. Something like this:
const checkUser = (req, res, next) => {
const token = req.cookies.jwt
if (token) {
try {
const decodedToken = jwt.verify(token, "Company_Special_Code")
// This only runs if the token was decoded successfully
let user = await User.findById(decodedToken.id)
res.locals.user = user
} catch (error) {
res.locals.user = null // Set it to null if the user does not exist
}
} else {
res.locals.user = null
}
next();
}
When you use an async callback like that, javascript will continue processing the rest of the script while that callback is running on the side (more or less). So next() is being called unaware of the need to wait for the callback or anything it might handle.
Your validation part misses the correct error handling in middleware. If token is invalid, then why should user get access to controller, you can send error from middleware itself. If you are not sending error from middleware and calling next(), then will defeat purpose of your authentication middleware.
Update your code as follows,
const jwt = require("jsonwebtoken")
const User = require("../models/User")
// The routes, which does not requires aurthentication
const usecuredRoutes = []
const checkUser = (req, res, next) => {
if(usecuredRoutes.indexOf(req.path) === -1){
const token = req.cookies.jwt
if (token) {
jwt.verify(token, "Company_Special_Code", async (err, decodedToken) => {
if (err) {
res.locals.user = null // Set it to null if the user does not exist
// Token is invalid, then telll user that he is don't have access to the resource
res.status(403).send('Unauthorized')
} else {
let user = await User.findById(decodedToken.id)
res.locals.user = user
next();
}
})
} else {
// Token does not exists, then telll user that he is don't have access to the resource
res.status(403).send('Unauthorized')
}
} else {
next()
}
}
module.exports = {
checkUser
}
Let's say I have 10 routes and each route is accessible to only a specific type of user. When a user logins, a token is generated.These user tokens are generated with their _id and a token secret which is stored in .env file.
Normally token verification for each user type is done with separate functions because different type of user has different token secret. For example, user1's token secret maybe TOKEN_SECRET_USER2 = 6ygfewf6hj, and user2's token maybe TOKEN_SECRET_USER1 = 87uhjkaf89.
And when any request is made to a route, the user token is verified to see if the user can access that route or not.
Here's two example route accessible to different user types,
// Route accessible to user type 1
router.get("/foo", verifyTokenUSER1, async (req, res) => {
// All the good stuff
});
// Route accessible to user type 2
router.post("/bar", verifyTokenUSER2, async (req, res) => {
// All the good stuff
});
Here's some method of verification module looks like,
// Verification for user 1
const verifyTokenUSER1 = (req, res, next) => {
const token = req.header("auth-token");
if (!token) return res.status(401).send();
try {
jwt.verify(token, process.env.TOKEN_SECRET_USER1);
next();
} catch (err) {
res.status(401).send();
}
};
// Verification for user 2
const verifyTokenUSER2 = (req, res, next) => {
const token = req.header("auth-token");
if (!token) return res.status(401).send();
try {
jwt.verify(token, process.env.TOKEN_SECRET_USER2);
next();
} catch (err) {
res.status(401).send();
}
};
As you can see, there's only one change in the above methods, which is the access token secret of the user types.
I would like to verify them using 1(one) single function if possible. But I can't pass any value as parameter to the verify methods. So, how can I remove duplication here?
You actually can pass a parameter to the verify method if you use bind (mdn):
// Route accessible to user type 1
router.get("/foo", verifyToken.bind(null, process.env.TOKEN_SECRET_USER1), async (req, res) => {
// All the good stuff
});
// Route accessible to user type 2
router.post("/bar", verifyToken.bind(null, process.env.TOKEN_SECRET_USER2), async (req, res) => {
// All the good stuff
});
the bind method of a function here is receiving 2 parameters: first, the this context for the function, and second, the first argument. It returns a new function that will receive the provided token as the first argument, and will receive req, res, next as the next arguments.
const verifyToken = (tokenSecret, req, res, next) => {
const token = req.header("auth-token");
if (!token) return res.status(401).send();
try {
jwt.verify(token, tokenSecret);
next();
} catch (err) {
res.status(401).send();
}
};
Another way to do this that is equivalent is building a verification method "factory" which returns a verification function that has a closure (mdn) on the token:
// Route accessible to user type 1
router.get("/foo", getTokenVerifier(process.env.TOKEN_SECRET_USER1), async (req, res) => {
// All the good stuff
});
// Route accessible to user type 2
router.post("/bar", getTokenVerifier(process.env.TOKEN_SECRET_USER2), async (req, res) => {
// All the good stuff
});
// this function returns a new function, with a closure on the provided tokenSecret
const getTokenVerifier = (tokenSecret) => {
return (req, res, next) => {
const token = req.header("auth-token");
if (!token) return res.status(401).send();
try {
jwt.verify(token, tokenSecret);
next();
} catch (err) {
res.status(401).send();
}
}
}
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 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.
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) => {
});