Simple token based authorization in Node.js

Node.js developers will sooner or later meet the problem of creating some sort of authentication and authorization logic. Having tokens in the header is one way to ensure that the user has the rights to access the private content. In this post, I'll create a simple application that uses a basic token.

Everyone who did internet shopping at least once or has an email address knows that they will need to provide (in most cases at least) a username and a password to log in to the relevant application. This is how we can access our private content that no one else is supposed to do.

If we try to click on the private content URL without logging in to the application (doesn’t matter if it’s email or something else), it’s highly likely that we will receive an error message.

1. Authentication and authorization

So somehow the application should know that we have registered, subscribed, paid etc. and are entitled to access the content.

The first step for the application to ensure that the user is who they tell themselves to be is to authenticate the user.

Authentication can happen in multiple ways. The creator of the application can apply a simple username/password authentication (in this post), can use third party authentication providers or make use of well-known services like Google and implement a login logic using the user’s Gmail username and password (not in this post).

From now on, I’ll talk about authentication with username and password

Once the application confirmed who the user is by checking if the username and password match the records in the database, it can let the user access the private content.

2. Tokens

But the private pages also need to know that the user who is accessing the content of that page is authorized, i.e. they are allowed to access the content. To inform the browser that the user is authorized to view the page, the server places a string in the request header upon successful authentication (that is after the server has checked that the username and password match).

Again, this can happen in different ways. The subject if this post will be a simple token based authorization.

A token is as string which the user can use to access the restricted content instead of typing their username and password each time. The server ensures that the username and password are valid, and it will provide the user with a token, which identifies the user. It’s like showing the ticket everywhere inside once we paid for the party.

3. Libraries

Obviously, you’ll need Node.js (or Docker) installed on your computer to continue with the setup.

Note: In real life, some sort of database should be used to store user information (username and password). Because the focus of this post is the login process and placing the token in the header, I’ll use a separate file as database. So instead of fetching the credentials from a real database, I’ll get them from that file.

Here’s the list of the libraries I use in this mini-project.

First comes bcrypt, which encodes the user’s password. Bcrypt uses Node.js’s crypto module under the hood to hash the password, and it also uses salt to make the password harder to break. Bcrypt is a secure library, and it’s often used in projects.

I’ll also use body-parser, which converts the incoming body to json format, This way the logic will be easy to test using cURL or Postman. The request body will contain the username and password.

Finally, I’ll make use of Express, which is an extremely popular web framework for creating Node.js servers and working with them. If the reader wants to know more about Express, I wrote an introduction series on the most important aspects of the framework a few months ago: Part 1: Introduction to Express.js: Setting up Express.js and basic routing, Part 2: Introduction to Express.js: Status codes and Part 3: Introduction to Express.js: Middleware. Have a look if you have time.

So in the project folder, let’s do an npm init -y, and then an npm i bcryptjs body-parser express to install the dependencies.

4. Code

The full code with test cases is available from my Github repository.

4.1. Database mock

Let’s start with the file which imitates the database. I call this file users.js, and it looks like this:

// users.js

const bcrypt = require('bcryptjs')

const users = [
  {
    username: 'John',
    password: bcrypt.hashSync('password', 10)
  }, {
    username: 'Jill',
    password: bcrypt.hashSync('123qwe', 10)
  }
]

module.exports = {
  users
}

Here we have two users, John and Jill, and their passwords are the super secure, unique and rarely used password and 123qwe, respectively. The passwords are hashed using the hashSync method of bcrypt.

4.2. Custom error

This step is optional. We can throw a normal Error when the user tries to access content they shouldn’t, but sometimes it’s a good idea to give a more customized feedback.

In the error.js file, we can create a custom error object which inherits from Error, and indicates what happens:

// error.js

class AuthorizationError extends Error {
  constructor(message) {
    super(message)
    this.name = 'AuthorizationError'
    this.httpStatusCode = 401
  }
}

module.exports = AuthorizationError

This is really not the style I like writing code, but I’ll take what I get.

The new AuthorizatonError class extends the Error class, and inherits its message property. Two new properties will be created in the object instance, name and httpStatusCode with values of AuthorizationError and 401, respectively.

4.3. The authorization middleware

When the user hits a private endpoint, we’ll check if they are authorized, i.e. their request contains the token.

This function will be middleware, and will be called before the endpoint logic is performed.

// authorization.js

const AuthorizationError = require('./errors')

