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 шт):
Я немного поэкспериментировал и нашёл идеальное и простое для меня решение. Не знаю, на сколько оно правильное и оптимальное, но в 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 }
}
}