Guard для подписок GraphQL на NestJS

В чём проблема

Вообщем решил настроить jwt авторизацию для GraphQL на Nest.js, но столкнулся с такой проблемой, что для подписок (subscriptions) guard не работает, поскольку req - не определён, оно и понятно, потому что подписки используют websocket.

Я изучил следующий материал, но так и не смог решить проблему. Вот мой конфиг GraphQL на данный момент:

GraphQLModule.forRoot<ApolloDriverConfig>({
  driver: ApolloDriver,
  playground: false,
  autoSchemaFile: 'src/graphql/generated/schema.gql',
  subscriptions: {
    'graphql-ws': true,
    'subscriptions-transport-ws': true,
  },
  plugins: [ApolloServerPluginLandingPageLocalDefault()],
}),

Собрал его из офф. документации Nest Ну а вот и сам guard

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
  }
}

Дефолтный, тоже с офф. документации

Проверяю всё в Apollo Explorer, там при подписке получаю это

TypeError: Cannot read properties of undefined (reading 'logIn') # req is undefined

Без 'subscriptions-transport-ws' Apollo отказывается работать, так, что настраивать guard нужно под него, но лучше для обоих.

Что я пробовал

Может быть для GraphQL без Apollo это сработало бы, но увы, ошибка та же

@Module({
  imports: [
    AuthModule,
    UsersModule,
    // chaincode modules
    ParticipantModule,
    PersonModule,
    CauseModule,
    TransactionModule,
    // apolloServer config: use forRootAsync to import AuthModule and inject AuthService
    GraphQLModule.forRootAsync({
      // import AuthModule
      imports: [AuthModule],
      // inject authService
      useFactory: async (authService: AuthService) => ({
        debug: true,
        playground: true,
        installSubscriptionHandlers: true,
        autoSchemaFile: 'schema.gql',
        // pass the original req and res object into the graphql context,
        // get context with decorator `@Context() { req, res, payload, connection }: GqlContext`
        // req, res used in http/query&mutations, connection used in webSockets/subscriptions
        context: ({ req, res, payload, connection }: GqlContext) => ({ req, res, payload, connection }),
        // configure graphql cors here
        cors: {
          origin: e.corsOriginReactFrontend,
          credentials: true,
        },
        // subscriptions/webSockets authentication
        subscriptions: {
          // get headers
          onConnect: (connectionParams: ConnectionParams) => {
            // convert header keys to lowercase
            const connectionParamsLowerKeys = mapKeysToLowerCase(connectionParams);
            // get authToken from authorization header
            const authToken: string = ('authorization' in connectionParamsLowerKeys)
              && connectionParamsLowerKeys.authorization.split(' ')[1];
            if (authToken) {
              // verify authToken/getJwtPayLoad
              const jwtPayload: GqlContextPayload = authService.getJwtPayLoad(authToken);
              // the user/jwtPayload object found will be available as context.currentUser/jwtPayload in your GraphQL resolvers
              return { currentUser: jwtPayload.username, jwtPayload, headers: connectionParamsLowerKeys };
            }
            throw new AuthenticationError('authToken must be provided');
          },
        },
      }),
      // inject: AuthService
      inject: [AuthService],
    }),
  ],
})

guard

@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {

  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);
    // req used in http queries and mutations, connection is used in websocket subscription connections, check AppModule
    const { req, connection } = ctx.getContext();

    // if subscriptions/webSockets, let it pass headers from connection.context to passport-jwt
    return (connection && connection.context && connection.context.headers)
      ? connection.context
      : req;
  }
}

Ответы (1 шт):

Автор решения: LIMPIX64

Я немного поэкспериментировал и нашёл идеальное и простое для меня решение. Не знаю, на сколько оно правильное и оптимальное, но в Apollo Explorer я без проблем могу использовать заголовок авторизации, что насчёт реального клиента, то я дополню ответ, если что-то изменится. Я использую NestJS + Fastify и Apollo, так что вероятно это решение не универсальное.

GraphQLModule

GraphQLModule.forRoot<ApolloDriverConfig>({
  driver: ApolloDriver,
  playground: false,
  autoSchemaFile: 'src/graphql/generated/schema.gql',
  subscriptions: {
    'graphql-ws': true,
    'subscriptions-transport-ws': {
      onConnect: params => ({ headers: toLowerKeys(params) /* Authorization -> authorization */ }),
    },
  },
  plugins: [ApolloServerPluginLandingPageLocalDefault()],
}),

Guard

@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context)
    const { req, headers } = ctx.getContext()
    return req ?? { headers }
  }
}
→ Ссылка