const authorize = (req, res, next) => {
  if (!(req.headers && req.headers.authorization)) {
    throw new AuthorizationError('Not authorized')
  }
  next()
}

module.exports = {
  authorize
}

The middleware is called authorize, and it checks if the header has the authorization property (its value is the token), which will be created when the user logs in.

If not, we throw the AuthorizationError that was created in the last step with the custom message of Not authorizes. If everything goes well, and the authorization header has token, the middleware won’t do anything, and will call next().

Calling next() is important, otherwise the server process flow won’t continue, and it will be stopped at this point.

5. The server

Finally, the server itself, in the file called server.js:

// server.js

const express = require('express')
const bcrypt = require('bcryptjs')
const bodyParser = require('body-parser')
const http = require('http')

const { authorize } = require('./authorization')
const { users } = require('./users')

const app = express()
const PORT = 3005

app.use(bodyParser.json())

app.get('/', (req, res, next) => {
  res.send({ message: 'The endpoint has been hit' })
})

app.post('/login', async (req, res, next) => {
  const { username, password } = req.body

  if (!(username && password)) {
    return next(new Error('Enter username or password'))
  }

  if (!users.find(({ username: user }) => username === user)) {
    return next(new Error('Incorrect username/password'))
  }

  const hashForUser = users.find(({ username: user }) => user === username).password
  const isMatchingPassword = await bcrypt.compare(password, hashForUser)

  if (!isMatchingPassword) {
    return next(new Error("Incorrect username/password"))
  }

  req.headers.authorization = `Basic ${ username }:${ hashForUser }`
  res.redirect('/private')
})

app.get('/private', authorize, (req, res, next) => {
  res.send({ message: 'The private endpoint is only allowed for authenticated users' })
})

// error handling
app.use((err, req, res, next) => {
  res.status(err.httpStatusCode || 400).json({ message: err.message })
})

const server = http.createServer(app)

const boot = (port) => {
  server.listen(port, () => {
    console.log(`Server is listening on ${ port }`)
  })
}

if (require.main === module) {
  boot(PORT)
} else {
  module.exports = server
}

First, we import the libraries and the custom modules we created above at the top of the file. We also defined the server (app) and the port, and make use of the body-parser library, and convert the request body to json.

5.1. The /login endpoint

The first interesting block is the /login endpoint. As its name implies, the user hits this endpoint to log in to the application, i.e. they provide the application with their username and password.

The first if condition is to check if the user has entered a username and a password. If not, an Error is thrown.

Then we check if the user has entered a valid username, which can be either John or Jill. If the username doesn’t match either of them, we’ll again throw an error.

If the username is correct, we can move on to check if the password is valid. Here we can use the compare method of bcrypt.

The hashed password, which is stored in the database, cannot be unhashed. But, when the same expression is hashed over and over again, it will produce the same output. The compare method simply takes the password the user enters, repeats the hashing process again, and compares it to the saved hashed password. If the two values match, it’ll return a Promise, which resolves to true.

Note that the route handler is an async function, this way we can await the promise, and save its value to a variable called isMatchingPassword.

If isMatchingPassword is false, it means that the user has entered an incorrect password, and we throw an exception again.

In this case (and for the username, too) we don’t tell the user if the username or the password was wrong to make the potential hackers’ life somewhat harder.

If the user types in the correct username and password, we can add the token as the value of the authorization header, and redirect them to the landing page of the private content (/private). The token is a string, and it has the Basic John:<HASHED_PASSWORD> format.

5.2. Other methods and exports

The /private endpoint doesn’t have any content except a message. Here we call the authorize middleware to check if the user has the token (see 4.3 above).

All errors thrown will be passed to the default error handler middleware. It has an extra err argument compared to other middleware, and this will contain the Error and AuthorizationError objects that have been thrown. If the error doesn’t have a status code defined, it’ll assign a 400 to the response, and the error message is displayed in a json format.

At the end of the file, we can make a difference between calling the server from server.js and using it in a test environment.

In the first case, we’ll call the boot function, which calls the listen method with the port. This time the main property of the require object will me equal to module.

When the server is called outside server.js, e.g. in a test file, we’ll simply export it. This way we can avoid getting EADDRINUSE errors when the tests run.

6. Conclusion

Token based authorization is one way for the application to ensure that only users with the right credentials can access the content.

The code uses custom middleware and username/password credentials.

This is a very simple application, and other forms of authentication/authorization as well as the use of a database should also be considered.

Thanks for reading and see you next time.