DEV Community

Mastering JS
Mastering JS

Posted on

How We Think About Securing Express.js APIs in 2024

Securing your Express.js API involves a lot of tradeoffs. Do you use cookies or authorization header, JWT or access token? Do build your own authentication or use something like Auth0? Do build your own authorization or use a service like Oso? In this blog post, I'll cover how Mastering JS currently thinks about security for the numerous Express APIs we maintain.

Cookies vs Authorization Header

This is admittedly somewhat controversial, but our Express APIs only support authentication via the authorization header. We don't support cookies at all. The primary reason is that header-based authentication is easier to work with, both for SPA web clients and mobile apps; and we don't want to support multiple authentication strategies.

Cookies, especially when combined with CORS, are quite tricky to get right. In our experience, it is much easier to just set an authorization header on an Axios request and be done with it.

JWT vs Access Token

We prefer randomly generated access tokens that we store in MongoDB using an AccessToken Mongoose model over JWTs. JWTs are faster, because you don't need a database round trip to validate a JWT.

But JWTs make it difficult to forcibly log out a user. Once a JWT is generated, the JWT is usable until its expiration date, unless you either delete the user, rotate your JWT key (which logs out everyone), or store a list of disabled JWTs in your database. This makes it tricky to implement a "log me out of all devices" feature, which is important in case one of your users loses their phone or laptop.

Authentication: Build vs Buy

Historically we have been big proponents of building your own authentication. We store users and authentication methods in our own database. Our primary concern with an external provider is vendor lock-in: authentication is fundamental to any app, and replacing an authentication provider is non-trivial.

However, recently we've been rethinking this stance, especially if we're able to still store our user data in our own database. Authentication gets extremely tricky: setting up keys for all the different OAuth providers (Apple, Google, etc.) is a tedious manual process, implementing 2FA with support for authenticator apps is non-trivial. There's just too much repetitive complexity to re-implement the same authentication logic in every single API, so we're considering switching to an authentication provider.

Authorization: Build vs Buy

Like authentication (prove to the server that Bob is who he says), historically we have preferred to implement our own authorization (is Bob allowed to do what he is trying to do?). For basic role-based access control, writing your own authorization is fairly straightforward.

However, like 2FA and OAuth, role-based access control in Node.js quickly becomes more complicated as business needs grow. Eventually the global "admin" role evolves into "a user has the 'admin' role on these resources", which evolves into "a user can perform an action on one resource if they have the 'admin' role on another resource."

When the third case starts popping up, that's when an authorization provider like Oso or Permit.io starts to sound appealing. Centralized, easily readable authorization logic combined with easy queries for "what users are allowed to perform this action?" sounds great when your authorization logic starts to get a bit too complex. And we're finding a few cases where our authorization logic is getting too hard to reason about, so we're also considering switching to an authorization provider.

Top comments (0)