Apollo-server: Pass in first parameter of onOperation to the subscriptionServer context function

Created on 8 Aug 2018  路  8Comments  路  Source: apollographql/apollo-server

There have been some pretty longstanding issues about how to do authentication for subscriptions (https://github.com/apollographql/apollo-link/issues/197#issuecomment-384581910).

If we pass the first param of onOperation in the subscription server then we can dynamically access any parameters we want that are passed by the SubscriptionClient.

The setup we use looks like this:

// client set up
const wsLink = new WebSocketLink({
  uri: websocketUrl,
  options: {
    reconnect: true,
  },
  webSocketImpl: ws,
})

const subscriptionMiddleware = {
  applyMiddleware: async (options, next) => {
    options.authToken = await getLoginToken()
    next()
  },
}

// add the middleware to the web socket link via the Subscription Transport client
wsLink.subscriptionClient.use([subscriptionMiddleware])
// server set up
const subscriptionServer = new SubscriptionServer(
  {
    keepAlive: 30000,
    execute,
    subscribe,
    schema,
    onOperation: async (message, params) => {
      const token = message.payload.authToken
      const context = await setupContext({token})

      return {
        ...params,
        context: {
          ...params.context,
          ...context,
        },
      }
    },
  },
  {
    server: httpServer,
    path: WEBSOCKET_PATH,
  }
)

The options from the client are passed as the message.payload on the server. As far as I can tell this is the easiest way to perform auth on the subscriptions. If we simply pass this payload in to the context function it would allow this pattern to work with [email protected].

The current code in apollo-server-core looks like this:

onOperation: async (_: string, connection: ExecutionParams) => {
  connection.formatResponse = (value: ExecutionResult) => ({
    ...value,
    errors:
      value.errors &&
      formatApolloErrors([...value.errors], {
        formatter: this.requestOptions.formatError,
        debug: this.requestOptions.debug,
      }),
  });
  let context: Context = this.context ? this.context : { connection };


  try {
    context =
      typeof this.context === 'function'
        ? await this.context({ connection })
        : context;

If we change it to something like this:

onOperation: async (message: any, connection: ExecutionParams) => {
  connection.formatResponse = (value: ExecutionResult) => ({
    ...value,
    errors:
      value.errors &&
      formatApolloErrors([...value.errors], {
        formatter: this.requestOptions.formatError,
        debug: this.requestOptions.debug,
      }),
  });
  let context: Context = this.context ? this.context : { connection };


  try {
    context =
      typeof this.context === 'function'
        ? await this.context({ connection, payload: message.payload })
        : context;

it should solve a problem for a lot of people.

馃摎 good-first-issue

Most helpful comment

@ysantalla I'm not sure exactly what you're asking but this has definitely been merged. I'm using it in my app like so:

// client
const wsLink = new WebSocketLink({
  uri: websocketUrl,
  options: {
    reconnect: true,
  },
  webSocketImpl: ws,
})

const subscriptionMiddleware = {
  applyMiddleware: async (options, next) => {
    options.authToken = await getLoginToken()
    next()
  },
}

// add the middleware to the web socket link via the Subscription Transport client
wsLink.subscriptionClient.use([subscriptionMiddleware])


// server
const server = new ApolloServer({
  schema,
  context: async ({req, payload}) => {
    const token = payload
      ? payload.authToken
      : getTokenFromRequest({request: req})
    return await setupContext({token})
  },
  subscriptions: {
    keepAlive: 30000,
    path: WEBSOCKET_PATH,
  },
})

All 8 comments

I checked the source code and saw that this feature is resolved, so the token does not reach the context???

@ysantalla I'm not sure exactly what you're asking but this has definitely been merged. I'm using it in my app like so:

// client
const wsLink = new WebSocketLink({
  uri: websocketUrl,
  options: {
    reconnect: true,
  },
  webSocketImpl: ws,
})

const subscriptionMiddleware = {
  applyMiddleware: async (options, next) => {
    options.authToken = await getLoginToken()
    next()
  },
}

// add the middleware to the web socket link via the Subscription Transport client
wsLink.subscriptionClient.use([subscriptionMiddleware])


// server
const server = new ApolloServer({
  schema,
  context: async ({req, payload}) => {
    const token = payload
      ? payload.authToken
      : getTokenFromRequest({request: req})
    return await setupContext({token})
  },
  subscriptions: {
    keepAlive: 30000,
    path: WEBSOCKET_PATH,
  },
})

Thanks, I was missing the payload parameter in the function

@clayne11 Thanks for showing this approach, but I couldn't figure out

const context = await setupContext({token})

is this just an internal function which adds token to the context? if yes, could you please elaborate? I didnt find anything in the documentation, except for setContext. Right now am doing in such a way

```
onOperation: (message, params, webSocket) => {
const token = message.payload.authToken;
if (token) {
const { id, email } = jwt.verify(token, process.env.JWT_KEY);
return {
...params,
context: {
...params.context,
user: { id, email },
},
};
}
return params;
}
````

is it the proper way?
Thanks!

setupContext is just an internal function to set up Dataloader caches and whatnot. What you've done looks reasonable. Once you have the auth token you can do whatever you need to with it.

could someone confirm that once the websocket channel has been opened (with Authorization header = token AAA), each subsequent request using the websocket link will always be identified as AAA token.

Or is there a way to send a different Authorization header on each request (other than re-opening another ws channel)?

I'd like to understand what's happening on a low level protocol for ws.

Thank you for you reply!

here is my code so far (working correctly with one token):

const wsClient = new SubscriptionClient(
  graphqlEndpoint,
  {
    reconnect: true,
    connectionParams: () => ({
      headers: {
        'Authorization': 'mytokenAAA',
      },
    }),
  },
  ws,
);
const link = new WebSocketLink(wsClient);

makePromise(execute(link, options)); // that's using token AAA
// how to make another query (execute) using token BBB without creating another link ?

@sulliwane

By this, you can create multiple websocket links, one per one client.

const wsLink = new ApolloLink(operation => {
    // This is your context!
    const context = operation.getContext().graphqlContext

    // Create a new websocket link per request
    return new WebSocketLink({
      uri: "<YOUR_URI>",
      options: {
        reconnect: true,
        connectionParams: { // give custom params to your websocket backend (e.g. to handles auth) 
          headers: {
            userId: context.user.id
            foo: 'bar'
          }
        },
      },
      webSocketImpl: ws,
    }).request(operation)
    // Instead of using `forward()` of Apollo link, we directly use websocketLink's request method
  })

Server is receiving one token from multiple clients.

Was this page helpful?
0 / 5 - 0 ratings