nestjs + Passport + GqlAuthGuard produces Cannot read property 'logIn' of undefined

1.5k views Asked by At

I have followed the nestjs example of how to integrate passport with apollo but it constantly crashes with the following exception when I call a guarded resolver.

Looking into it in detail when the @nestjs/passport auth-guard class is extended, it does not call the getRequest function of the child class, instead it calls the pre-existing one in the class (as if inheritance never took place)

[Nest] 25029  - 05/26/2022, 8:29:40 PM   ERROR [ExceptionsHandler] Cannot read property 'logIn' of undefined
TypeError: Cannot read property 'logIn' of undefined
    at authenticate (.../node_modules/passport/lib/middleware/authenticate.js:96:21)
    at ...node_modules/@nestjs/passport/dist/auth.guard.js:96:3
    at new Promise (<anonymous>)
    at ...node_modules/@nestjs/passport/dist/auth.guard.js:88:83
    at JwtAuthGuard.<anonymous> (...node_modules/@nestjs/passport/dist/auth.guard.js:49:36)
    at Generator.next (<anonymous>)
    at fulfilled (.../node_modules/@nestjs/passport/dist/auth.guard.js:17:58)
    at processTicksAndRejections (internal/process/task_queues.js:93:5)

I have the following setup

import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GqlExecutionContext } from '@nestjs/graphql';

@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);
    return ctx.getContext().req;
  }
}
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private readonly reflector: Reflector) {
    super();
  }
  canActivate(context: ExecutionContext) {
    const isGuestAllowed = this.reflector.getAllAndOverride<boolean>(IS_GUEST_ALLOWED_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (isGuestAllowed) {
      return true;
    }

    // Add your custom authentication logic here
    // for example, call super.login(request) to establish a session.
    return super.canActivate(context);
  }

  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;
  }
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: JwtConstants.secret,
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: `.env.${process.env.NODE_ENV}`,
    }),
    TypeOrmModule.forRoot(),
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      debug: true,
      playground: true,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      installSubscriptionHandlers: true,
      context: ({ req }) => ({ req }),
    }),
    RecipesModule,
    AuthModule,
    UsersModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
@Resolver((of) => Recipe)
export class RecipesResolver {
  constructor(private readonly recipesService: RecipesService) {}

  @UseGuards(GqlAuthGuard)
  @Query((returns) => Recipe)
  async recipe(@CurrentUser() user: any, @Args('id') id: string): Promise<Recipe> {
    const recipe = await this.recipesService.findOneById(id);
    if (!recipe) {
      throw new NotFoundException(`Recipe with ID "${id}" not found`);
    }

    return recipe;
  }
}

Using the following package versions.

"dependencies": {
    "@nestjs/apollo": "^10.0.12",
    "@nestjs/common": "^8.0.0",
    "@nestjs/config": "^2.0.1",
    "@nestjs/core": "^8.0.0",
    "@nestjs/graphql": "^10.0.12",
    "@nestjs/jwt": "^8.0.1",
    "@nestjs/passport": "^8.2.1",
    "@nestjs/platform-express": "^8.0.0",
    "@nestjs/typeorm": "^8.0.4",
    "apollo-server-express": "^3.8.0",
    "class-transformer": "^0.5.1",
    "class-validator": "^0.13.2",
    "cross-env": "^7.0.3",
    "graphql": "^16.5.0",
    "graphql-query-complexity": "^0.11.0",
    "graphql-subscriptions": "^2.0.0",
    "passport": "^0.6.0",
    "passport-local": "^1.0.0",
    "pg": "^8.7.3",
    "reflect-metadata": "^0.1.13",
    "rimraf": "^3.0.2",
    "rxjs": "^7.2.0",
    "typeorm": "^0.3.6",
    "uuid": "^8.3.2"
  },
2

There are 2 answers

0
ptheofan On

The issue was making the JwtGuard global (not visible in the source code above). It was getting queued before the JqlGuard which it would seem as if my getRequest was never called (was running a different class but being js there was no clear indication as to which class is actually running).

1
max-lt On

I've got the same problem with a custom GraphQL driver. I forgot to add a setter for context and to override getRequest in global JwtAuthGuard :

import { Module } from '@nestjs/common';
import { GraphQLModule as NestGraphQLModule } from '@nestjs/graphql';
import { AbstractGraphQLDriver, GqlModuleOptions } from '@nestjs/graphql';
import { createHandler } from 'graphql-http/lib/use/express';
import { TestResolver } from './resolvers/test';

class GraphQLDriver extends AbstractGraphQLDriver {
  async start(options: GqlModuleOptions): Promise<void> {
    const { schema } = await this.graphQlFactory.mergeWithSchema(options);

    const { httpAdapter } = this.httpAdapterHost;

    httpAdapter.use(
      '/api/graphql',
      createHandler({
        schema,
        context(req, params) { // <-- Context was missing
          return { req, params };
        }
      })
    );
  }

  async stop() {}
}

@Module({
  imports: [
    NestGraphQLModule.forRoot<GqlModuleOptions>({
      driver: GraphQLDriver,
      typePaths: ['../types/graphql/**/*.gql']
    })
  ],
  providers: [TestResolver]
})
export class GraphQLModule {}
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GqlContextType, GqlExecutionContext } from '@nestjs/graphql';

@Injectable()
export class JwtAuthGuard extends AuthGuard('access-token') {
  constructor(private reflector: Reflector) {
    super();
  }

  // Must override getRequest to handle graphql context type
  override getRequest(context: ExecutionContext) {
    switch (context.getType<GqlContextType>()) {
      case 'graphql':
        const ctx = GqlExecutionContext.create(context);
        return ctx.getContext().req;
      default: // 'http' | 'ws' | 'rpc'
        return context.switchToHttp().getRequest();
    }
  }
}