Securing an Express/MongoDB/Mongoose API

Rick Glascock
4 min readNov 28, 2020

Last week I outlined route planning for an Node.js/Express/MongoDB API. This week I will outline one approach to securing the API that includes hashing user passwords and creating tokens to authenticate API requests from the client. Another solution to this problem would be to use Google’s FireBase authentication service, that takes care of sign in, and session token creation and verification out of the box. While this turnkey approach is tempting, I think building security from the ground up is important to understanding the fundamentals for yourself.

Yunnan, China — Rick Glascock

I install two packages to handle security:

BCrypt is used to create and verify hashed password. Hashed passwords cannot be used if your database is hacked and the hashed passwords exposed, but can be used to verify submitted passwords of users.

JWT is used to create and verify tokens. The tokens themselves have three parts separated by ‘.’ The first part contains configuration options. The second part contains the actual payload (in our case, the user’s ID) and the third contains your encrypted signature. Copy the token below and paste into the jwt.io debugger and you can see the User ID displayed:

token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1ZmJiZThmN2MyMDczZTYwMjllOTE5YTAiLCJpYXQiOjE2MDYzMTMyNDN9.4xu_4bWNVGHbtxJAwB5xhdOaSrvUnQZFNaP0HDyWT5ouserId:
"userId": "5fbbe8f7c2073e6029e919a0"

So what’s the point if you can access the payload without the secret? You should also see “Invalid Secret” displayed. This is how the token functions. The payload is not intended to hold sensitive information, but the token will fail verification if the correct signature is not included.

With these two tools installed we set up routes. Each basic route uses the security feature in different ways:

Security operations

We see from this chart that tokens are created and sent when 1) a new user is created (their password will also be hashed and stored), and 2) when an existing user signs in (password verified). The token is created and sent to the client where it is stored in local storage (or session storage, or cookie storage). Placing the token in local storage will allow the token to persist between browser sessions so a user doesn’t need to login every time they return to the site. When the app is visited and a token exists, it will be sent (in the Header of the request, to the backend which, if verified, will allow the server to send the user’s specific info from the database to populate the browser — a get profile or auto login endpoint.

The stored tokens are also used to verify that a user is authorized to make requests to specific endpoints thereby securing read and/or write privileges to sensitive data.

The security functions separate from the routers and then called from the routers themselves as requests are made to the API, depending on the type of request.

Create and store a hashed password using a Mongoose Schema.pre method in User Model file. This will be called when creating a new user or if a user changes their password. The function calls the Bcrypt.hash() method that takes the password and the number of salt rounds as arguments. Salting a password adds random characters into the password making it more difficult to guess the hash. The salted characters are embedded with the hash and don’t need to be stored separately. The higher the number of rounds, the more secure, but with a trade off that the verification takes longer.

// src/models/user.jsuserSchema.pre('save', async function() {
const user = this; // So we can refer to user instead of this
if (user.isModified('password'))
user.password = bcrypt.hash(user.password, 8);
}
next()
});

Create a JWT based on User ID. This will happen in a Mongoose Schema.methods method (instance method)that can be called by router. The function calls jwt.sign() with the user’s ID and your secret signature as arguments.

// src/models/user.jsUserSchema.methods.generateAuthToken = async function () {
const user = this;
const JWTSecret = 'secret';
const token = await jwt.sign({ userId: user._id.toString() }, JWTSecret);
user.tokens = user.tokens.concat({ token });
await user.save();
return token;
};

Verify email, password and get user. This will happen using a Mongoose Schema.statics method (class method) that can be called by router. The function calls bcrypt.compare() that accepts the stored hash and the submitted password. The password is hashed and the two hashes are compared.

// src/models/user.jsUserSchema.statics.findByCredentials = async (email, password) => {
const user = await User.findOne({ email });
if (!user) {
throw new Error('Unable to login.');
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
throw new Error('Unable to login.');
}
return user;
};

Authorize the sent token before running the route handler. This is Express Middleware that can called on all routes that need to be authorized. The function calls jwt.verify() and accepts two arguments, the submitted token and the signature secret. As a bonus, once the token and user are verified the user is returned to the router avoiding having to do a second query. Essentially returning the current user with every successful call to authenticate.

// src/middleware/authentication.jsconst auth = async (req, res, next) => {
try {
const sentToken = req.headers.authorization.replace('Bearer ', '');
const JWTSecret = 'secret';
const decoded = jwt.verify(sentToken, JWTSecret);
// The following checks to see if user exists AND if that user has the correct token. const user = await User.findOne({ _id: decoded.userId, 'tokens.token': sentToken }); if (!user) {
throw new Error();
}
req.user = user;
next();
} catch (error) {
res.status(400).send({ error: 'Please login.' });
}
};

This setup provides a good basic security setup for your Express App. One more important detail. Make sure you do not expose your JWT secret in your client’s code as it creates a huge security risk. Rather the secret should be hidden within an environment variable.

--

--

Rick Glascock

After years of teaching music in Austin, Shanghai and Yangon, I’m making a career change to my other passion, software development.