Search code examples
javascriptnode.jsnestjs

How to make guard optional only in particular case in NestJs?


I have the below two guards in NestJS(one for api key based authentication and another for token based authentication).The ApiKeyGuard is the top priority.I want to implement a system where if anyone has a key it will not check the other guard.Is there any way I can make the AuthGuard optional based on whether the first Guard passed in cases where there is a ApiKeyGuard?

// Can be accessed with token within app as well as third party users
    @UseGuards(ApiKeyGuard, AuthGuard)
    @Get('/get-products')
      async getProducts(): Promise<any> {
        try {
          return this.moduleRef
            .get(`appService`, { strict: false })
            .getProducts();
        } catch (error) {
          throw new InternalServerErrorException(error.message, error.status);
        }
    
      }

// Only to be accessed with token within app
    @UseGuards(AuthGuard)
    @Get('/get-users')
      async getUsers(): Promise<any> {
        try {
          return this.moduleRef
            .get(`appService`, { strict: false })
            .getUsers();
        } catch (error) {
          throw new InternalServerErrorException(error.message, error.status);
        }
    
      }

The below guard is used to check for api key based authentication

api-key.guard.ts

@Injectable()
export class ApiKeyGuard implements CanActivate {
  constructor(private readonly apiKeyService: ApiKeyService) {} 

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const req = context.switchToHttp().getRequest();
    const key = req.headers['X-API-KEY'] ?? req.query.api_key;
    return this.apiKeyService.isKeyValid(key);
  }

The below guard is used to check for token based authentication

        authentication.guard.ts
        
        @Injectable()
        
        export class AuthGuard implements CanActivate, OnModuleInit {
          constructor(private readonly moduleRef: ModuleRef) {}
          onModuleInit() {}
          async canActivate(context: ExecutionContext): Promise<boolean> {
        
            try {
        
              // Get request data and validate token
        
              const request = context.switchToHttp().getRequest();
        
              if (request.headers.authorization) {
                const token = request.headers.authorization.split(' ')[1];
                const response = await this.checkToken(token);
           
                if (response) {
                  return response;
                } else {
                  throw new UnauthorizedException();
                }
              } else {
                throw new UnauthorizedException();
              }
        
            } catch (error) {
              throw new UnauthorizedException();
            }
        
          }
        
        }

Solution

  • What I did was using @nestjs/passport and using the AuthGuard and making custom PassportJS strategies. I had a similar issue, and looked for a way to accomplish this without using some "magic". The documentation can be found here.

    In the AuthGuard, you can add multiple guards. It's a bit hidden away in the documentation, although it is very powerful. Take a look here, especially the last line of the section, it states:

    In addition to extending the default error handling and authentication logic, we can allow authentication to go through a chain of strategies. The first strategy to succeed, redirect, or error will halt the chain. Authentication failures will proceed through each strategy in series, ultimately failing if all strategies fail.

    Which can be done like so:

    export class JwtAuthGuard extends AuthGuard(['strategy_jwt_1', 'strategy_jwt_2', '...']) { ... }
    

    Now, back to your example, you've to create 2 custom strategies, one for the API key and one for the authorization header, and both these guards should be activated.

    So for the API strategy (as example):

    import { Strategy } from 'passport-custom';
    import { Injectable } from '@nestjs/common';
    import { PassportStrategy } from '@nestjs/passport';
    import { Strategy } from 'passport-custom';
    import { Injectable, UnauthorizedException } from '@nestjs/common';
    import { PassportStrategy } from '@nestjs/passport';
    
    @Injectable()
    export class ApiStrategy extends PassportStrategy(Strategy, 'api-strategy') {
      constructor(private readonly apiKeyService: ApiKeyService) {} 
    
      async validate(req: Request): Promise<User> {
        const key = req.headers['X-API-KEY'] ?? req.query.api_key;
        if ((await this.apiKeyService.isKeyValid(key)) === false) {
          throw new UnauthorizedException();
        }
    
        return this.getUser();
      }
    }
    

    Do something similar for your other way of authenticating, and then use the Passport guard as follows:

    @UseGuard(AuthGuard(['api-strategy', 'other-strategy'])
    

    This way, the guard will try all strategies (in order) and when all of them fail, your authentication has failed. If one of them succeeds, you're authenticated!