Authenticating users to our application prevents the wrong people from gaining access to our service. Without strong authentication and proper implementation strategies, our service could be compromised. In this article, we would look at how to implement a custom authentication strategy in Fastify.
Fastify is a very fast, developer-friendly, and modular Node.js framework. Also, Fastify has a huge plugin ecosystem and it is fully extensible with decorators, plugins, and hooks. In this article, we would build our authentication strategy using the fastify-auth plugin.
The fastify-auth module is a Fastify plugin that provided a utility to handle authentication in routes without adding overhead. It gives us a way to compose multiple authentication strategies in Fastify. fastify-auth does not provide any authentication strategy consequently, we are required to provide our own via a decorator or another plugin.
Kindly consider the code below:
The code above demonstrates the usage of the fastify-auth plugin. Here Fastify instance is decorated with two authentication strategies namely verifyJWT and verifyUsernameAndPassword. These authentication methods are then used in the preHandler hook to protect the endpoint as seen below:
The relationship between these customized strategies is or; we could change it to and as seen below:
Also, in Fastify, routes are declared either as a plugin or within the .after() callback. In our implementation above we declared our routes in the .after() callback and we would use this convention as we build our custom authentication strategy. Let’s get started with the prerequisite in the next section.
To follow along in this article, here are a few prerequisites to note:
- Node.js and MongoDB installed.
- Basic knowledge of MongoDB and Mongoose
- Basic knowledge of Node.js
To get started, we need to set up our server. Follow the steps below to create the server:
- Create an npm project and install all the needed dependencies:
- Enable es6 modules:
Simply add the following code to your package.json file — if you have Node version 12 and above installed.
However, if you have Node version 8 or 10 you can enable this feature by using esm.
- Add our scripts:
Open the package.json file and edit the scripts section as follows:
- "start": "node --es-module-specifier-resolution=node src/index.js" is the production command to start our server it uses node.
- "dev": "nodemon --es-module-specifier-resolution=node ./src/index.js" uses nodemon to restart our server any time we compile our code.
The --es-module-specifier-resolution=node is required to aid interoperability between the es module — ESM and Node’s commonJS modules.
- Setup environment variables:
In the root directory create a .env file and add the following code:
- Setup our server:
In the root directory, create a src directory with an index.js file containing the following code:
Now we can start the server by running npm run dev the outputs:
Great our server is working. Let’s start implementing our custom authentication strategy in the next section.
Building Our Custom Authentication Strategy
In this section, we would start by building our models and connecting our app to MongoDB.
In the src directory create a config directory with an index.js file and a models, directory. Add the following codes to the index.js file.:
The code above connects our app to MongoDB using mongoose and logs the connection status to the console. Also, we decorate Fastify with our models to enhance code reuse. Also, inside the models directory, create a user.js file with the following codes:
In our code above, the userSchema.pre hook is a middleware function used to hash our password using the bcrypt algorithm before the users are saved. Since storing passwords in our database in plain text is a terrible idea; we can securely store our passwords as hashed values. Also, in our schema above, we encapsulate our database logic within the data model by creating mongoose methods and statics. Statics are methods that can be invoked directly by a model — User; while Methods are invoked on an instance of a mongoose document — user.
The generateToken Method generates a new token using the user ID and our JWT_SECRET while the findByToken Statics method decodes a user token and finds the user using the ID and token as parameters. Lastly, the findByCredentials Statics method validates the user credentials — username and password and returns the user. These methods would be used in our authentication strategies in the next section.
Building our authentication strategies
In this section we will create two authentication strategies namely:
- asyncVerifyJWT — authenticates a route based on the provided token
- asyncVerifyUsernameAndPassword — authenticates a route based on the provided credentials — username and password.
We would create four routes to demonstrates the usage of these strategies. In the src directory create a routes folder with a users.js file. Add the following codes to the users.js file:
In the code above we decorate Fasitify with our two authentication strategies. The asyncVerifyJWT strategy gets the user token from the header and calls the findByToken statics method on the User model. The user is then passed to the request object making it accessible in the handler function — where our requests are handled. The asyncVerifyUsernameAndPassword strategy gets the user’s username and password from the request.body and passes them as arguments to the findByCredentials statics method. The returned user is then passed to the request object. Also, the .after() hook would contain all our routes let’s create them in the next section.
Building our routes
In the .after() in the code above add the following codes below the
// our routes goes here comment:
In our code above we have four routes namely:
- Register route /register: it is unauthenticated. It registers a new user and generates a token by calling the generateToken method.
- Profile route /profile: it is authenticated using the asyncVerifyJWT.
- Login route /login: typically login routes should not be authenticated but the pattern used here validates the user credentials using the asyncVerifyUsernameAndPassword strategy as seen here.
- Logout route /logout: it is authenticated using the asyncVerifyJWT.
Register plugins and test our endpoints
To register our plugins we first import them into the index.js file in the src directory as seen below:
Then below add the following code below the // Activate plugins below: comment:
Next, we start our server by running mongod and npm run dev. This starts MongoDB and our application server.
Note in linux we may be required to run sudo mongod.
Now we can start testing our endpoint using curl and jq.
Fastify is a great framework and the developer experience is awesome. In this article, we have successfully built our custom authentication strategy however, It is good to mention that we can compose our strategies for a single route as seen below:
In the code above, fastify-auth will run all our authentication methods and continue the request if anyone succeeds. For a basic authentication strategy you can try out the fastify-basic-auth plugin and you can also get the full code for this app in the Github repository.