Mar 01 2020
Authentication with Higher Order Functions
I have a small Express.js side project that I occasionally pick up. I just
got to the point where I need to enforce authentication over several
routes. I implemented a simple higher order function (HOF) that loads an
authenticated user into the req
object.
When authentication fails, I throw an exception and fallback to my error handler. Introducing the HOF eliminates the need to copy-paste in the controller and thus keeps the code DRY.
I did not download a dependency. While I could use something like Passport, this would not become beneficial until several authentication strategies are needed. Even so, installing additional dependencies are carefully downloaded.
Before HOF:
const get = async (req, res) => {
const [aErr, decoded] = await authenticate(
_.get(req, 'headers.authorization', null)
)
if (aErr.length > 0) {
throw new Exception(aErr)
}
const _instanceId = _.get(decoded, 'payload.id', null)
// Do something with the verified Authorization HTTP header
// In our case, we are grabbing the `id` out of our JWT payload
}
After HOF:
const withAuthentication = (cb) => async (req, res) => {
const [aErr, decoded] = await authenticate(
_.get(req, 'headers.authorization', null)
)
if (aErr.length > 0) {
throw new Exception(aErr)
} else {
_.set(req, 'meta.token', decoded)
cb(req, res)
}
}
const get = withAuthentication(async (req, res) => {
const _instanceId = _.get(req, 'meta.token.payload.id', null)
// Do something with the verified Authorization HTTP header
// In our case, we are grabbing the `id` out of our JWT payload
})
This also could have been implemented as middleware and registered on a per-route basis. Instead I chose the HOF for its explicit nature. Since the routing is declared in a separate file, using the middleware approach seemed to be more error-prone.
For controllers, I believe in “the thinner the better”. The reason is that you get performance gains when logic is moved out into its own isolated function. Typically a controller test will take between 15-30x longer to execute than your typical isolated function. Most of the spent time is in setting up and tearing down application instances.
Just imagine: for one controller test, you could have “paid” for 15-30 isolated function tests. Plus, isolated code naturally leads to better design. Which would you choose?