4

I'm trying a create a shared Guard as an external library in order to be imported and used across services. I'm not doing anything special that what is described in some guides but with the particularity that the code will reside in a shared library. Everything is working but the Exception to return a 401 error.

My guard looks something like this:

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

@Injectable()
export class MainGuard extends AuthGuard('jwt') {}

Nothing else. If I use that in a service folder it works, but at the time that I move as in their own library, the response changes.

The way that I'm using in the service has nothing special:

import { MainGuard } from 'shared-guard-library';
import { Controller, Get, UseGuards } from '@nestjs/common';
import { SomeService } from './some.service';

@Controller()
export class SomeController {
  constructor(private someService: SomeService) {}

  @Get('/foo')
  @UseGuards(MainGuard)
  async getSomething(): Promise<any> {
    return this.someService.getSomething();
  }
}

The client receives an error 500:

http :3010/foo
HTTP/1.1 500 Internal Server Error
Connection: keep-alive
Content-Length: 52
Content-Type: application/json; charset=utf-8
Date: Thu, 09 Dec 2021 04:11:42 GMT
ETag: W/"34-rlKccw1E+/fV8niQk4oFitDfPro"
Keep-Alive: timeout=5
Vary: Origin
X-Powered-By: Express

{
    "message": "Internal server error",
    "statusCode": 500
}

And in the logs shows:

[Nest] 93664  - 12/08/2021, 10:11:42 PM   ERROR [ExceptionsHandler] Unauthorized
UnauthorizedException: Unauthorized
    at MainGuard.handleRequest (/sharedGuardLibrary/node_modules/@nestjs/passport/dist/auth.guard.js:68:30)
    at /sharedGuardLibrary/node_modules/@nestjs/passport/dist/auth.guard.js:49:128
    at /sharedGuardLibrary/node_modules/@nestjs/passport/dist/auth.guard.js:86:24
    at allFailed (/sharedGuardLibrary/node_modules/passport/lib/middleware/authenticate.js:101:18)
    at attempt (/sharedGuardLibrary/node_modules/passport/lib/middleware/authenticate.js:174:28)
    at Object.strategy.fail (/sharedGuardLibrary/node_modules/passport/lib/middleware/authenticate.js:296:9)
    at Object.JwtStrategy.authenticate (/sharedGuardLibrary/node_modules/passport-jwt/lib/strategy.js:96:21)
    at attempt (/sharedGuardLibrary/node_modules/passport/lib/middleware/authenticate.js:360:16)
    at authenticate (/sharedGuardLibrary/node_modules/passport/lib/middleware/authenticate.js:361:7)
    at /sharedGuardLibrary/node_modules/@nestjs/passport/dist/auth.guard.js:91:3

The logs are telling me that the correct exception was thrown, but is ignored at some point and I don't know the reason. Again: the same code in the same project works.

I took a look at the original class and I don't see any particular way to treat the exception

Any clue or guide it will appreciate.

maturanomx
  • 88
  • 1
  • 9
  • if the same code in the same project works, try `rm -rf node_modules` and install it again (without touching the lock file) – Micael Levi Dec 09 '21 at 12:48
  • Already tried that and similar related things like cleaning npm cache with the service and the library; Same result – maturanomx Dec 09 '21 at 17:36

3 Answers3

4

So, this happens to be a "feature" of Typescript and how JavaScript object equality works in general. So in Nest's BaseExceptionFilter there's a check that exception instanceof HttpException, and normally, UnauthorizedException would be an instance of this, but because this is a library there's a few things that need to be considered.

  1. All of the NestJS dependencies you're using have to be peerDependencies. This makes sure that when the library is installed, there's only one resulting package for the @nestjs/* package.

  2. during local development, you'll need to take care to ensure that you're not resolving multiple instances of the same package (even if it's the exact same version, to JavaScript { hello: 'world' } === { hello: 'world' } // false). To take care of this, things like npm/yarn/pnpm link should not be used, but instead you should copy the dist and the package.json to the main application's node_modules/<package_name> directory.

    a. The other option is using a monorepo tool like Nest's monorepo approach or Nx which have single package version approaches, and use the paths of the libraries rather than internal links.

If you follow this, when your production application installs the npm library, everything will work without an issue. It's an annoyance for sure, but it's a side effect of how JavaScript works

Jay McDoniel
  • 57,339
  • 7
  • 135
  • 147
  • I would never have come up with the explanation without your help. Thank you so much! – maturanomx Dec 13 '21 at 15:23
  • 2
    How did you solve this? I have the same problem, I have a Rust monorepo with a pnpm, after updating nestjs to 8.2.0 it responds with 500 error instead of 401. Any suggestion? – Asder999 Mar 28 '22 at 08:17
  • Why is this answer being accepterd if does not answer the question? – Gabriel Llamas Jul 07 '22 at 12:58
  • 1
    @GabrielLlamas But it does answer the question. The `UnauthorizedExcepotion` is being treated as an `Internal Server Error` because the `instanceof HttpException` check is failing due to how JavaScript treats object equality. – Jay McDoniel Jul 07 '22 at 13:26
0

I had a similar problem with a custom auth package inside a monorepo.

My auth-library exposed AuthModule, JwtAuthGuard, and some utility functions. All needed packages were installed under my library so any other projects that were using it had not installed other versions of dependencies. Unfortunately using a custom guard caused Internal Server Error.

I've solved this issue by adding a global custom exception filter. It looks for a workaround but at least solves this issue.

This filter is exported from auth-library, so UnauthorizedException indicates on the same object as AuthGuard.

import { ArgumentsHost, Catch, ExceptionFilter, UnauthorizedException } from '@nestjs/common';
import { Response } from 'express'

@Catch(UnauthorizedException)
export class UnauthorizedExceptionFilter implements ExceptionFilter {
  public catch(exception: UnauthorizedException, host: ArgumentsHost): Response {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();

    return response.status(401).json({ statusCode: 401 });
  }
}
0

See Extending Guards: https://docs.nestjs.com/security/authentication#extending-guards

In most cases, using a provided AuthGuard class is sufficient. However, there might be use-cases when you would like to simply extend the default error handling or authentication logic. For this, you can extend the built-in class and override methods within a sub-class.

Implement handleRequest(err, user, info) as follows:

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
      
  handleRequest(err, user, info) {
    // You can throw an exception based on either "info" or "err" arguments
    if (err || !user) {
      throw err || new UnauthorizedException();
    }
    return user;
  }

}
Andres
  • 1