Leveraging Express Middleware to Authorize your API

Code Jun 24, 2018

Recently I've had to build a bunch of REST API's and I've been writing them mostly with Node.js and express. I ran into a scenario where I was building an internal admin panel and I wanted to ensure there was a proper authentication & authorization scheme to both

  1. Authenticate users who were allowed to access the panel
  2. Provide permissions and authorize certain API's only for select roles/users

Solving for (1) is fairly easy nowadays because with the power of libraries such as Passport or delegating authentication to a Backend-as-a-Service platforms like Firebase, getting that working is a quick task and a non-issue. However, the harder work comes with authorization. How do I ensure that only properly authenticated/authorized users could access the API's that I was writing? That is the focus for this post.

Implementing an authorization rule

Creating an authorization rule is actually not that difficult. Let's say you will only allow a user to modify his/her own data if he/she is the owner of that data. Assuming you follow standard security practices and provide an Authorization HTTP header for one of the following Authentication types, it's as simple as re-authenticating and validating that the user is who he says he is, checking whether the user has access to his own account, and then allowing him to update it.

As a simple example, I'm going to describe a use case where you are making a POST request to a url endpoint ending in /:userId/verify, where userId is the id of the user you want to make the update. Let's also say the user has already been authenticated on the browser and sends an Authorization header with the request: a JWT token using the Bearer token schema.

So your header might look like:

Authorization: Bearer <JWT token>

And your express code to handle this request might look like:

import { userUpdate } from '../services/users'
import { getBearerToken, verifyTokenAndGetUID } from '../utils/authentication'

...

router.post('/:userId/verify', (req, res, next) => {
  const authHeader = req.headers.authorization
  const { userId } = req.params
  const data = req.body
    
  if (!authHeader) {
    return res.status(403).json({
      status: 403,
      message: 'FORBIDDEN'
    })
  } else {
    const token = getBearerToken(authHeader)

    if (token) {
      // This promise returns the userId of this token
      // We will validate whether the current authenticated user has access to update the current userId. 
      return verifyTokenAndGetUID(token)
        .then((userId) => {
          if (req.auth && req.auth.userId && userId === req.auth.userId) {
            return userUpdate(userId, data)
          } else {
            res.status(401).json({
              status: 401,
              message: 'UNAUTHORIZED'
            })
          }
        })
        .catch((err) => {
          logger.logError(err)

          return res.status(401).json({
            status: 401,
            message: 'UNAUTHORIZED'
          })
        })
    } else {
      return res.status(403).json({
        status: 403,
        message: 'FORBIDDEN'
      })
    }
  }
})

In layman's terms, this code block is doing the following:

  1. If an authorization token is not provided, return a 403
  2. If a token is provided, make sure it's of the right Authorization scheme
  3. If not, return a 401. Otherwise, verify the token and retrieve the user id for this token.
  4. If it is not a valid/verifiable token, return 401. Otherwise check if the returned user id it is allowed to make the update against the provided user id in the HTTP request
  5. If not, return 401. Otherwise, continue with the request.

This works great for what we need this API to do. Only one problem - you don't want to have to write this over and over again for each REST API endpoint you create. There's a principle in software development called DRY (don't repeat yourself), so let's make it so that we don't have to redundantly write this same authorization rule everywhere.

Express middleware to the rescue

If you've done any sort of development in express, you may be aware of a lil something called Express Middleware. The TL;DR is that express middleware performs the following tasks:

  • Execute any code.
  • Make changes to the request and the response objects.
  • End the request-response cycle.
  • Call the next middleware function in the stack.

For any API endpoint that we write, we can bind an application-level middleware and have it run the authorization rule before deciding whether or not to proceed. Using the example from before, this is how we can rewrite it:

// Router 
import { userUpdate } from '../services/users'
import { isUserAuthenticated } = '../authMiddleware'

router.post('/:userId/verify', isUserAuthenticated, (req, res, next) => {
  const { userId } = req.params
  const data = req.body

  if (res.locals.auth && res.locals.auth.userId && userId === res.locals.auth.userId) {
    return userUpdate(userId, data)
  } else {
    res.status(401).json({
      status: 401,
      message: 'UNAUTHORIZED'
    })
  }
})
// Authorization Rules Middleware
// authMiddleware.js

export const isUserAuthenticated = (req, res, next) => {
  const authHeader = req.headers.authorization

  if (!authHeader) {
    return res.status(403).json({
      status: 403,
      message: 'FORBIDDEN'
    })
  } else {
    const token = getBearerToken(authHeader)

    if (token) {
      return verifyTokenAndGetUID(token)
        .then((userId) => {
        // ------------------------------------
        // HI I'M THE UPDATED CODE BLOCK, LOOK AT ME
        // ------------------------------------
          res.locals.auth = {
            userId
          }
          next()
        })
        .catch((err) => {
          logger.logError(err)

          return res.status(401).json({
            status: 401,
            message: 'UNAUTHORIZED'
          })
        })
    } else {
      return res.status(403).json({
        status: 403,
        message: 'FORBIDDEN'
      })
    }
  }
}

Notice that we put the middleware into its own module so that it can be reused in any other express endpoint as necessary. You'll also see that in our Express route initialization, that we add this middleware as part of the app.METHOD initilization, where it will execute the authorization rule before continuing. In our updated code block within the authorization rule, you'll see that we call next() to pass control to the next middleware function. Otherwise, the request will be left hanging.

One last thing to notice is that once the middleware authorization rule is done, we need to pass the data back onto the next middleware function (i.e to do the user id validation). So you'll see that we are passing the returned userId through res.locals, which stores any object scoped throughout the lifetime of this request. This is recommended through even the official express docs.

And that's it!

More and more middleware

Writing this middleware will enable us to write an authorization rule once, and then for every request to app.METHOD that needs this rule, we can run simply add the middleware to the request. The best part is that we can add as many as we want to the request by simply wrapping it in an array!

Here is an example of a series of REST API's that I have implemented that leverage similar authorization middleware:

/**
 * Gets all newsletters created
 */
router.get('/', [isUserAuthenticated], (req, res, next) => {
  res.api(getNewsletterHistory())
})

/**
 * Schedules a newsletter to be sent
 */
router.post('/', [isUserAuthenticated, isAdminUser], (req, res, next) => {
  const data = req.body

  res.api(scheduleAndSendWeeklyNewsletter(data))
})

/**
 * Schedules a test newsletter to be sent to the specified email
 */
router.post('/test', [isUserAuthenticated, isAdminUser], (req, res, next) => {
  const data = req.body
  const { email, content } = data

  res.api(sendTestNewsletter(email, content))
})

/**
 * Gets all scheduled newsletters
 */
router.get('/scheduled', [isUserAuthenticated], (req, res, next) => {
  res.api(getScheduledNewsletters())
})

/**
 * Deletes a scheduled newsletter
 */
router.delete('/scheduled', [isUserAuthenticated, isAdminUser], (req, res, next) => {
  const data = req.query

  res.api(deleteScheduledNewsletter(data))
})

export default router

You'll see that some of the API's have different rules. "READS" only requires the user to be authenticated, but making any "WRITES" requiers to the user to be an admin user. (If you look closely, you'll also notice that I wrote a custom middleware to standardize all API responses as well, but more on that in a future post).

Got any other suggestions?

In a year of security breaches and GDPR, data privacy has become super important. So if you're an Express pro, have other ideas, or if my solution  is trash and you got a better one, please share it!

Tags