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

Nodejs exits when trying to authorize with an expired access token #292

Closed
m-takata opened this issue Apr 1, 2022 · 4 comments · May be fixed by donavon/node-jwks-rsa#14
Closed

Nodejs exits when trying to authorize with an expired access token #292

m-takata opened this issue Apr 1, 2022 · 4 comments · May be fixed by donavon/node-jwks-rsa#14
Labels

Comments

@m-takata
Copy link

m-takata commented Apr 1, 2022

Describe the problem

I'm trying to extend NestJS's PassportStrategy to incorporate jwks-rsa and protect the API with OAuth authorization.

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { passportJwtSecret } from 'jwks-rsa';
import * as dotenv from 'dotenv';

dotenv.config();

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      secretOrKeyProvider: passportJwtSecret({
        cache: true,
        rateLimit: true,
        jwksRequestsPerMinute: 5,
        jwksUri: `${process.env.AUTH0_ISSUER_URL}.well-known/jwks.json`,
      }),

      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      audience: process.env.AUTH0_AUDIENCE,
      issuer: `${process.env.AUTH0_ISSUER_URL}`,
      algorithms: ['RS256'],
    });
  }

  validate(payload: unknown): unknown {
    return payload;
  }
}

When I made a request to the NestJS AuthGuard protected API with an expired access token, an unexpected exception occurred and the NodeJS process exited.

UnauthorizedException: Unauthorized
    at JwtAuthGuard.handleRequest (..../node_modules/@nestjs/passport/dist/auth.guard.js:68:30)
    at ..../node_modules/@nestjs/passport/dist/auth.guard.js:49:128
    at ..../node_modules/@nestjs/passport/dist/auth.guard.js:86:24
    at allFailed (..../node_modules/passport/lib/middleware/authenticate.js:110:18)
    at attempt (..../node_modules/passport/lib/middleware/authenticate.js:183:28)
    at JwtStrategy.strategy.fail (..../node_modules/passport/lib/middleware/authenticate.js:305:9)
    at ..../node_modules/passport-jwt/lib/strategy.js:106:33
    at ..../node_modules/jsonwebtoken/verify.js:152:16
    at getSecret (..../node_modules/jsonwebtoken/verify.js:90:14)
    at Object.module.exports [as verify] (..../node_modules/jsonwebtoken/verify.js:94:10)

error Command failed with exit code 1.

What was the expected behavior?

Invalid token, so I want to be protected by AuthGuard and respond 403

Reproduction

Environment

node : v16.13.2
framework: NestJS 8

dependencies of package.json

  "dependencies": {
    "@algoan/nestjs-logging-interceptor": "^2.1.10",
    "@google-cloud/storage": "^5.18.3",
    "@nestjs/common": "^8.0.0",
    "@nestjs/config": "^2.0.0",
    "@nestjs/core": "^8.0.0",
    "@nestjs/jwt": "^8.0.0",
    "@nestjs/mapped-types": "*",
    "@nestjs/passport": "^8.0.1",
    "@nestjs/platform-express": "^8.4.3",
    "@nestjs/swagger": "^5.1.5",
    "@prisma/client": "^3.11.1",
    "camelcase-keys": "^7.0.2",
    "class-transformer": "^0.5.1",
    "class-validator": "^0.13.2",
    "csvtojson": "^2.0.10",
    "joi": "^17.5.0",
    "jwks-rsa": "^2.0.5",
    "jwt-decode": "^3.1.2",
    "log4js": "^6.4.4",
    "memory-streams": "^0.1.3",
    "passport": "^0.5.2",
    "passport-jwt": "^4.0.0",
    "prisma": "^3.11.1",
    "reflect-metadata": "^0.1.13",
    "rimraf": "^3.0.2",
    "rxjs": "^7.5.5",
    "swagger-ui-express": "^4.3.0",
    "uuid": "^8.3.2"
  },

Consideration

