Simple token based authorization in Node.js
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.