Hi there! Today's article is a continuation of my previous article on Fastify.
We will improve on our CRUD API in this article. You should check out the previous article else I don't think you'll be able to follow along.
Or you can just clone this repo, and follow along.
What are we introducing? Today's article is going to focus on authentication. How would we easily protect routes in Fastify? Fastify's documentation is very detailed, I recommend you go through the docs after reading this article.
Quickly let's go into our app and get things started. If you cloned the API's repo, you wanna make sure you run npm install
to install all dependencies and then test all endpoints in the test.http file to ensure it's working.
In the previous article, I didn't talk about installing fastify-auth. But in the API's repo, fastify-auth is a dependency along with several others like jsonwebtoken, fastify-swagger (which we will get to in a second). So if you haven't installed the above dependencies you should do so now because we will be using them in this article.
Let's get started
Creating and Registering the Admins Routes
The first thing we want to do before protecting routes is to have a form of registering and logging in admins to the app (when connected to the front-end of course). We will have a route for registering admins, and for logging them in.
We are not connecting this API to a database, so like we did with the posts
array in cloud/posts.js, that is how we would do the same for the admins.
We will have a simple array of admins in a file, have it exported and used whenever and however we want. The array can be empty initially or you can add placeholders. I will leave mine empty.
When a user creates an account, his/her details are appended to the array. As soon as he/she logs in a token will be generated for him/her. It is with this token he/she can access protected routes like that of deleting posts.
Simple right!
Alright then, let's start by creating our database array. In the cloud folder, create a file called admins.js, and add the following
const admins = [
{
id: 1,
username: 'johndoe_360',
email: 'johndoe@gmail.com_',
password: '341',
},
{
id: 2,
username: 'sarahjohnson',
email: 'sarah@websitename.com',
password: 'sarahCodes',
},
];
module.exports = admins;
In the routes folder, create a file called admins.js and create a function called adminRoute
. This function is our route plugin that we will register in server.js in a minute. This function usually takes three parameters i.e fastify, options, and done. Find more explanation on this in the previous article or in Fastify's docs.
We will create all of our admins' routes in this function. And just before the closure of the function, we wanna make sure we call done()
to signify that we are done.
const adminRoutes = (fastify, options, done) => {
// all our routes will appear here
done();
};
module.exports = adminRoutes;
Before we start creating routes, let's register this adminRoutes
in server.js. Just after the fastify.register(require('./routes/posts'));
add this
fastify.register(require('./routes/admins'));
That should register your route and get you going, you can test it's working with a simple route e.g
fastify.get('/test', (req, reply) => {
reply.send('Hello world');
});
Add the code above inside the adminRoutes
function in routes/admins.js and test your enpoint.
Get all Admins
This may not be useful in a real-time app, but just in case you want to get all admins, we can use the GET
method to do that.
Create the Route in routes/admins.js
In place of our test route we made in adminRoutes
function, we should add this
fastify.get('/api/admins', getAdminsOpts);
Now let's create the getAdminsOpts
object. This object as always should go outside of the adminRoutes
function. Create and add the following
const getAdminsOpts = {
schema: getAdminsSchema,
handler: getAdminsHandler,
};
Create the Schema in schemas/admins.js
This file has not been created yet, so we will create it now. In controllers/schemas folder, create a file called a admins.js. In this file create an object called getAdminsOpts
.
With this schema, we want to filter out what to send to the client from our array of admins. For example, you wouldn't want to send the password of each admin to the client. So this is the easy way to do it
const getAdminsSchema = {
response: {
200: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'number' },
username: typeString, // typeString will be created soon
email: typeString,
},
},
},
},
};
At the beginning of this file, create a variable called typeString
and assign { type: 'string' }
to it.
Now let's export getAdminsSchema
out of the schemas/admins.js file.
module.exports = { getAdminsSchema };
Create the Handler in handlers/admins.js
This file has not been created yet, so let's do that now. In controllers/handlers folder, create a file called admins.js. This file will have all the handler functions of our admin routes.
Let's create our first handler, which will return all the admins we have, then we will export it.
const admins = require('../../cloud/admins'); // import the admins array
const getAdminsHandler = (req, reply) => {
reply.send(admins);
};
module.exports = { getAdminsHandler };
Import getAdminsHandler
and getAdminsSchema
into your routes/admins.js as objects.
Now save your files and test your new route.
There is a route I am going to skip, that is getting an admin, if this API was for production, I definitely would have made it. But it's not so we wouldn't need it.
Register an Admin
Let's create accounts for our new admins. So far I believe you have grasped a lot about creating routes with Fastify, so I am going to speed up the process a little bit.
I will just show you what your schema should look like and what your handler should do.
Schema
const registerAdminSchema = {
body: {
type: 'object',
required: ['username', 'email', 'password'],
properties: {
username: typeString,
email: typeString,
password: typeString,
},
},
response: {
200: typeString,
},
};
Handler
const registerAdminHandler = (req, reply) => {
const { username, email, password } = req.body;
const id = admins.length + 1;
admins.push({
id,
username,
email,
password, // you can hash the password if you want
});
reply.send('Account created successfully');
};
Log in Admin
When we log in an admin, we would send a token to the client. This token will be generated using JsonWebToken (JWT). It is only with this access token the user can access protected (private) routes.
There are currently no protected routes, but we would make some posts' routes private in a moment.
First, let's see what the schema for this route should look like and how the handler should function
Schema
const loginAdminSchema = {
body: {
type: 'object',
required: ['username', 'password'],
properties: {
username: typeString,
password: typeString,
},
},
response: {
200: {
type: 'object',
properties: {
token: typeString,
},
},
},
};
Handler
// at the beginning of the file, import jwt and assign to a variable
const jwt = require('jsonwebtoken');
const loginAdminHandler = (req, reply) => {
const { username, password } = req.body;
const admin = admins.filter((admin) => {
return admin.username === username;
})[0];
if (!admin) {
return reply.send("This admin doesn't exist");
}
// check if password is correct
if (password !== admin.password) {
return reply.send('Invalid credentials');
}
// sign a token
jwt.sign(
{ id: admin.id },
'my_jwt_secret',
{ expiresIn: 3 * 86400 },
(err, token) => {
if (err) reply.status(500).send(new Error(err));
reply.send({ token });
}
);
};
The first thing we did was bring in jwt, you should install it first if you haven't. Use npm i jsonwebtoken
to install it.
Then we did some verification to make sure the user exists and the given password is the correct password.
Lastly, we signed a token that will expire in three days with a payload of just the admin's id, you can add username and scope if you want.
If we had used it with a database and then had to get the data asynchronously using async/await
we would have run into some Fastify errors. This is what I mean
const loginAdminHandler = async (req, reply) => {
const { username, password } = req.body;
try {
const admin = await Admins.findOne({ username }); // assumming we used mongodb
if (!admin) {
return reply.send("This admin doesn't exist");
}
// check if password is correct
if (password !== admin.password) {
return reply.send('Invalid credentials');
}
// sign a token
jwt.sign(
{ id: admin.id },
'my_jwt_secret',
{ expiresIn: 3 * 86400 },
(err, token) => {
if (err) throw err;
reply.send({ token });
}
);
} catch (err) {
console.log(err);
reply.status(500).send('Server error');
}
};
You should note that the token we are signing is being done asynchronously. So that means our reply.send
is inside an async function that is inside another async function. This can confuse Fastify to give you an error like this:
Solving this is simple. We just need to tell Fastify to wait for a reply in an async function. We usually do this in the root async function i.e down the try block add await reply
. That will solve the problem.
So you would have something like this
const loginAdminHandler = async (req, reply) => {
const { username, password } = req.body;
try {
const admin = await Admins.findOne({ username }); // assumming we used mongodb
if (!admin) {
return reply.send("This admin doesn't exist");
}
// check if password is correct
if (password !== admin.password) {
return reply.send('Invalid credentials');
}
// sign a token
jwt.sign(
{ id: admin.id },
'my_jwt_secret',
{ expiresIn: 3 * 86400 },
(err, token) => {
if (err) throw err;
reply.send({ token });
}
);
await reply;
} catch (err) {
console.log(err);
reply.status(500).send('Server error');
}
};
Making Private Routes
This is the simple part. Some of our routes are going to be restricted to a specific type of users. These routes are from the last article I made. Go check it out.
Private Routes
/api/posts/new : adding a post
/api/posts/edit/:id : updating a post
/api/posts/:id : deleting a post
These are the routes we will make private.
What makes a route private is the authentication that restricts unauthorized users from gaining access. If access is granted, these users can perform any action within the routes with the same authentication.
This means that we would need to authenticate the user for each of our private routes and this authentication needs to be done before any action can be performed.
With Fastify this is easy, Fastify has a plugin for easier authentication, and this authentication will be done in the preHandler function (in our routes opts).
With the fastify-auth plugin, we will tell fastify that whoever doesn't have a token should be rejected.
To do that first thing we'd do is register the fastify-auth
plugin using any Fastify instance.
// in routes/posts.js, at the bottom of the postRoutes function add this
fastify
.register(require('fastify-auth'))
.after(() => privatePostRoutes(fastify)); // we will create the privatePostRoutes later
The next thing is to create an authentication function. This function is what Fastify will use to validate (authenticate) the user. In our app, we need the user to have a valid token. This token would come from the request header
// create this function in an auth folder in controllers and export it
const verifyToken = (req, reply, done) => {
const { token } = req.headers;
jwt.verify(token, 'my_jwt_secret', (err, decoded) => {
if (err) {
done(new Error('Unauthorized'));
}
req.user = {
id: decoded.id, // pass in the user's info
};
});
done();
};
Because we passed an error to done, Fastify will not give that user any access.
You would notice we didn't check if there is a token before verifying the token. This is because in our route's schema we restrict whoever doesn't have a token as part of the request.
Now let's apply the verifyToken
function to a route. You should do this in the preHandler
property in routes opts. This is the function Fastify will run first before running the handler
.
Import the verifyToken
into our routes/posts.js file. Create a function outside of the postRoutes
function called privatePostRoutes
, pass fastify as the only parameter. Now cut and paste all our private routes from postRoutes
to privatePostRoutes
. You should have something like this:
const postRoutes = (fastify, opts, done) => {
// get all posts
fastify.get('/api/posts', getPostsOpts);
// get a post
fastify.get('/api/posts/:id', getPostOpts);
fastify
.register(require('fastify-auth'))
.after(() => privatePostRoutes(fastify));
done();
};
const privatePostRoutes = (fastify) => {
// create a new post
fastify.post('/api/posts/new', addPostOpts);
// update a post
fastify.put('/api/posts/edit/:id', updatePostOpts);
// delete a post
fastify.delete('/api/posts/:id', deletePostOpts);
};
Finally, let's add the preHandlers to our private route opts. Each of our preHandlers will contain a function from fastify-auth
that checks if the user is authenticated using our verifyToken
function.
const privatePostRoutes = (fastify) => {
// create a new post
fastify.post('/api/posts/new', {
preHandler: fastify.auth([verifyToken]),
...addPostOpts,
});
// update a post
fastify.put('/api/posts/edit/:id', {
preHandler: fastify.auth([verifyToken]),
...updatePostOpts,
});
// delete a post
fastify.delete('/api/posts/:id', {
preHandler: fastify.auth([verifyToken]),
...deletePostOpts,
});
};
When a user is unauthorized, Fastify will return a 401 error with our customized message. If you are going to use the same auth function for more than one route, instead of importing it into each of the routes files, you can make the auth function available to every file in the API using fastify.decorate
. In server.js
import verifyToken
and add this before your routes registering
fastify.decorate('verifyToken', verifyToken); // the string can be any name you like
Now we can remove the verifyToken
function we created in routes/posts.js and add this to our preHandlers
const privatePostRoutes = (fastify) => {
// create a new post
fastify.post('/api/posts/new', {
preHandler: fastify.auth([fastify.verifyToken]),
...addPostOpts,
});
// same thing goes for the other routes
};
Something we should add is the headers schema that will return an error whenever there is no token provided as part of the request's header.
In schemas/posts.js, create an object called headerSchema
. Pass in the following
const headerSchema = {
type: 'object',
required: ['token'],
properties: {
token: typeString,
},
};
For every of our private route's schema, add the headerScheema
object like this
const addPostSchema = {
headers: headerSchema,
body: {
type: 'object',
required: ['title', 'body'],
properties: {
title: typeString,
body: typeString,
},
},
response: {
200: typeString, // sending a simple message as string
},
};
Your API is ready to go. Finally, let's talk about fastify-swagger. Fastify-swagger basically gives us documentation of our API, the endpoints, methods, and we can also test our endpoints with it.
We will register it as a plugin and then we are good to go. In our server.js file, add this
fastify.register(require('fastify-swagger'), {
exposeRoute: true,
routePrefix: '/docs',
swagger: {
info: { title: 'Fastify-api' },
},
});
Visit http://localhost:your-port/docs
to see the docs.
Conclusion
Great job if you finished this project. Now you should start building more with Fastify. Source of our Fastify CRUD API
Thank you for reading. See you next time. Don't forget to buy me a coffee.
Top comments (1)
const admin = admins.filter((admin) => {
return admin.username === username;
})[0];
vvv
const admin = admins.find((admin) => admin.username === username);