DEV Community

Cover image for 🔐 How to create an authentication system with JWT in a Node.js API
Micael Miranda Inácio
Micael Miranda Inácio

Posted on

2

🔐 How to create an authentication system with JWT in a Node.js API

A crucial part of almost every system is a secure authentication mechanism. In this post, we'll implement authentication in a Node.js API built with Fastify. The creation of the API and the first routes related to the user table have already been covered in my previous posts — Check it out here. The base code is available in this GitHub repository: Blog - by micaelmi.

Creating a Login Route

To start the authentication process, we need to create a login route where the user will obtain an access token to use in protected routes. Inside src/routes/users, create login.ts:

import bcrypt from "bcrypt";
import type { FastifyInstance } from "fastify";
import { ZodTypeProvider } from "fastify-type-provider-zod";
import jwt from "jsonwebtoken";
import z from "zod";
import { env } from "../../env";
import { ClientError } from "../../errors/client-error";
import { prisma } from "../../lib/prisma";

export async function login(app: FastifyInstance) {
  app.withTypeProvider<ZodTypeProvider>().post(
    "/users/login",
    {
      schema: {
        summary: "User Login",
        tags: ["users"],
        body: z.object({
          credential: z.string().min(4), // username or email
          password: z.string().min(8).max(32),
        }),
      },
    },
    async (request, reply) => {
      const { credential, password } = request.body;

      const user = await prisma.user.findFirst({
        where: {
          OR: [{ username: credential }, { email: credential }],
        },
      });

      if (!user) throw new ClientError("User does not exist");

      const passwordMatch = await bcrypt.compare(password, user.password);

      if (!passwordMatch) throw new ClientError("Password does not match");

      const secretJwtKey = env.JWT_SECRET_KEY;
      const expirationTime = "30d";

      const token = jwt.sign(
        {
          sub: user.id,
          name: user.name,
          username: user.username,
          type: user.userTypeId,
        },
        secretJwtKey,
        { expiresIn: expirationTime }
      );

      return reply.send({ token });
    }
  );
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. Search for a user in the database using the provided username or email.
  2. Compare the provided password with the stored hashed password using bcrypt.
  3. Create a JWT token containing relevant user data from the database.
  4. Send the generated token back to the user.

Now, register the route in server.ts:

app.register(login);
Enter fullscreen mode Exit fullscreen mode

Test it in Swagger UI. The expected response should look like this:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Enter fullscreen mode Exit fullscreen mode

You can inspect the token's payload on the JWT Website.

Creating a Middleware to Verify Token Validity

Now we need to create a middleware — a function that runs between the request and response cycle, processing requests before they reach the final handler. This middleware ensures that only authenticated users can access protected routes.


import { FastifyRequest, FastifyReply } from "fastify";
import jwt, { JwtPayload } from "jsonwebtoken";
import { env } from "../env";

const secretKey = env.JWT_SECRET_KEY;

export const verifyToken = async (
  request: FastifyRequest,
  reply: FastifyReply
) => {
  try {
    const authorizationHeader = request.headers.authorization;
    if (!authorizationHeader) {
      return reply.status(401).send({ error: "Token not provided" });
    }

    const token = authorizationHeader.split(" ")[1];
    const decoded = jwt.verify(token, secretKey);

    if (typeof decoded === "string") {
      return reply.status(401).send({ error: "Invalid token" });
    }

    request.user = decoded as JwtPayload & {
      sub: string;
      name: string;
      username: string;
      type: string;
      iat: number;
      exp: number;
    };
  } catch (err: any) {
    console.error("Error on token validation:", err.message);
    return reply.status(401).send({ error: "Invalid token" });
  }
};
Enter fullscreen mode Exit fullscreen mode

This function:

  • Ensures a token is provided.
  • Verifies the token's validity.
  • Stores the decoded user data in request.user for later use.

Applying the Middleware and Testing Route Protection

To enforce authentication, add a preHandler hook in server.ts:

app.register(async (app) => {
  app.addHook("preHandler", verifyToken);
  // Protected routes go here
});
Enter fullscreen mode Exit fullscreen mode

Now, test a protected route without a token. The response should be:

{
  "error": "Token not provided"
}
Enter fullscreen mode Exit fullscreen mode

To enable token authentication in Swagger UI, update fastifySwagger configuration, adding the following code right after the info object:

//...
info: {
  title: "Blog API",
  description: "API for my blog project.",
  version: "1.0.0",
}, // ⬇️⬇️⬇️
securityDefinitions: {
  BearerAuth: {
    type: "apiKey",
    name: "Authorization",
    in: "header",
    description: "Enter your JWT token in the format: Bearer <token>",
  },
},
security: [{ BearerAuth: [] }],
// ...
Enter fullscreen mode Exit fullscreen mode

A new "Authorize" button will appear. Click it and enter your token in the following format:

Bearer <your-token>
Enter fullscreen mode Exit fullscreen mode

Note: Don't forget to include "Bearer" before the token.

After authorization, protected routes will be accessible.

Conclusion

Now we have a simple but effective authentication system that allows public and protected routes in the API. If anything seems off, check out the project repository: Blog - by micaelmi.

Further improvements could include role-based access control (RBAC) to set different permissions for different users or another user-type validation process. But for now, we have a fully functional authentication system.

AWS Q Developer image

Your AI Code Assistant

Automate your code reviews. Catch bugs before your coworkers. Fix security issues in your code. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

Top comments (0)

Billboard image

Try REST API Generation for Snowflake

DevOps for Private APIs. Automate the building, securing, and documenting of internal/private REST APIs with built-in enterprise security on bare-metal, VMs, or containers.

  • Auto-generated live APIs mapped from Snowflake database schema
  • Interactive Swagger API documentation
  • Scripting engine to customize your API
  • Built-in role-based access control

Learn more

👋 Kindness is contagious

If you found this article helpful, please give a ❤️ or share a friendly comment!

Got it