Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pass in first parameter of onOperation to the subscriptionServer context function #1505

Closed
clayne11 opened this issue Aug 8, 2018 · 8 comments
Labels
📚 good-first-issue Issues that are more approachable for first-time contributors.

Comments

@clayne11
Copy link
Contributor

clayne11 commented Aug 8, 2018

There have been some pretty longstanding issues about how to do authentication for subscriptions (apollographql/apollo-link#197 (comment)).

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 apollo-server@2.0.0.

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.

@ysantalla
Copy link

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

@clayne11
Copy link
Contributor Author

clayne11 commented Sep 6, 2018

@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,
  },
})

@ysantalla
Copy link

Thanks, I was missing the payload parameter in the function

@JayBee007
Copy link

JayBee007 commented Oct 3, 2018

@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!

@clayne11
Copy link
Contributor Author

clayne11 commented Oct 3, 2018

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.

@sulliwane
Copy link

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 ?

@jjangga0214
Copy link

jjangga0214 commented Jan 31, 2020

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

@nemanjam
Copy link

nemanjam commented Feb 16, 2020

Server is receiving one token from multiple clients.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
📚 good-first-issue Issues that are more approachable for first-time contributors.
Projects
None yet
Development

No branches or pull requests

6 participants