NestJS GraphQL Unable to Authenticate GraphQL subscription with graphql-ws and apollo client

75 Views Asked by At

I am using Nestjs API with @nestjs/graphql. On a Next.js front I am trying to subscribe to an authenticated GraphQL subscription, it works with WebSocketLink but not GraphQLWsLink.

My resolver is

@Subscription(() => Post)
@Roles(UserRole.USER)
@UseGuards(JwtAuthGuard, RolesGuard)
postCreated() {
    return this.pubSub.asyncIterator('postCreated')
}

My GraphQL config is

@Injectable()
export class GqlConfigService implements GqlOptionsFactory {
  constructor(private configService: ConfigService) {}
  createGqlOptions(): ApolloDriverConfig {
    const graphqlConfig = this.configService.get<GraphqlConfig>('graphql')
    return {
      // schema options
      autoSchemaFile: graphqlConfig.schemaDestination,
      sortSchema: graphqlConfig.sortSchema,
      buildSchemaOptions: {
        numberScalarMode: 'integer',
      },
      subscriptions: {
        'graphql-ws': {
          path: '/graphql',
          onConnect: (context: Context) => {
            const { connectionParams } = context
            return {
              req: {
                headers: { authorization: connectionParams.Authorization },
              },
            }
          },
        },
        'subscriptions-transport-ws': {
          path: '/graphql',
          onConnect: (connectionParams) => {
            return {
              req: {
                headers: { authorization: connectionParams.Authorization },
              },
            }
          },
        },
      },
      includeStacktraceInErrorResponses: graphqlConfig.debug,
      playground: graphqlConfig.playgroundEnabled,
      context: ({ req }) => ({ req }),
      // Error
      formatError: (error) => {
        return {
          message: error.message,
          code: error.extensions?.code || 'INTERNAL_SERVER_ERROR',
        }
      },
    }
  }
}

My AuthGuard

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

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context)
    return ctx.getContext().req
  }
}

And my Apollo client setup

import { getSession } from 'next-auth/react'
import { createClient } from 'graphql-ws'
import {
  ApolloClient,
  createHttpLink,
  InMemoryCache,
  split,
} from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
import { getMainDefinition } from '@apollo/client/utilities'
import { WebSocketLink } from '@apollo/client/link/ws'

import { config } from '@/config'

const token = 'A_VALID_TOKEN_GOES_HERE'

const wsLinkOld = new WebSocketLink({
  uri: 'ws://localhost:4000/graphql',
  options: {
    reconnect: true,
    connectionParams: {
      Authorization: token ? `Bearer ${token}` : '',
    },
  },
})

const wsLink = new GraphQLWsLink(
  createClient({
    url: 'ws://localhost:4000/graphql',
    connectionParams: () => ({
      authorization: `Bearer ${token}`,
    }),
  }),
)

const httpLink = createHttpLink({
  uri: config.api.graphql.url.href,
})

const authLink = setContext(async (_, { headers }) => {
  const session = await getSession()
  return {
    headers: {
      ...headers,
      authorization: session?.accessToken
        ? `Bearer ${session.accessToken}`
        : '',
    },
  }
})

const link = split(
  ({ query }) => {
    const definition = getMainDefinition(query)
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    )
  },
  wsLink,
  authLink.concat(httpLink),
)

const client = new ApolloClient({
  link,
  cache: new InMemoryCache(),
})

export default client

It works with

import { WebSocketLink } from '@apollo/client/link/ws'
const wsLinkOld = new WebSocketLink({
  uri: 'ws://localhost:4000/graphql',
  options: {
    reconnect: true,
    connectionParams: {
      Authorization: token ? `Bearer ${token}` : '',
    },
  },
})

But not with

import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
import { createClient } from 'graphql-ws'
const wsLink = new GraphQLWsLink(
  createClient({
    url: 'ws://localhost:4000/graphql',
    connectionParams: () => ({
      authorization: `Bearer ${token}`,
    }),
  }),
)

Everytime I got

TypeError: Cannot read properties of undefined (reading 'authorization')

2

There are 2 best solutions below

0
On

I think that problem with "connectionParams.Authorization".

You can rewrite from:

headers: { authorization: connectionParams.Authorization }

to

headers: { authorization: connectionParams.authorization }

and on client side from:

connectionParams: {
  Authorization: token ? `Bearer ${token}` : '',
}

to

connectionParams: {
  authorization: token ? `Bearer ${token}` : '',
}
0
On

Not sure why, but my graphql-ws req has headers in connectionParams like:

{
 ...
 connectionParams: {
    headers: {
      authorization: 'Bearer eyJhbGci...'
    }
  },
}

So I solved this by adding a condition in the JwtAuthGuard to pass the headers directly.

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

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context)
    const { req } = ctx.getContext()
    if (req.connectionParams?.headers) {
      req.headers = req.connectionParams.headers
    }
    return req
  }
}

and I removed onConnect and context override in

import { ConfigService } from '@nestjs/config'
import { ApolloDriverConfig } from '@nestjs/apollo'
import { Injectable } from '@nestjs/common'
import { GqlOptionsFactory } from '@nestjs/graphql'

import './graphql.enums'
import { GraphqlConfig } from 'src/config/config.interface'

@Injectable()
export class GqlConfigService implements GqlOptionsFactory {
  constructor(private configService: ConfigService) {}
  createGqlOptions(): ApolloDriverConfig {
    const graphqlConfig = this.configService.get<GraphqlConfig>('graphql')
    return {
      // schema options
      autoSchemaFile: graphqlConfig.schemaDestination,
      sortSchema: graphqlConfig.sortSchema,
      buildSchemaOptions: {
        numberScalarMode: 'integer',
      },
      subscriptions: {
        'graphql-ws': {
          path: '/graphql',
        },
        'subscriptions-transport-ws': {
          path: '/graphql',
          onConnect: (connectionParams) => {
            return {
              req: {
                headers: { authorization: connectionParams.Authorization },
              },
            }
          },
        },
      },
      includeStacktraceInErrorResponses: graphqlConfig.debug,
      playground: graphqlConfig.playgroundEnabled,
      // Error
      formatError: (error) => {
        return {
          message: error.message,
          code: error.extensions?.code || 'INTERNAL_SERVER_ERROR',
        }
      },
    }
  }
}

At least now I am able to use subscription with graphql-ws and subscriptions-transport-ws