Apollo-server: Session in webSocket subscriptions

Created on 11 Sep 2018  路  7Comments  路  Source: apollographql/apollo-server

I'm using Apollo Server 2 and Express.js vanilla (with apollo-server-express).

Everything works good also with Subscriptions except the Express session mechanism.

The problem:

I'm using cookie-session (https://github.com/expressjs/cookie-session, but I think this is the same for express-session middleware) and when my browser start a new connection with my server the ApolloServer onConnect hook doesn't have the req attribute and neither req.session and so on...

What I can do is to parse the cookies from webSocket.upgradeReq.headers.cookie in onConnect lifecycle hook, but it seems to me very hacky.

The code:

const { ApolloServer } = require('apollo-server-express')

const typeDefs = require('../src/graphql/types')
const resolvers = require('../src/graphql/resolvers')
const models = require('../src/models')

const apolloServer = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req, connection }) => {
    // connection exists only on webSocket connection
    if (connection) {
      return {
        currentUser: connection.context.currentUser // <-- I NEED THIS!
      }
    }
    // if not a (webSocket) connection it is a "default" HTTP call
    return {
      models,
      currentUser: { id: req.user.id }
    }
  },
  subscriptions: {
    onConnect: (connectionParams, webSocket) => {
      // "connectionParams" is from the client but I cannot use it because cookies are HTTP-Only
      // I can retrieve cookies from here: "webSocket.upgradeReq.headers.cookie" but then I need to parse them which seems a bit hacky to me
      // return { currentUser: req.user.id } // <-- I NEED THIS (req.user.id doesn't exists)!
    }
  }
})

module.exports = apolloServer

I can't find anything on Apollo Server Docs site (for other topics very well documented! Just this: https://www.apollographql.com/docs/apollo-server/features/subscriptions.html#Context-with-Subscriptions).

Where am I doing wrong?

StackOverflow question: https://stackoverflow.com/questions/52280481/graphql-subscription-websocket-nodejs-express-session-with-apollo-server-2

bug documentation has-reproduction 馃К subscriptions

Most helpful comment

The way i found to deal with this using express session is doing something like this:

subscriptions: {
    onConnect: async (connectionParams, webSocket) => {
      const wsSession = await new Promise(resolve => {
        // use same session parser as normal gql queries
        session(webSocket.upgradeReq, {}, () => {
          if (webSocket.upgradeReq.session) {
            resolve(webSocket.upgradeReq.session);
          }
          return false;
        });
      });
      // We have a good session. attach to context
      if (wsSession.userId) {
        return { session: wsSession };
      }
      // throwing error rejects the connection
      throw new Error('Missing auth token!');
    },
 }

All 7 comments

Any updates on this?

Any news?

The way i found to deal with this using express session is doing something like this:

subscriptions: {
    onConnect: async (connectionParams, webSocket) => {
      const wsSession = await new Promise(resolve => {
        // use same session parser as normal gql queries
        session(webSocket.upgradeReq, {}, () => {
          if (webSocket.upgradeReq.session) {
            resolve(webSocket.upgradeReq.session);
          }
          return false;
        });
      });
      // We have a good session. attach to context
      if (wsSession.userId) {
        return { session: wsSession };
      }
      // throwing error rejects the connection
      throw new Error('Missing auth token!');
    },
 }

@adavia Where is your session variable coming from?

In

session(webSocket.upgradeReq, {}, () => {
  if (webSocket.upgradeReq.session) {
    resolve(webSocket.upgradeReq.session);
  }
  return false;
 });

EDIT: nvm, figured it out, comes from

import session from "express-session";

const mySession = session({
      store: new SomeStore(),
      resave: false,
      secret: "mysecret",
      saveUninitialized: false,
    }),

basically your session is the intialized session();

session support is pretty bad, if you use passport it makes you add a bunch of get requests, but graphql is built around POST, unclear how to mix the two

The way i found to deal with this using express session is doing something like this:

subscriptions: {
    onConnect: async (connectionParams, webSocket) => {
      const wsSession = await new Promise(resolve => {
        // use same session parser as normal gql queries
        session(webSocket.upgradeReq, {}, () => {
          if (webSocket.upgradeReq.session) {
            resolve(webSocket.upgradeReq.session);
          }
          return false;
        });
      });
      // We have a good session. attach to context
      if (wsSession.userId) {
        return { session: wsSession };
      }
      // throwing error rejects the connection
      throw new Error('Missing auth token!');
    },
 }

I get the following warnings as a result of including the code snippet provided by @adavia. I'm not sure how to add the missing options as I don't really understand what's going on in the code. Could I please get some guidance?

express-session deprecated undefined resave option; provide resave option server\server.js:95:66                                                                          er.js:95:66
express-session deprecated undefined saveUninitialized option; provide saveUninitialized option server\server.js:95:66zed option server\server.js:95:66
express-session deprecated req.secret; provide secret option server\server.js:95:66 

Probably because of webSocket.upgradeReq.session.
express-session middleware is attached on the request.

Here is my implementation.

import session from 'express-session';

.......

const mySession = session(sessionConf);
app.use(mySession);

......

subscriptions: {
      onConnect: async (connectionParams, webSocket, { request }) => {
        const subscriptionContext: SubscriptionContext = await new Promise(res => {
          mySession(request, {}, () => {
            const userSession: UserSession = (request?.session as UserSession).user;
            res(
              userSession
                ? ({
                    req: request,
                    user: userSession,
                  } as SubscriptionContext)
                : null
            );
          });
        });
        if (!subscriptionContext) {
          throw new AuthenticationError('Unauthorized');
        }
        return subscriptionContext;
      },
    },

    context: ({ req, res, connection }) => {
      const subscriptionContext: SubscriptionContext = connection?.context;
      const ctx: MyContext = subscriptionContext
        ? {
            req: subscriptionContext.req,
            user: subscriptionContext.user,
            res,
          }
        : {
            req,
            user: req.session?.user,
            res,
          };

      return ctx;
    },
Was this page helpful?
0 / 5 - 0 ratings