Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth for NestJS and Fastify #698

Open
anteqkois opened this issue Mar 9, 2024 · 15 comments
Open

Auth for NestJS and Fastify #698

anteqkois opened this issue Mar 9, 2024 · 15 comments

Comments

@anteqkois
Copy link

Is there any way to add auth layer for this configuration? I have tried middleware and npm packages for basic auth but they don't work. With middlewares, there is probably an issue with resolving paths. So now, I don't find any way to add secure auth layer ://

@felixmosh
Copy link
Owner

felixmosh commented Mar 10, 2024

Did you checked the examples folder?

@jakub-zawislak
Copy link

@anteqkois Did you manage to set up the authentication?

@niraj-khatiwada
Copy link

Hi @felixmosh the example directory seem to have code for ExpressAdapter but not for FastifyAdapter. I'm also trying to implement auth for bullboard. Is there any reference for auth with Nest+Fastify?

Thanks for the awesome library.

@felixmosh
Copy link
Owner

Nope, I don't have secret examples that are not in the examples folder 😅

@ChazUK
Copy link

ChazUK commented Dec 2, 2024

I'm also struggling trying to figure out how to add Authentication via the Nest.js Module approach.

@niraj-khatiwada
Copy link

For now, I'm just disabling the access of bull board UI in Nginx and access it with ssh and port forwarded to my local. It's a bit cumbersome but works.

@clintonb
Copy link

clintonb commented Jan 6, 2025

@felixmosh can you reopen this? There is a real need for an example of using Bull Board with NestJS. At the very least this issue can be a call for someone to provide such an example. I've been struggling with this for the past few hours, and there is a lot to understand between Bull Board, NestJS, Passport (most likely), and the underlying server adapter (e.g., Express or Fastify).

I understand most of these components individually. Putting them together is proving more difficult than I anticipated. An example would go a long way toward saving many others hours of effort.

Edit: Now that I've dug into the code itself, it seems that BullBoardRootModule bypasses NestJS app guards altogether:

if (isExpressAdapter(this.adapter)) {
return consumer
.apply(this.options.middleware, this.adapter.getRouter())
.forRoutes(this.options.route);
}
if (isFastifyAdapter(this.adapter)) {
this.adapterHost.httpAdapter
.getInstance()
.register(this.adapter.registerPlugin(), { prefix });
return consumer
.apply(this.options.middleware)
.forRoutes(this.options.route);
}

We effectively need to treat this as a separate middleware that exists almost entirely outside of the NestJS framework.

@felixmosh felixmosh reopened this Jan 7, 2025
@felixmosh
Copy link
Owner

@clintonb do you have any improvment for the Nest.js adapter?
(I'm not familier with Nest.js at all, therefore, I relay on contribution)

@juandl
Copy link
Contributor

juandl commented Mar 2, 2025

Hey guys, bull-board nestjs package supports "express, fastify" middlewares, so it should be very easy to add authentication.

import basicAuth from "express-basic-auth";


BullBoardModule.forRoot({
	route: "/queues",
	adapter: ExpressAdapter,
	middleware: basicAuth({
		challenge: true,
		users: { admin: "supersecret" },
	}),
}),

that will trigger auth basic when you access to "/queues"

Image

@felixmosh maybe we can add this in the readme...

@felixmosh
Copy link
Owner

Hey guys, bull-board nestjs package supports "express, fastify" middlewares, so it should be very easy to add authentication.

import basicAuth from "express-basic-auth";


BullBoardModule.forRoot({
	route: "/queues",
	adapter: ExpressAdapter,
	middleware: basicAuth({
		challenge: true,
		users: { admin: "supersecret" },
	}),
}),

that will trigger auth basic when you access to "/queues"

Image

@felixmosh maybe we can add this in the readme...

can you make a PR that update the nest.js README file?

@juandl
Copy link
Contributor

juandl commented Mar 2, 2025

@felixmosh #906

@clintonb
Copy link

clintonb commented Mar 9, 2025

I really wish Bull Board was updated to be a more "Nesty" app instead of operating as an independent Express app.

I ultimately did the following:

  1. Write a function that converts guard to middleware.
  2. Pass a "redirect to login" guard/middleware to BullBoardModule.
  3. Implement login with Google OAuth + session storage.

This ensures that users must be authenticated to access Bull Board, and authentication is powered by existing components (e.g., Passport).

Here is come code.

// NOTE: BullBoardModule interfaces directly with Express. We implement some custom middleware to
// adapt our NestJS guards into compatible Express middleware so we can avoid duplicating logic.
BullBoardModule.forRootAsync({
  imports: [AdminAuthModule],
  inject: [RedirectToLoginGuard],
  useFactory: (guard: RedirectToLoginGuard) => {
    return {
      route: '/queues',
      adapter: ExpressAdapter,
      middleware: [guardToMiddleware(guard)],
    };
  },
})
/**
 * Convert an authentication guard to Express middleware.
 *
 * This useful for integrating with modules that should be placed behind authentication,
 * but don't interface nicely with NestJS.
 */
export function guardToMiddleware(guard: CanActivate) {
  return async (req: Request, res: Response, next: NextFunction) => {
    try {
      class mockControllerClass {}
      const context: ExecutionContext = {
        // @ts-expect-error This is not relevant
        getClass: () => mockControllerClass,
        getHandler: () => () => {
          // This is intentionally empty.
        },
        switchToHttp: () => ({
          // @ts-expect-error I (clintonb) don't know why TypeScript complains. The code works, though.
          getRequest: () => req,
          // @ts-expect-error I (clintonb) don't know why TypeScript complains. The code works, though.
          getResponse: () => res,
        }),
      };

      if (await guard.canActivate(context)) {
        next();
      } else {
        res.status(403).json({ message: 'Forbidden' });
      }
    } catch (error) {
      next(error);
    }
  };
}
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request, Response } from 'express';
import { Observable } from 'rxjs';

import { IS_PUBLIC_KEY } from '@vori/nest/libs/auth/constants';

@Injectable()
export class RedirectToLoginGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(
    context: ExecutionContext
  ): boolean | Promise<boolean> | Observable<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (isPublic) {
      return true;
    }

    const request = context.switchToHttp().getRequest<Request>();
    const response = context.switchToHttp().getResponse<Response>();

    if (request.isAuthenticated()) {
      return true;
    }

    response.redirect('/auth/login');
    return false;
  }
}

The /auth/login endpoint has a guard that facilitates Google OAuth 2.0 via Passport. It's not directly relevant, but adding here for posterity since I spent two days down this rabbit hole! If you're trying to do session auth (after getting the Google token), the call to logIn is crucial along with configuring sameSite: 'lax' for express-session.

import { ExecutionContext, Injectable, Logger } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

import { PassportStrategyName } from '@vori/nest/libs/auth/constants';

@Injectable()
export class AdminAuthGuard extends AuthGuard([
  PassportStrategyName.InternalGoogleOauth,
]) {
  private readonly logger = new Logger(AdminAuthGuard.name);

  async canActivate(context: ExecutionContext): Promise<boolean> {
    try {
      const result = await super.canActivate(context);
      const request = this.getRequest(context);

      if (result && request.user) {
        this.logger.debug({ user: request.user }, `Authentication succeeded.`);

        // NOTE (clintonb): This is crucial to ensuring successful login.
        // It's unclear why none of the NestJS docs mention this.
        await super.logIn(request);
      }

      // @ts-expect-error We aren't using Observable
      return result;
    } catch (err) {
      this.logger.error({ err }, `Authentication failed!`);

      return false;
    }
  }
}

P.S. Yes, I know I am using Express and this ticket is about Fastify, but I suspect this knowledge applies to both frameworks.

@felixmosh
Copy link
Owner

@clintonb I'm not familiar with Nest.js at all, do u think that the module should be updated? Will you make a PR for it?

@clintonb
Copy link

Yes, I think it should be updated, but I don’t have the capacity to do so at this time. It took me two months to get back to my basic installation. 🤣

@wginsberg
Copy link

Here's another possible way to use an auth guard. Not as robust as what @clintonb posted above but might be useful.

@Module({
    imports: [
        BullBoardModule.forRootAsync({
            useFactory: (jwtAuthStrategy: JwtStrategy) => {
                return {
                    route: "/queues",
                    adapter: ExpressAdapter,
                    middleware: async (req, res, next) => {
                        const authCookie = req.cookies?.["auth"]
                        if (!authCookie) return res.sendStatus(401)

                        try {
                            jwtAuthStrategy.validate(authCookie)
                            next()
                        } catch {
                            res.sendStatus(401)
                        }
                    }
                }
            },
            imports: [AuthModule],
            inject: [JwtStrategy]
        }),
    ],
})
class SecureBullBoardModule { }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants