Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stitching secure subscriptions using makeRemoteExecutableSchema

We have implemented schema stitching where GraphQL server fetches schema from two remote servers and stitches them together. Everything was working fine when we were only working with Query and Mutations, but now we have a use-case where we even need to stitch Subscriptions and remote schema has auth implemented over it.

We are having a hard time figuring out on how to pass authorization token received in connectionParams from client to remote server via the gateway.

This is how we are introspecting schema:

API Gateway code:

const getLink = async(): Promise<ApolloLink> => {
const http = new HttpLink({uri: process.env.GRAPHQL_ENDPOINT, fetch:fetch})

const link = setContext((request, previousContext) => {
    if (previousContext
        && previousContext.graphqlContext
        && previousContext.graphqlContext.request
        && previousContext.graphqlContext.request.headers
        && previousContext.graphqlContext.request.headers.authorization) {
        const authorization = previousContext.graphqlContext.request.headers.authorization;
        return {
            headers: {
                authorization
            }
        }
    }
    else {
        return {};
    }
}).concat(http);

const wsLink: any = new WebSocketLink(new SubscriptionClient(process.env.REMOTE_GRAPHQL_WS_ENDPOINT, {
    reconnect: true,
    // There is no way to update connectionParams dynamically without resetting connection
    // connectionParams: () => { 
    //     return { Authorization: wsAuthorization }
    // }
}, ws));


// Following does not work
const wsLinkContext = setContext((request, previousContext) => {
    let authToken = previousContext.graphqlContext.connection && previousContext.graphqlContext.connection.context ? previousContext.graphqlContext.connection.context.Authorization : null
    return {
        context: {
            Authorization: authToken
        }
    }
}).concat(<any>wsLink);

const url = split(({query}) => {
    const {kind, operation} = <any>getMainDefinition(<any>query);
    return kind === 'OperationDefinition' && operation === 'subscription'
},
wsLinkContext,
link)

return url;
}

const getSchema = async (): Promise < GraphQLSchema > => {
  const link = await getLink();
  return makeRemoteExecutableSchema({
    schema: await introspectSchema(link),
    link,
  });
}
const linkSchema = `
  extend type UserPayload {
    user: User
  }
`;
const schema: any = mergeSchemas({
  schemas: [linkSchema, getSchema],
});
const server = new GraphQLServer({
  schema: schema,
  context: req => ({
    ...req,
  })
});

Is there any way for achieving this using graphql-tools? Any help appreciated.

like image 846
Niks Avatar asked Sep 05 '25 02:09

Niks


1 Answers

I have one working solution: the idea is to not create one instance of SubscriptionClient for the whole application. Instead, I'm creating the clients for each connection to the proxy server:

server.start({
    port: 4000,
    subscriptions: {
      onConnect: (connectionParams, websocket, context) => {
        return {
          subscriptionClients: {
            messageService: new SubscriptionClient(process.env.MESSAGE_SERVICE_SUBSCRIPTION_URL, {
              connectionParams,
              reconnect: true,
            }, ws)
          }
        };
      },
      onDisconnect: async (websocket, context) => {
        const params = await context.initPromise;
        const { subscriptionClients } = params;
        for (const key in subscriptionClients) {
          subscriptionClients[key].close();
        }
      }
    }
  }, (options) => console.log('Server is running on http://localhost:4000'))

if you would have more remote schemas you would just create more instances of SubscriptionClient in the subscriptionClients map.

To use those clients in the remote schema you need to do two things:

  1. expose them in the context:

    const server = new GraphQLServer({
      schema,
      context: ({ connection }) => {
        if (connection && connection.context) {
          return connection.context;
        }
      }
    });
    
  2. use custom link implementation instead of WsLink

    (operation, forward) => {
        const context = operation.getContext();
        const { graphqlContext: { subscriptionClients } } = context;
        return subscriptionClients && subscriptionClients[clientName] && subscriptionClients[clientName].request(operation);
    };
    

In this way, the whole connection params will be passed to the remote server.

The whole example can be found here: https://gist.github.com/josephktcheung/cd1b65b321736a520ae9d822ae5a951b

Disclaimer:

The code is not mine, as @josephktcheung outrun me with providing an example. I just helped with it a little. Here is the original discussion: https://github.com/apollographql/graphql-tools/issues/864

like image 138
gandalfml Avatar answered Sep 07 '25 19:09

gandalfml