17

I am trying to incorporate the express-jwt library and I do not quite understand how it's error handling works.

The documentation says:

Error handling

The default behavior is to throw an error when the token is invalid, so you can >add your custom logic to manage unauthorized access as follows:

    app.use(function (err, req, res, next) {
      if (err.name === 'UnauthorizedError') {
        res.status(401).send('invalid token...');
      }
    });

But I am confused how that works. If I have a simple req res situation, and I want to call next if the token is valid, or call next with an error if it is not, where to I put that app.use function?

For instance, here is my code:

router.post('/', expressJwt({  
  secret: jwtSecret,     
  credentialsRequired: false  
}), (req, res, next) => {   
  databaseController.findUser(req.user.email, (err, user) => {          
    if (err) {          
      return next(err)      
    }                        
    res.json(user)     
  })         
})

The err here would come from my DB call, not from the express-jwt validation. Any help is appreciated.

Community
  • 1
  • 1
Startec
  • 12,496
  • 23
  • 93
  • 160
  • If you look into the code of `express-jwt` https://github.com/auth0/express-jwt/blob/master/lib/index.js#L51, you will find that it uses `next(err)` and `next` for passing request to error handler or request handler. – Mukesh Sharma Dec 05 '16 at 10:12
  • So I am unclear, where is `next` passed into the `expressJwt` function? – Startec Dec 05 '16 at 10:24
  • 1
    On L33, `middleware` function has signature `function(req, res, next)`. And on L130, `middleware` is returned. So, when you pass `expressJwt({})` in your router, it returns a `function(req, res, next)` that accepts express `req`, `res` and `next`. Hope it is clear to you :) – Mukesh Sharma Dec 05 '16 at 10:36

5 Answers5

44

Another way is you could place the middleware with app.use to scan all the routes for a valid jwt in the header or the query string. Any public endpoints can be exempted using the unless keyword. Ex:

app.use(expressjwt({credentialsRequired: true, secret: config.TOKEN_SECRET, requestProperty: 'user'}).unless({path: config.PUBLIC_URLs}));

app.use(function(err, req, res, next) {
    if(err.name === 'UnauthorizedError') {
      res.status(err.status).send({message:err.message});
      logger.error(err);
      return;
    }
 next();
});
Hanu
  • 1,087
  • 11
  • 21
  • 1
    Works perfectly, this should be the accepted answer – Martin De Simone Sep 10 '17 at 03:07
  • Also note you must put next as an argument to that function, otherwise it won't be executed. – magician11 Nov 16 '17 at 11:12
  • @magician11 there is next() as an argument, indeed. What did you mean other than that? – Hanu Nov 16 '17 at 12:11
  • I wasn't using the next function in the body of that function, so I left it out.. and it wasn't executing that block of code at all on an error. I couldn't work it out until I explicitly added that argument back, and it then started executing correctly. Seems like a tricky gotcha. – magician11 Nov 17 '17 at 00:03
  • 1
    shouldn't the last call inside error function be `next(err)` instead? – Vlad Aug 01 '18 at 17:29
  • @Vlad you meant `return next(err)` ? yes it could be depending on if you have another middleware followed by. In my case, it returns the status to the user with a 401 and ends there. – Hanu Aug 02 '18 at 06:11
  • @Amruta-Pani - Sure. Just remember that as long as you have a "next" handle, you should always invoke it so that any other error handlers could trigger. Easy thing to miss... – Vlad Aug 02 '18 at 21:33
  • 2
    I just want to mention that if you only want to include expressJwt in one route instead of all of them. I put the app.use() error function after the route I used expressJwt with. – BamBam22 Sep 11 '18 at 22:03
8

this is my solution for individual routes.

function UseJwt(){
    return [
        jwtExpress({ secret: configuration.jwtSecret, algorithms: ['HS256'] }),
        function(err, req, res, next){
            res.status(err.status).json(err);
        }
    ]
}

usage...

app.get(`/${prefix}/:user_id`,
        ...UseJwt(),
        async function (req, res) {           
           // handle your code here.
        }
)
1

You can create an express middleware just before the code you use to start express server.

// Global error handler that takes 4 arguments and ExpressJS knows that
app.use((err, req, res, next) => {
    res.status(err.status).json(err);
});
app.listen(3000);

I use that for apps use REST but you can use the same approach and modify what should happen based on your needs. If you use Jade template for instance then you need to populate the template with the data you want to show to end user and log the rest on your log file.

app.use((err, req, res, next) => {
    res.locals.status = status;
    res.render('error')
});
Mohammed Ramadan
  • 655
  • 7
  • 10
1

Express-jwt is just a method that returns a RequestHandler (a function that takes in req, res, and next). This RequestHandler can be wrapped in a closure that replaces its next input with one you design that handles or formats errors. For example:

/**
 * Wraps an Express request handler in an error handler
 * @param method RequestHandler to wrap
 * @returns RequestHandler
 */
export function formatError<T extends RequestHandler>(method: T): RequestHandler {

  return async (req, res, next) => {
    const wrapError = (err: any) => {
      return (err)
        ? res.status(err.status).send({ message: err.message })
        : next();
    };

    await method(req, res, wrapError);
  };
}


const jwtMiddleware = formatError(expressjwt(...));

Then just use this closure instead of using expressjwt() directly:

router.post('/', jwtMiddleware, ...);
corpico
  • 617
  • 3
  • 16
  • 26
-1
import { Schema, model } from "mongoose";

export const ROLES = ["Admin", "Estudiante","Docente","Secretario","Vicerrector","Inpector"];

const roleSchema = new Schema(
  {
    name: String,
  },
  {
    versionKey: false,
  }
);

export default model("Role", roleSchema);


//
import { Schema, model } from "mongoose";
import bcrypt from "bcryptjs";

const productSchema = new Schema(
  {
    username: {
      type: String,
      unique: true,
    },
    email: {
      type: String,
      unique: true,
    },
    password: {
      type: String,
      required: true,
    },
    //********************************NUEVOS CAMPOS PARA USUARIOS ADMINISTRADORES
    nombres: {
      type: String,
      required: true,
    },
    apellidos: {
      type: String,
      required: true,
    },
    cedula: {
      type: String,
      unique: true,
    },
    foto: {
      type: String,
      required: true,
    },
    status: {
      type: String,
      required: true,
    },
    telefono: {
      type: String,
      required: true,
    },
    //---------------TIPO DE DOCUMENTOS
    typo:{
      type: String,
    },
    //---------------TIPO MAS DATOS
    roles: [
      {
        type: Schema.Types.ObjectId,
        ref: "Role",
      },
    ],
  },
  {
    timestamps: true,
    versionKey: false,
  }
);

productSchema.statics.encryptPassword = async (password) => {
  const salt = await bcrypt.genSalt(10);
  return await bcrypt.hash(password, salt);
};

productSchema.statics.comparePassword = async (password, receivedPassword) => {
  return await bcrypt.compare(password, receivedPassword)
}

export default model("User", productSchema);

//
import Role from "../models/Role";
import User from "../models/User";

import bcrypt from "bcryptjs";

export const createRoles = async () => {
  try {
    // Count Documents
    const count = await Role.estimatedDocumentCount();

    // check for existing roles
    if (count > 0) return;

    // Create default Roles
    const values = await Promise.all([
      new Role({ name: "Estudiante" }).save(),//user
      new Role({ name: "Docente" }).save(),//moderator
      new Role({ name: "Admin" }).save(),//admin
      new Role({ name: "Secretario" }).save(),//-------+++
      new Role({ name: "Vicerrector" }).save(),//-------+++
      new Role({ name: "Inpector" }).save(),//-------+++
    ]);

    console.log(values);
  } catch (error) {
    console.error(error);
  }
};

export const createAdmin = async () => {
  // check for an existing admin user
  const user = await User.findOne({ email: "10004095632w@gmailcom" });
  // get roles _id
  const roles = await Role.find({ name: { $in: ["Admin", "Estudiante","Docente","Secretario","Vicerrector","Inpector"] } });

  if (!user) {
    // create a new admin user
    await User.create({
      username: "admin",
      email: "10004095632w@gmail.com",
      password: await bcrypt.hash("Imperio 789.", 10),
      roles: roles.map((role) => role._id),
      nombres: "ad",
      apellidos: "ad",
      cedula: "123456789",
      foto: "profile.jpg",
      status: "Activo",
      telefono: "+570995283857",
    });
    console.log('Admin User Created!')
  }
};

//
import jwt from "jsonwebtoken";
import config from "../config";
import User from "../models/User";
import Role from "../models/Role";

export const verifyToken = async (req, res, next) => {
  let token = req.headers["x-access-token"];

  if (!token) return res.status(403).json({ message: "No token provided" });

  try {
    const decoded = jwt.verify(token, config.SECRET);
    req.userId = decoded.id;

    const user = await User.findById(req.userId, { password: 0 });
    if (!user) return res.status(404).json({ message: "No user found" });

    next();
  } catch (error) {
    return res.status(401).json({ message: "Unauthorized!" });
  }
};

export const isSecretario = async (req, res, next) => {
  try {
    const user = await User.findById(req.userId);
    const roles = await Role.find({ _id: { $in: user.roles } });

    for (let i = 0; i < roles.length; i++) {
      if (roles[i].name === "Secretario") {
        next();
        return;
      }
    }

    return res.status(403).json({ message: "Require Moderator Role!" });
  } catch (error) {
    console.log(error)
    return res.status(500).send({ message: error });
  }
};

export const isAdmin = async (req, res, next) => {
  try {
    const user = await User.findById(req.userId);
    const roles = await Role.find({ _id: { $in: user.roles } });

    for (let i = 0; i < roles.length; i++) {
      if (roles[i].name === "Admin"||roles[i].name === "Secretario") {
        next();
        return;
      }
    }

    return res.status(403).json({ message: "Require Admin Role!" });
  } catch (error) {
    console.log(error)
    return res.status(500).send({ message: error });
  }
};

//

import User from "../models/User";
import { ROLES } from "../models/Role";

const checkDuplicateUsernameOrEmail = async (req, res, next) => {
  try {
    const user = await User.findOne({ username: req.body.username });
    if (user)
      return res.status(400).json({ message: "El numero de cédula ya existe" });
    const email = await User.findOne({ email: req.body.email });
    if (email)
      return res.status(400).json({ message: "El correo electrónico ya existe" });
    next();
  } catch (error) {
    res.status(500).json({ message: error });
  }
};

const checkRolesExisted = (req, res, next) => {
  if (req.body.roles) {
    for (let i = 0; i < req.body.roles.length; i++) {
      if (!ROLES.includes(req.body.roles[i])) {
        return res.status(400).json({
          message: `Role ${req.body.roles[i]} does not exist`,
        });
      }
    }
  }

  next();
};

export { checkDuplicateUsernameOrEmail, checkRolesExisted };

//
import * as authJwt from "./authJwt";
import * as verifySignup from "./verifySignup";

export { authJwt, verifySignup };