fastify/fastify-passport

Protected routes array when registering passport.initalize()

BenjaminLindberg opened this issue ยท 3 comments

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the feature has not already been requested

๐Ÿš€ Feature Proposal

A way to pass a list of routes that are protected, where the user needs to login to access them. Then fastify-passport takes this list and on requests to those routes, if the request.isAuthenticated() is false, then a error message is returned to the user as well as a 403 status code.

I would also want to be able to pass a config value trough the routes to directly be able to mark a route as protected. Or to disable it.

Motivation

I've worked with fastify-passport on a few projects. And I feel like I'm always repeating myself on making a middleware function that I add to protected routes, that checks if the request.isAuthenticated() function returns true and if not it throws a 403 http code back.

But this becomes a bit inconvenient with the type of the PassportUser, since I then need to rewrite the type of the request.user on all of the routes i use the middleware on. As well as having to add the preValidation function to all of the routes individually.

And I was about to make a plugin for this that i can use throughout my applications, but I came to the conclusion that it is probably best to make this a native feature of fastify-passport.

(EDIT: So I read the docs a bit, and seems like this function down below is not intended for checking if the user is logged in, and only for making them signed in/up. So ignore the text regarding that. )

I know that the

passport.authenticate(['custom', 'google'], {
    authInfo: false
})

function exist, but it does not work for me since i get this error. And whenever i call the route, the application silently hangs itself and i get a connection error. And i suspect that you cannot use the passport.authenticate() function along with other preValidation functions??

image

Example

fastify.register(passport.initialize(), {
   protectedRoutes: ["/user/*", "/dashboard/edit"],

   // Ignore routes gets overwrites protectedRoutes, but both of those gets overwritten by specifying it directly in the route config
   ignoredRoutes: ["/users/auth/*"],
});
fastify.route({
  url: "/",
  method: "GET",
  schema: ExampleSchema,
  config: {
    //Optional value, default false. Overwrites the protectedRoutes & ignoredRoutes specified when registering fasitify-passport

    authentication: true|false
  },
  handler: async (request, reply) => {
    return { message: "Example route" };
  },
});

Either you pass the value to the route to enable/disable the protected route, or you pass it to the registration function. Wildcards should be allowed, to make it easier and faster to make a whole collection of routes protected or ignored. And this could then be overwritten on single routes via disabling it in that specific route. By default no route is protected. And ignored routes is to make it easier to work with nested routes, for eg. all user routes should be protected except /users/auth/*.

I've made a simple plugin for personal use that i uses for this and I'm fine with using it, but i still believe that this should be a native feature of fastify-passport. And I mean, I'm not sure if I'm using passport wrong, but I dont think that you're supposed to use passport.authenticate(["strategy1", "strategy2"], { authInfo: false}) for this usecase.

import { FastifyInstance } from "fastify";
import fp from "fastify-plugin";
/**
 * This plugin checks if the route is set to be protected, and compares the route against a list of protected routes
 */

declare module "fastify" {
  interface FastifyContextConfig {
    authentication?: boolean;
  }
}

export interface FastifyPluginOptions {
  protectedRoutes?: string[];
  ignoredRoutes?: string[];
}

const fastifyRouteProtector = fp(
  async (fastify: FastifyInstance, opts: FastifyPluginOptions) => {
    const protectedRoutes = opts?.protectedRoutes || [];
    const ignoredRoutes = opts?.ignoredRoutes || [];

    function isProtected(url: string, authentication?: boolean): boolean {
      let _protected = false;

      if (protectedRoutes.includes(url)) _protected = true;
      if (
        protectedRoutes.find(
          (route) =>
            (route.endsWith("*") || route.endsWith("/*")) &&
            url.startsWith(
              route.endsWith("/*") ? route.split("/*")[0] : route.split("*")[0]
            )
        )
      )
        _protected = true;

      if (ignoredRoutes.includes(url)) _protected = false;
      if (
        ignoredRoutes.find(
          (route) =>
            (route.endsWith("*") || route.endsWith("/*")) &&
            url.startsWith(
              route.endsWith("/*") ? route.split("/*")[0] : route.split("*")[0]
            )
        )
      )
        _protected = false;

      if (authentication) _protected = true;
      else if (!authentication && typeof authentication !== "undefined")
        _protected = false;

      return _protected;
    }

    fastify.addHook("preValidation", async (request, reply) => {
      const { authentication } = request.routeOptions.config;

      if (!isProtected(request.url, authentication)) return;

      if (!request.isAuthenticated())
        throw {
          error: "Unathorized",
          message: "You're not allowed to access this route.",
          statusCode: 403,
        };

      return;
    });
  }
);

export default fp(async (fastify) => {
  await fastify.register(fastifyRouteProtector, {
    protectedRoutes: ["/organizations/*", "/admin/*", "/users/*"],
    ignoredRoutes: ["/users/auth/*"],
  });
});

matching routes in that way is one of the reasons why Express gets slower the more middleware you add.

There are two options to implement this efficiently via an onRoute hooks:

  1. add the preValidation hook only to the matching routes.
  2. add a fastify.route({ config: { passportAuthentiate: true }, ... }) check. Then add the preValidation hook accordingly.

@mcollina I'll check that out. Thanks!