Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NestJS - Combine multiple Guards and activate if one returns true

Tags:

nestjs

Is it possible to use multiple auth guards on a route (in my case basic and ldap auth). The route should be authenticated when one guard was successful.

like image 202
blacksheep_2011 Avatar asked Sep 15 '20 06:09

blacksheep_2011


People also ask

What is Authguard Nestjs?

A guard is a class annotated with the @Injectable() decorator, which implements the CanActivate interface. Guards have a single responsibility. They determine whether a given request will be handled by the route handler or not, depending on certain conditions (like permissions, roles, ACLs, etc.) present at run-time.


Video Answer


5 Answers

Short answer: No, if you add more than one guard to a route, they all need to pass for the route to be able to activate.

Long answer: What you are trying to accomplish is possible however by making your LDAP guard extend the basic one. If the LDAP specific logic succeeds, return true, otherwise return the result of the call to super.canActivate(). Then, in your controller, add either the basic or LDAP guard to your routes, but not both.

basic.guard.ts

export BasicGuard implements CanActivate {
  constructor(
      protected readonly reflector: Reflector
  ) {}

  async canActivate(context: ExecutionContext) {
     const request = context.switchToHttp().getRequest();
     if () {
        // Do some logic and return true if access is granted
        return true;
     }

    return false;
  }
}

ldap.guard.ts

export LdapGuard extends BasicGuard implements CanActivate {
  constructor(
      protected readonly reflector: Reflector
  ) {
   super(reflector);
}

  async canActivate(context: ExecutionContext) {
     const request = context.switchToHttp().getRequest();
     if () {
        // Do some logic and return true if access is granted
        return true;
     }
    
    // Basically if this guard is false then try the super.canActivate.  If its true then it would have returned already
    return await super.canActivate(context);
  }
}

For more information see this GitHub issue on the official NestJS repository.

like image 127
thomaux Avatar answered Oct 19 '22 01:10

thomaux


You can create an abstract guard, and pass instances or references there, and return true from this guard if any of the passed guards returned true.

Let's imagine you have 2 guards: BasicGuard and LdapGuard. And you have a controller UserController with route @Get(), which should be protected by these guards.

So, we can create an abstract guard MultipleAuthorizeGuard with next code:

@Injectable()
export class MultipleAuthorizeGuard implements CanActivate {
  constructor(private readonly reflector: Reflector, private readonly moduleRef: ModuleRef) {}

  public canActivate(context: ExecutionContext): Observable<boolean> {
    const allowedGuards = this.reflector.get<Type<CanActivate>[]>('multipleGuardsReferences', context.getHandler()) || [];

    const guards = allowedGuards.map((guardReference) => this.moduleRef.get<CanActivate>(guardReference));

    if (guards.length === 0) {
      return of(true);
    }

    if (guards.length === 1) {
      return guards[0].canActivate(context) as Observable<boolean>;
    }

    const checks$: Observable<boolean>[] = guards.map((guard) =>
      (guard.canActivate(context) as Observable<boolean>).pipe(
        catchError((err) => {
          if (err instanceof UnauthorizedException) {
            return of(false);
          }
          throw err;
        }),
      ),
    );

    return forkJoin(checks$).pipe(map((results: boolean[]) => any(identity, results)));
  }
}

As you can see, this guard doesn't contain any references to a particular guard, but only accept the list of references. In my example, all guards return Observable, so I use forkJoin to run multiple requests. But of course, it can be adopted to Promises as well.

To avoid initiating MultipleAuthorizeGuard in the controller, and pass necessary dependencies manually, I'm left this task to Nest.js and pass references via custom decorator MultipleGuardsReferences

export const MultipleGuardsReferences = (...guards: Type<CanActivate>[]) =>
  SetMetadata('multipleGuardsReferences', guards);

So, in controller we can have next code:

@Get()
@MultipleGuardsReferences(BasicGuard, LdapGuard)
@UseGuards(MultipleAuthorizeGuard)
public getUser(): Observable<User> {
  return this.userService.getUser();
}
like image 30
Sergiy Voznyak Avatar answered Oct 19 '22 02:10

Sergiy Voznyak


According to AuthGuard it just works out of the box

AuthGuard definition

If you look at AuthGuard then you see the following definition:

(File is node_modules/@nestjs/passport/dist/auth.guard.d.ts)

export declare const AuthGuard: (type?: string | string[]) => Type<IAuthGuard>;

That means that AuthGuard can receive an array of strings.

Code

In my code I did the following:

  @UseGuards(AuthGuard(["jwt", "api-key"]))
  @Get()
  getOrders() {
    return this.orderService.getAllOrders();
  }

Postman test

In Postman, the endpoint can have the api-key and the JWT.

  • Tested with JWT in Postman Authorization: It works
  • Tested with API-Key in Postman Authorization: It works

That implies there is an OR function between the 2 Guards.

like image 27
BertC Avatar answered Oct 19 '22 02:10

BertC


You can use combo guard that injects all guards what you need and combines their logic. There is closed github issue: https://github.com/nestjs/nest/issues/873

like image 1
Layam Avatar answered Oct 19 '22 03:10

Layam


There is also a npm package that address this scenario: https://www.npmjs.com/package/@nest-lab/or-guard.

Then you call a unique guard that references all the necessary guards as parameters:

guards([useGuard('basic') ,useGuard('ldap')])
like image 1
strtCoding Avatar answered Oct 19 '22 01:10

strtCoding