Search code examples
node.jspassport-jwt

Asymmetric keys with passport jwt. Verify always returns Unauthorized


Working on an app, and I want security from the start, so I've created a private/public key pair, and I'm setting up passport-jwt like this: (key is the public part of the keypair)

(passport, key) => {
  const opts = {
    jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    secretOrKey: key
  };
   passport.use(
     new JwtStrategy(opts, (payload, done) => {
       log.info({message: 'verifying the token', payload});
       User.findById(payload.id)
         .then(user => {
           if (user) {
             return done(null, {
               id: user._id,
               name: user.userName,
               email: user.emailAddress
             });
           }
           log.info(payload);
           return done(null, false);
         })
         .catch(err => {
           log.error(err)
           return done('Unauthorized', false, payload);
          });
     })
   );
};

and when the user logs in, I'm signing the token with the private key like this:

router.post('/login', (req, res) => {
        const email = req.body.email;
        const password = req.body.password;

        User.findOne({ email }).then(user => {
            if (!user) {
                errors.email = 'No Account Found';
                return res.status(404).json(errors);
            }

            bcrypt.compare(password, user.password).then(isMatch => {
                if (isMatch) {
                    const payload = {
                        id: user._id,
                        name: user.userName,
                        email: user.emailAddress
                    };
                    log.info(payload);
                    jwt.sign(payload, private, { expiresIn: 30000000 }, (err, token) => {
                        if (err)
                            res.status(500).json({ error: 'Error signing token', raw: err });
                        // const refresh = uuid.v4();
                        res.json({ success: true, token: `Bearer ${token}` });
                    });
                } else {
                    errors.password = 'Password is incorrect';
                    res.status(400).json(errors);
                }
            });
        });
    });

I think there might be something that I'm missing, but I'm unsure what it could be.

Also I've been generating the keys inside the app on initialization as well, using the following code.

const ensureKeys = () => {
    return new Promise((resolve, reject) => {
        ensureFolder('./keys').then(() => {
            /**
             * Ensure that both the private and public keys
             * are created, and if not create them both.
             * Never generate just a single key.
             */
            try {
                if (
                    !fs.existsSync('./keys/private.key') &&
                    !fs.existsSync('./keys/public.key')
                ) {
                    log.info('Keys do not exist. Creating them.');
                    diffHell.generateKeys('base64');
                    const public = diffHell.getPublicKey('base64');
                    const private = diffHell.getPrivateKey('base64');
                    fs.writeFileSync('./keys/public.key', public);
                    fs.writeFileSync('./keys/private.key', private);
                    log.info('keys created and being served to the app.');
                    resolve({ private, public });
                } else {
                    log.info('keys are already generated. Loading from key files.');
                    const public = fs.readFileSync('./keys/public.key');
                    const private = fs.readFileSync('./keys/private.key');
                    log.info('keys loaded from files. Serving to the rest of the app.');
                    resolve({ private, public });
                }
            } catch (e) {
                log.error('issue loading or generating keys. Sorry.', e);
                reject(e);
            }
        });
    });
};

Solution

  • Okay, so the issue was two-fold. First, I was generating the keys incorrectly for passport. According to the documentation for passport-jwt, documentation, keys must be encoded in PEM format, and according to this post on Medium, there needs to be some more configuration for passport and JWT.

    The final solution included the use of the keypair library which is available on npm.

    Here are the modifications used to make the working resulting code.

    const keypair = require('keypair');
    const ensureKeys = () => {
        return new Promise((resolve, reject) => {
            ensureFolder('./keys').then(() => {
                /**
                 * Ensure that both the private and public keys
                 * are created, and if not create them both.
                 * Never generate just a single key.
                 */
                try {
                    if (
                        !fs.existsSync('./keys/private.key') &&
                        !fs.existsSync('./keys/public.key')
                    ) {
                        log.info('Keys do not exist. Creating them.');
                        const pair = keypair();
                        fs.writeFileSync('./keys/public.key', pair.public);
                        fs.writeFileSync('./keys/private.key', pair.private);
                        log.info('keys created and being served to the app.');
                        resolve({ private: pair.private,public: pair.public });
                    } else {
                        log.info('keys are already generated. Loading from key files.');
                        const public = fs.readFileSync('./keys/public.key', 'utf8');
                        const private = fs.readFileSync('./keys/private.key', 'utf8');
                        log.info('keys loaded from files. Serving to the rest of the app.');
                        resolve({ private, public });
                    }
                } catch (e) {
                    log.error('issue loading or generating keys. Sorry.', e);
                    reject(e);
                }
            });
        });
    };
    

    Keys are signed with the private key, which is never to be shared.

        router.post('/login', (req, res) => {
            const { errors, isValid } = require('../validation/user').loginUser(
                req.body
            );
            if (!isValid) {
                return res.status(400).json(errors);
            }
            const email = req.body.email;
            const password = req.body.password;
    
            User.findOne({ email }).then(user => {
                if (!user) {
                    errors.email = 'No Account Found';
                    return res.status(404).json(errors);
                }
    
                bcrypt.compare(password, user.password).then(isMatch => {
                    if (isMatch) {
                        const payload = {
                            id: user._id,
                            name: user.userName,
                            email: user.emailAddress
                        };
                        log.info(payload);
                        jwt.sign(payload, private, { 
                            expiresIn: 30000000,
                            subject: user.emailAddress,
                            algorithm: 'RS256'
                         }, (err, token) => {
                            if (err)
                                res.status(500).json({ error: 'Error signing token', raw: err });
                            res.json({ success: true, token: `Bearer ${token}` });
                        });
                    } else {
                        errors.password = 'Password is incorrect';
                        res.status(400).json(errors);
                    }
                });
            });
    
    

    And the verification function:

      const opts = {
        jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('Bearer'),
        secretOrKey: key,
        algorithm: ["RS256"]
      };
    passport.use(
         new JwtStrategy(opts, (payload, done) => {
           log.info({message: 'verifying the token', payload});
           User.findById(payload.id)
             .then(user => {
               if (user) {
                 return done(null, {
                   id: user._id,
                   name: user.userName,
                   email: user.emailAddress
                 });
               }
               log.info(payload);
               return done(null, false);
             })
             .catch(err => {
               log.error(err)
               return done('Unauthorized', false, payload);
              });
         })
       );
    

    I hope this helps anyone looking to use asymmetric keys in the future.