Apollo подписки JWT аутентификация - PullRequest
1 голос
/ 04 февраля 2020

Я использую полный шаблон Robin Wieruch, но для подписок отсутствует аутентификация. Он использует токен JWT для сеансов и работает нормально для http, но для ws auth полностью отсутствует.

Мне также нужно передать пользовательский контекст через подписки, мне нужна информация о сеансе в решателе подписок, чтобы иметь возможность решить погоду я должен уволить подписку или нет.

Я выполнил поиск в документации Apollo, я увидел, что должен использовать функцию onConnect: (connectionParams, webSocket, context), но нет полнофункционального примера с полным стеком, я не уверен, как передать JWT из клиента, чтобы получить его в объекте webSocket ,

Вот что у меня есть:

Сервер:

import express from 'express';
import {
  ApolloServer,
  AuthenticationError,
} from 'apollo-server-express';

const app = express();

app.use(cors());

const getMe = async req => {
  const token = req.headers['x-token'];

  if (token) {
    try {
      return await jwt.verify(token, process.env.SECRET);
    } catch (e) {
      throw new AuthenticationError(
        'Your session expired. Sign in again.',
      );
    }
  }
};

const server = new ApolloServer({
  introspection: true,
  typeDefs: schema,
  resolvers,
  subscriptions: {
    onConnect: (connectionParams, webSocket, context) => {
      console.log(webSocket);
    },
  },
  context: async ({ req, connection }) => {
    // subscriptions
    if (connection) {
      return {
        // how to pass me here as well?
        models,
      };
    }

    // mutations and queries
    if (req) {
      const me = await getMe(req);

      return {
        models,
        me,
        secret: process.env.SECRET,
      };
    }
  },
});

server.applyMiddleware({ app, path: '/graphql' });

const httpServer = http.createServer(app);
server.installSubscriptionHandlers(httpServer); 

const isTest = !!process.env.TEST_DATABASE_URL;
const isProduction = process.env.NODE_ENV === 'production';
const port = process.env.PORT || 8000;


httpServer.listen({ port }, () => {
    console.log(`Apollo Server on http://localhost:${port}/graphql`);
});

Клиент:


const httpLink = createUploadLink({
  uri: 'http://localhost:8000/graphql',
  fetch: customFetch,
});

const wsLink = new WebSocketLink({
  uri: `ws://localhost:8000/graphql`,
  options: {
    reconnect: true,
  },
});

const terminatingLink = split(
  ({ query }) => {
    const { kind, operation } = getMainDefinition(query);
    return (
      kind === 'OperationDefinition' && operation === 'subscription'
    );
  },
  wsLink,
  httpLink,
);

const authLink = new ApolloLink((operation, forward) => {
  operation.setContext(({ headers = {} }) => {
    const token = localStorage.getItem('token');

    if (token) {
      headers = { ...headers, 'x-token': token };
    }

    return { headers };
  });

  return forward(operation);
});

const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path }) => {
      console.log('GraphQL error', message);

      if (message === 'UNAUTHENTICATED') {
        signOut(client);
      }
    });
  }

  if (networkError) {
    console.log('Network error', networkError);

    if (networkError.statusCode === 401) {
      signOut(client);
    }
  }
});

const link = ApolloLink.from([authLink, errorLink, terminatingLink]);

const cache = new InMemoryCache();

const client = new ApolloClient({
  link,
  cache,
  resolvers,
  typeDefs,
});

1 Ответ

1 голос
/ 11 февраля 2020

Вам необходимо использовать connectionParams , чтобы установить JWT со стороны клиента. Ниже приведен фрагмент кода с использованием angular framework:

    const WS_URI = `wss://${environment.HOST}:${environment.PORT}${
      environment.WS_PATH
    }`;

    const wsClient = subscriptionService.getWSClient(WS_URI, {
      lazy: true,
      // When connectionParams is a function, it gets evaluated before each connection.
      connectionParams: () => {
        return {
          token: `Bearer ${authService.getJwt()}`
        };
      },
      reconnect: true,
      reconnectionAttempts: 5,
      connectionCallback: (error: Error[]) => {
        if (error) {
          console.log(error);
        }

        console.log('connectionCallback');
      },
      inactivityTimeout: 1000
    });

    const wsLink = new WebSocketLink(wsClient);

На вашей стороне сервера вы правы, используя обработчик событий onConnect для обработки JWT. Например,

  const server = new ApolloServer({
    typeDefs,
    resolvers,
    context: contextFunction,
    introspection: true,
    subscriptions: {
      onConnect: (
        connectionParams: IWebSocketConnectionParams,
        webSocket: WebSocket,
        connectionContext: ConnectionContext,
      ) => {
        console.log('websocket connect');
        console.log('connectionParams: ', connectionParams);
        if (connectionParams.token) {
          const token: string = validateToken(connectionParams.token);
          const userConnector = new UserConnector<IMemoryDB>(memoryDB);
          let user: IUser | undefined;
          try {
            const userType: UserType = UserType[token];
            user = userConnector.findUserByUserType(userType);
          } catch (error) {
            throw error;
          }

          const context: ISubscriptionContext = {
            // pubsub: postgresPubSub,
            pubsub,
            subscribeUser: user,
            userConnector,
            locationConnector: new LocationConnector<IMemoryDB>(memoryDB),
          };

          return context;
        }

        throw new Error('Missing auth token!');
      },
      onDisconnect: (webSocket: WebSocket, connectionContext: ConnectionContext) => {
        console.log('websocket disconnect');
      },
    },
  });

на стороне сервера: https://github.com/mrdulin/apollo-graphql-tutorial/blob/master/src/subscriptions/server.ts#L72

на стороне клиента: https://github.com/mrdulin/angular-apollo-starter/blob/master/src/app/graphql/graphql.module.ts#L38

...