Lokicoule / nestjs-cognito

AWS Cognito utilities module for NestJS.
https://www.npmjs.com/package/@nestjs-cognito/auth
MIT License
81 stars 7 forks source link

Question: Is there a way to define public routes if whole controller uses `@Authentication()` decorator? #773

Closed okonon closed 10 months ago

okonon commented 11 months ago

@Lokicoule Is there a way to define public routes if whole controller uses @Authentication() decorator? basically i would like /config endpoint to not require JWT token.

for example i have following code:

import { Authentication, AuthenticationGuard, CognitoUser } from '@nestjs-cognito/auth';

@Authentication()
@Controller(URL)
export class AuthController {
  constructor(
    private readonly authService: AuthService,
    private userActivityService: UserActivityService
  ) {}

  /**
   * Returns the user object based on the JWT token.
   *
   * @param {User} authUser
   * @returns {Promise<UserResponse>}
   * @memberof AuthController
   */
  @ApiBearerAuth()
  @UseGuards(AuthenticationGuard)
  @Get()
  async getUserWithToken(@CognitoUser() authUser: User): Promise<UserWithFavorites> {
    const favorites = await this.userActivityService.getUserFavorites(authUser.id);
    return {
      ...authUser,
      favorites
    };
  }
  //////////////////////////////////////////////////////////
  // is there a way to make this public?
  //////////////////////////////////////////////////////////
  @ApiBearerAuth()
  @Get('config')
  async getConfig(): Promise<ConfigResponse> {
    return this.authService.getConfig();
  }
}

Thank you!

Lokicoule commented 11 months ago

Good question, simple answer :D
You have two ways to apply the protection logic.

  1. if you work with private routes, you can directly annotate the class with @Authentication or @Authorization
  2. if you mix private and public routes, you can use @UseGuards` decorator and apply the guards you want.

For your use case, you should have to remove the @Authentication class decorator, that's all ;)

import { Authentication, AuthenticationGuard, CognitoUser } from '@nestjs-cognito/auth';

// @Authentication() just don't use this class decorator
@Controller(URL)
export class AuthController {
  constructor(
    private readonly authService: AuthService,
    private userActivityService: UserActivityService
  ) {}

  /**
   * Returns the user object based on the JWT token.
   *
   * @param {User} authUser
   * @returns {Promise<UserResponse>}
   * @memberof AuthController
   */
  @ApiBearerAuth()
  @UseGuards(AuthenticationGuard) // Perfectly fine, you don't need Authentication or Authorization to use it
  @Get()
  async getUserWithToken(@CognitoUser() authUser: User): Promise<UserWithFavorites> {
    const favorites = await this.userActivityService.getUserFavorites(authUser.id);
    return {
      ...authUser,
      favorites
    };
  }
  //////////////////////////////////////////////////////////
  // is there a way to make this public?
  //////////////////////////////////////////////////////////
  @ApiBearerAuth()
  @Get('config')
  async getConfig(): Promise<ConfigResponse> {
    return this.authService.getConfig();
  }
}
okonon commented 11 months ago

@Lokicoule thanks a lot for your answer. Actually i am using a guard for all routes and i list it in providers section of app module:

@Module({
  imports: [
    AuthModule,
    CommonModule,
    UserModule
  ],
  providers: [
    { provide: APP_GUARD, useClass: AuthenticationGuard }
  ],
  controllers: []
})
export class AppModule {}

As a result i have authorization guard applied to all routes. The reason i do that because i only have a few routes that are public (health-check, config etc) and it is easy for me to forget to apply guards on individual route level and leave one leaking sensitive info :)

Just wondering if i had many many routes how would i set all routes to private first and then "whitelist" public ones?

Thanks a lot

Lokicoule commented 11 months ago

@okonon, @nestjs-cognito doesn't have this feature. In my opinion, it's more of a structural issue and you should organize your modules differently. You can group your modules into two distinct 'mid-root' modules instead of one (AppModule). Let's say PublicAppModule and PrivateAppModule. At this point, you should have something like this:

AppModule (root level)
Inject(
1. PublicModule
       inject(
       - PublicModuleOne
       - PublicModuleTwo
       )

2. PrivateModule
       provide: APP_GUARD
       inject(
       - PrivateModuleOne
       - PrivateModuleTwo
       )
)

When I talk about Module, it's also valid for Controller, etc... To depends on your organization's workflow.

okonon commented 10 months ago

@Lokicoule totally make sense thanks for your help.

okonon commented 10 months ago

@Lokicoule i tried to import moduels like this:

import { Module } from '@nestjs/common';
import { CommonModule } from './private/common/common.module';
import { PrivateModule } from './private/private.module';
import { PublicModule } from './public/public.module';

@Module({
  imports: [CommonModule, PublicModule, PrivateModule],
  providers: [],
  controllers: []
})
export class AppModule {}

Public module:

import { Module } from '@nestjs/common';
import { HealthcheckModule } from './healthcheck/healthcheck.module';
import { SettingsModule } from './settings/settings.module';

@Module({
  imports: [HealthcheckModule, SettingsModule],
  providers: [],
  controllers: []
})
export class PublicModule {}

and my private module has APP_GUARD and still public routes require auth:

@Module({
  imports: [
    UserModule,
  ],
  providers: [
    // this actually makes this module 'private'
    { provide: APP_GUARD, useClass: AuthenticationGuard }
  ],
  controllers: []
})
export class PrivateModule {}

I know i am close but missing something. Would you be able to point me to right direction @Lokicoule ?

I really appreciate your help

Lokicoule commented 10 months ago

My bad sorry, according to the doc the APP_GUARD is global. A naive solution for this requirement is to inject metadata inside the request context and retrieve it when the guard performs. I will do my best to do it this week but if you don't want to be blocked you can override https://github.com/Lokicoule/nestjs-cognito/blob/main/packages/auth/lib/abstract.guard.ts and create a new decorator which will inject the metadata.

Lokicoule commented 10 months ago

Please update to version: @nestjs-cognito/auth version 1.1.1. Method decorator @PublicRouteis introduced to fit your needs.

@Get("iampublic")
@PublicRoute()
getPublic() {
    return "public";
}

I didn't test it outside the e2e test scope so I count on you to let me know if you encounter some issues.

okonon commented 10 months ago

@Lokicoule thanks a lot tested and works like a charm ! thank you very much again!