UnauthorizedException: Unauthorized
    at JwtAuthGuard.handleRequest (..../node_modules/@nestjs/passport/dist/auth.guard.js:68:30)  (7)
    at ..../node_modules/@nestjs/passport/dist/auth.guard.js:49:128
    at ..../node_modules/@nestjs/passport/dist/auth.guard.js:86:24
    at allFailed (..../node_modules/passport/lib/middleware/authenticate.js:110:18)
    at attempt (..../node_modules/passport/lib/middleware/authenticate.js:183:28)
    at JwtStrategy.strategy.fail (..../node_modules/passport/lib/middleware/authenticate.js:305:9)
    at ..../node_modules/passport-jwt/lib/strategy.js:106:33                                     (6)
    at ..../node_modules/jsonwebtoken/verify.js:152:16                                           (5)
    at getSecret (..../node_modules/jsonwebtoken/verify.js:90:14)
    at Object.module.exports [as verify] (..../node_modules/jsonwebtoken/verify.js:94:10)        (4)

(1) Authenticate of strategy.js of passport-jwt is called in canActivate() of AuthGuard of @nestjs/passport.
https://github.com/nestjs/passport/blob/master/lib/auth.guard.ts#L56

(2) authenticate of strategy.js of passport-jwt calls secretProvider returned by passportJwtSecret of jwks-rsa.
https://github.com/mikenicholson/passport-jwt/blob/master/lib/strategy.js#L99

(3) The secretProvider returned by jwks-rsa's passportJwtSecret calls the callback function in the response process then() of the asynchronous call to client.getSigningKey().
https://github.com/auth0/node-jwks-rsa/blob/master/src/integrations/passport.js#L44

(4) The function in authenticate of strategy.js of passport-jwt is called as a callback function, and verify.js of jsonwebtoken is called.

(5) If the validity period has expired in verify.js of jsonwebtoken, the callback function is called.

(6) A function within the function in authenticate of strategy.js of passport-jwt is called as a callback function and starts error processing.

(7) handleRequest() of AuthGuard in @nestjs/passport handleRequest(null, false,...) and throws UnauthorizedException

(8) The exception is thrown up to (3), and the exception is thrown in then(), so the exception is thrown up to the top level and the process is terminated.
https://github.com/auth0/node-jwks-rsa/blob/master/src/integrations/passport.js#L44

I think it will work correctly if the exception is properly handled in place of (8)

@adamjmcgrath
Copy link
Contributor

Hi @m-takata - thanks for raising this

I'm not too familiar with Nest.js - but I'm not sure extending the Passport strategy would be the easiest way to go.

Have a look at one of our example applications api_nestjs_typescript_hello-world - which creates an auth guard to protect api's using jwks-rsa

@m-takata
Copy link
Author

m-takata commented Apr 7, 2022

Hi @adamjmcgrath.

I referred to this article.
https://auth0.com/blog/developing-a-secure-api-with-nestjs-adding-authorization/

I think this article is also an official Auth0 article, but on the other hand, can the above SAMPLE and this article do the same thing?

I am not familiar with Passport itself either, so I don't understand the difference.

@m-takata
Copy link
Author

m-takata commented Apr 8, 2022

As a bug, can you please fix it?

@m-takata
Copy link
Author

Fixing AuthGuard solved the problem.
Thank you very much.

before

export class JwtAuthGuard extends AuthGuard('jwt') {
  canActivate(context: ExecutionContext): boolean {
    if (!super.canActivate(context)) {
      return false;
    }

after

export class JwtAuthGuard extends AuthGuard('jwt') {
async canActivate(context: ExecutionContext): Promise<boolean> {
    const canActivateSuper = super.canActivate(context);
    if (
      (canActivateSuper instanceof Promise && !(await canActivateSuper)) ||
      (canActivateSuper instanceof Observable &&
        !(await firstValueFrom(canActivateSuper))) ||
      (typeof canActivateSuper === 'boolean' && !canActivateSuper)
    )
      return false;

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

Successfully merging a pull request may close this issue.

2 participants