node.jstypescriptauthenticationfastifyfastify-jwt

Fastify Decorator not acting as a preHandler in Fastify-REST-API


I’m fairly new to Fastify, and I’m using it to build an API. I'm having some trouble when using an authentication decorator in a route, which is throwing the following error: "code":"FST_ERR_HOOK_INVALID_HANDLER","name":"FastifyError","statusCode":500},"msg":"preHandler hook should be a function, instead got [object Undefined]".

I've set up a decorator file at @/decorators/authentication.ts

import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import fjwt, { FastifyJWT } from "@fastify/jwt";
import fCookie from "@fastify/cookie";
import { env } from "@/config/constants";  // Ensure the correct path

export default async function authentication(app: FastifyInstance) {
  // Register JWT plugin
  app.register(fjwt, { secret: String(env.JWT_SECRET_KEY) });
  
  // Register cookies plugin
  app.register(fCookie, {
    secret: env.JWT_SECRET_KEY,
    hook: "preHandler",
  });

  // Decorate app with the authenticate function
  app.decorate(
    "authenticate",
    async (req: FastifyRequest, reply: FastifyReply) => {
      try {
        const token = req.cookies.access_token;
        if (!token) {
          return reply.status(401).send({ message: "Authentication required" });
        }
        // Verify the token and set the user in the request object
        const decoded = req.jwt.verify<FastifyJWT['user']>(token)
        req.user = decoded;
        
      } catch (err) {
        return reply.status(401).send({ message: "Invalid or expired token" });
      }
    }
  );
}

and using it on @/modules/authentication/authentication.routes.ts

import { FastifyInstance } from "fastify";
import AuthenticationController from "./authentication.controller";
import UserService from "@/data-services/user-service";
import { $ref, userSchemas } from "./authentication.schema";

export default async function authRoutes(fastify: FastifyInstance) {
  // Decorate fastify with the user service
  fastify.decorate("userService", new UserService());

  // Register schemas
  userSchemas.forEach((schema) => fastify.addSchema(schema));

  const authController = new AuthenticationController(fastify.userService);

  // Login route
  fastify.post(
    "/login",
    {
      schema: {
        body: $ref("loginSchema"),
        response: {
          201: $ref("loginResponseSchema"),
        },
      },
    },
    authController.login.bind(authController)
  );

  // Register route
  fastify.post(
    "/registro",
    {
      schema: {
        body: $ref("registerUserSchema"),
        response: {
          201: $ref("registerUserResponseSchema"),
        },
      },
    },
    authController.register.bind(authController)
  );

  // Get current user route with preHandler for authentication
  fastify.get(
    "/me",
    {
      preHandler: [fastify.authenticate],
    },
    authController.getCurrentUser.bind(authController)
  );
}

I set up my server.ts file as this

import Fastify from "fastify";
import { env } from "./config/constants";
import "./types";
import authentication from "@/decorators/authentication";
import routes from "./config/routes";

const app = Fastify({
  logger: true,
});

const listeners = ["SIGINT", "SIGTERM"];
listeners.forEach((signal) => {
  process.on(signal, async () => {
    await app.close();
    process.exit(0);
  });
});

app.register(authentication);
app.register(routes);

app.listen(
  {
    port: env.PORT,
  },
  (err, address) => {
    if (err) {
      app.log.error(err);
      process.exit(1);
    }
    app.log.info(`server listening on ${address}, PORT: ${env.PORT}`);
  }
);

I'm logging a temporary handler on the /me route, but fastify.authenticate is returning as undefined. Can anyone explain why this is happening?

packages versions

"@fastify/cookie": "^10.0.1",
 "@fastify/jwt": "^9.0.1",
  "fastify": "^5.0.0",
  "fastify-zod": "^1.4.0",

also already checked these solutions related stack overflow topic Github related issue but didnot found an answer in any of these

I was trying to authenticate the user using the decorator method i created before, but seems to not work, since my routes complains that authenticate is undefined. I have already registered the authentication decorator before the routes and added logs to confirm that the decorators are being registered properly before the routes.


Solution

  • The issue is that fastify.authenticate returns undefined This is because the export default async function authentication(app: FastifyInstance) plugin does not use the fastify-plugin module, so when it is registered app.register(authentication); it creates its own context that does not effect the routes one.

    To fix it you need to wrap the function:

    import fp from 'fastify-plugin'
    
    export default fp(
      async function authentication(app: FastifyInstance) { ... }
    )
    

    Useful readings: