Matheus Bernardi
Matheus Bernardi

Reputation: 151

How to implement JWT with Passport on this example

I'm setting a server and want to respond the user with an JWT, after he loged in. I'm using passport for now, but don't know how to implement the verification JWT in this case.

Here's the route to log in:

  app.route('/login')
    .get(users.renderLogin)
    .post(users.auth);

When the user loged in, i send his Token with some information:

exports.auth = function (req, res, next){

  passport.authenticate('local', {session: false}, (err, user, info) => {

    if (err) { return next(err); }
      console.log(err);

    if (!user) { return res.redirect('/login'); }

    req.logIn(user, function(err) {
      if (err) { return next(err); }

      // Loged in 
      const userInfo = {
        username: user.username,
        name: user.name,
        age: user.age,
        groupid: user.groupid,
        email: user.email,
      }
      const token = jwt.sign(userInfo, process.env.JWT_SEC);
      res.json({
        user,
        token
      })
      // res.redirect('/');
    });
  })(req, res, next);
};

After that, i need verify JWT token in every user call. Someone has any tips?

Thank you

Upvotes: 0

Views: 743

Answers (1)

Xcrowzz
Xcrowzz

Reputation: 192

From passport :

Passport's sole purpose is to authenticate requests, which it does through an extensible set of plugins known as strategies.

Moreover from passport-local :

The local authentication strategy authenticates users using a username and password. The strategy requires a verify callback, which accepts these credentials and calls done providing a user.

Therefore, passport's local strategy will only authenticate requests made with both a username and a password provided. Hence the client will have to send these credentials every time it wants to reach the app.

So, to authenticate a request using web tokens you need to provide a login process that sets a JWT. (The client will then have to send the token only, without storing and transmitting the clear password each time)

To do so, there are a lot of packages available on npm. I personaly use and recommend jsonwebtoken

User Model

Assuming you have a user model such as

const UserSchema = new mongoose.Schema({
email: {
    type: String,
    unique: true,
    required: true,
    maxlength: 254,
    trim: true,
    match: /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/
    // Ref :            RFC 5322 compliant regex
    // Visualizer :     https://regexper.com/#(%3F%3A%5Ba-z0-9!%23%24%25%26'*%2B%2F%3D%3F%5E_%60%7B%7C%7D~-%5D%2B(%3F%3A%5C.%5Ba-z0-9!%23%24%25%26'*%2B%2F%3D%3F%5E_%60%7B%7C%7D~-%5D%2B)*%7C%22(%3F%3A%5B%5Cx01-%5Cx08%5Cx0b%5Cx0c%5Cx0e-%5Cx1f%5Cx21%5Cx23-%5Cx5b%5Cx5d-%5Cx7f%5D%7C%5C%5C%5B%5Cx01-%5Cx09%5Cx0b%5Cx0c%5Cx0e-%5Cx7f%5D)*%22)%40(%3F%3A(%3F%3A%5Ba-z0-9%5D(%3F%3A%5Ba-z0-9-%5D*%5Ba-z0-9%5D)%3F%5C.)%2B%5Ba-z0-9%5D(%3F%3A%5Ba-z0-9-%5D*%5Ba-z0-9%5D)%3F%7C%5C%5B(%3F%3A(%3F%3A(2(5%5B0-5%5D%7C%5B0-4%5D%5B0-9%5D)%7C1%5B0-9%5D%5B0-9%5D%7C%5B1-9%5D%3F%5B0-9%5D))%5C.)%7B3%7D(%3F%3A(2(5%5B0-5%5D%7C%5B0-4%5D%5B0-9%5D)%7C1%5B0-9%5D%5B0-9%5D%7C%5B1-9%5D%3F%5B0-9%5D)%7C%5Ba-z0-9-%5D*%5Ba-z0-9%5D%3A(%3F%3A%5B%5Cx01-%5Cx08%5Cx0b%5Cx0c%5Cx0e-%5Cx1f%5Cx21-%5Cx5a%5Cx53-%5Cx7f%5D%7C%5C%5C%5B%5Cx01-%5Cx09%5Cx0b%5Cx0c%5Cx0e-%5Cx7f%5D)%2B)%5C%5D)
    // SFSM :           https://en.wikipedia.org/wiki/Finite-state_machine
    // Xcrowzz' note :  Seems to work for 99.99% of email addresses, containing either local, DNS zone and/or IPv4/v6 addresses.
    // Xcrowzz' note :  Regex can be targeted for Catastrophic Backtracking (https://www.regular-expressions.info/catastrophic.html) ; See https://www.npmjs.com/package/vuln-regex-detector to check them before someone DDOS your app.
},
password: {
    type: String,
    required: true,
    minlength: 8,
    maxlength: 254,
    select: false
},
username: {
    type: String,
    required: false,
    unique: true,
    maxlength: 254
}
});
module.exports = mongoose.model('User', UserSchema);

User Controller

Now, it's time to implement the JWT generation and signature, which should be done when a user successfully logs in. Aka, when both the username and password provided in the body request matches the DB (Don't forget to compare the clear password with the hash stored using bcrypt.compare)

const jwt = require('jsonwebtoken');
[...]
exports.logUser = async (req, res) => {
    if (!req.body.email || !req.body.password) { return res.status(400).send('Bad Request');}
    else {
        await UserModel.findOne({ email: req.body.email }, async (err, user) => {
            if (err) return res.status(500).send('Internal Server Error');
            if (!user) return res.status(404).send('Not Found');

            let passwordIsValid = await bcrypt.compare(req.body.password, user.password);
            if (passwordIsValid) {
                let token = jwt.sign({ id: user._id }, secret, { expiresIn: 86400 }); // 24 Hours
                return res.status(200).send({ auth : true, token: token });
            } else {
                return res.status(404).send('Not Found');
            }
        }).select('+password'); // Overrides model's 'select:false' property of the password model's property in order to compare it with given plaintext pwd.
    }
};

Notes

let token = jwt.sign({ id: user._id }, secret, { expiresIn: 86400 }); creates the token based on an id and a previously defined secret that should be a very long and random string, the whole integrity and security of the token can be compromised by a weak secret.

This is the most basic configuration for a JWT, some researches will prove you that this code is not production ready. But in your case, that will do the trick.

Auth Controller

Finally, you have to design the authentication middleware that compares the provided token with the one previously signed. Once again, it uses the secret to compare them.

const secret = (process.env.TokenSuperSecret) ? 
process.env.TokenSuperSecret : 'SuperSecret';
const jwt = require('jsonwebtoken');
const passport = require('passport');
const CustomStrategy = require('passport-custom');

passport.use('jwt', new CustomStrategy(async (req, callback, err) => {
const token = req.headers['x-access-token'];
if (!token) return callback(err);
await jwt.verify(token, secret, (err, decoded) => {
    if (err) return callback(err);
    req.userId = decoded.id;
    callback(null, req);
    });
}));

exports.isAuthenticated = passport.authenticate('jwt', {
  session: false
}, null);

Notes

jwt.verify(token, secret, (err, decoded) => {} returns a promise containing the decoded userId (that you encoded with jwt.sign during the login process). Feel free to pass it around your middlewares to know which user is currently performing the request.

So long, and thanks for all the fish !

Upvotes: 2

Related Questions