Search code examples
typescriptexpressasynchronousjwtnestjs

@nestjs - JwtModule.registerAsync not Registering in Time, Cant Resolve Dependencies


Having a little trouble with the registerAsync functionality and JwtModule+JwtService. I looked through a lot of the other threads on this and seems like everyone was hung up on ConfigModule, but I am not using ConfigModule in this project.

Let me start with the set-up:

// secret-key.helper.ts

/*
  Retrieve the JWT secret value from AWS Secrets Manager. This is used to
  sign JWTs. Dont worry about imports, they're there.
*/
async function retrieveJwtSecret(): Promise<string> {
  const secretClient = new SecretsManagerClient({
    region: process.env.AWS_REGION,
  });
  const getSecretCom = new GetSecretValueCommand({
    SecretId: process.env.JWT_SECRET_ARN,
  });
  const jwtSecret = (await secretClient.send(getSecretCom)).SecretString;
  return jwtSecret;
}
// auth.module.ts

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtModule } from '@nestjs/jwt';
import { retrieveJwtSecret } from '../../utils/secret-key.helper';

@Module({
  imports: [
    JwtModule.registerAsync({
      useFactory: async () => ({
        global: true,
        secret: await retrieveJwtSecret(),
        signOptions: { expiresIn: '16h' },
      }),
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}
// my.guard.ts

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Request } from 'express';
import { logEvent } from '../log-event.helper';
import { JwtService } from '@nestjs/jwt';
import { JwtPayloadDto } from 'src/models/auth/dto/jwt-payload.dto';

@Injectable()
export class MyGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}
  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const req = context.switchToHttp().getRequest();

    /* JWT Authentication */
    const token = this.extractTokenFromHeader(req);
    if (!token) {
      logEvent('ERROR: No token found.', req.headers);
      return false;
    } else {
      const payload = this.jwtService.decode(token) as JwtPayloadDto;
      logEvent('Token found.', payload);
      return true;
    }
  }
}

So the first method is how I retrieve my JWT secret key from AWS secret manager. The second code block is the auth module showing the registerAsync global import and the usage of the retrieveJwtSecret() method (this is what is causing problems). The third code block is the guard I am using.

The first module where Nest runs into the Guard being imported/used returns a "can't resolve dependencies" error. When I switch away from registerAsync to register, pull the secret key in main.ts and place it in process.env manually, everything runs fine.

The exact error is:

ERROR [ExceptionHandler] Nest can't resolve dependencies of the MyGuard (?).
Please make sure that the argument JwtService at index [0] is available in the BenefitsModule context.

Potential solutions: ...

Through the use of console.log, I've found that execution begins in the retrieveJwtSecret function and that at run time, it has access to the .env variables necessary. However, it never fully completes the const jwtSecret = await secretClient.send... call, and my guess is that Nest continues to load modules that require JwtModule before Nest receives the secret and finishes setting up JwtModule.

I'd like to stay away from the register implementation I mentioned, if possible. Is there a way to force Nest to wait for async dependencies to be await-ed on?

Thanks!


Solution

  • The global option of JwtModule.registerAsync should be on the same level as useFactory, not a part of the options returned by the useFactory. So your module should be:

    import { Module } from '@nestjs/common';
    import { AuthService } from './auth.service';
    import { AuthController } from './auth.controller';
    import { JwtModule } from '@nestjs/jwt';
    import { retrieveJwtSecret } from '../../utils/secret-key.helper';
    
    @Module({
      imports: [
        JwtModule.registerAsync({
          global: true,
          useFactory: async () => ({
            secret: await retrieveJwtSecret(),
            signOptions: { expiresIn: '16h' },
          }),
        }),
      ],
      controllers: [AuthController],
      providers: [AuthService],
    })
    export class AuthModule {}
    

    This will make the JwtModule actually global.