GraphQL subscription, websocket, nodejs, Express session

Viewed 1763

I'm using Apollo Server 2 (but this problem is not only with Apollo) 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!).

Where am I doing wrong?

4 Answers

I know this old but ran into this issue myself and how i solved it was I ran webSocket.upgradeReq.session through my session middleware.

enter image description here

Then in onConnect:

enter image description here

And you have access to your session

This is what ! use

  //Session middleware before resolvers
  const sessionParser = session({
    store,
    secret: config.get('keys.session_secret'),
    //...The rest config here
    }
  });
  const apolloServer = new ApolloServer({
    schema,
    context: ({req, res, connection}: TheContext): TheContext => {
      return {req, res, redisStore: store, connection}; //connection is must here
    },
    subscriptions: {
      onConnect: (_params, _ws, ctx) => {
        return new Promise((resolve, reject) => {
          const req = ctx.request as express.Request;
          const res = ({} as any) as express.Response;

          //Parsing session
          sessionParser(req, res, (_: any) => {
            const session = req.session as any;

            //Getting userId which was stored during login
            const userId = session.userId;

            if (!userId) {
              reject(new AppError('You are not logged in'));
            }

            //This will be available in subscription via connection as
            //something.connection.context.userId
            resolve({userId});
          });
        });
      }
    }
  });

And I use with TypeGraphql like this

  @Subscription(() => Post, {
    topics: ({context}) => `${Topic.DELETED_POST}:${parseUserId(context)}`,
    description: 'To be used in requests list and not for individual request'
  })
  deletedPost(@Root() post: Post) {
    return post;
  }

This is how I get access to userId which was stored in session

export function parseUserId(obj: any): string {
  return obj.connection.context.userId ?? '';
}

Credit to all answers here

See this comment: https://github.com/expressjs/cookie-session/issues/117#issuecomment-452046225 by dougwilson to get the session values.

If you just want to get the session contents (and not write back a new session -- which would not be possible without a res anyhow), you could likely just use the cookies module directly, which is what this module uses under the hood to read the cookie and validate the signature. A possible example:

const Cookies = require('cookies')

// …

const cookies = new Cookies(req, null, { keys }) const session =
JSON.parse(Buffer.from(cookies.get('session'),'base64').toString('utf8'))

And onConnect return whatever you need to use on subscribe()

Related