Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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);
            }
        });
    });
};
like image 586
Chris Rutherford Avatar asked Mar 05 '19 21:03

Chris Rutherford


People also ask

What is difference between Passport and JWT?

JSON Web Token is an open standard that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed; Passport: Simple, unobtrusive authentication for Node. js.

Does Passport use JWT?

A Passport strategy for authenticating with a JSON Web Token. This module lets you authenticate endpoints using a JSON web token. It is intended to be used to secure RESTful endpoints without sessions.

What is JWT in NestJS?

JWT stands for JSON Web Tokens. Using JWT effectively can make our applications stateless from an authentication point of view. We will be using the NestJS JWT Authentication using Local Strategy as the base for this application.


2 Answers

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.

like image 108
Chris Rutherford Avatar answered Oct 04 '22 16:10

Chris Rutherford


Nice one, this helped me, thanks.

If it helps anyone else, I didn't have to use the library - found a link that explained how to convert the public key to PEM format, which seemed to work (private key is already in the correct format)

ssh-keygen -f id_rsa.pub -m 'PEM' -e > id_rsa.pem

My question

like image 34
NickW Avatar answered Oct 04 '22 15:10

NickW