apollographql / graphql-subscriptions

:newspaper: A small module that implements GraphQL subscriptions for Node.js
MIT License
1.59k stars 133 forks source link

Authorizing subscriptions #90

Open altschuler opened 7 years ago

altschuler commented 7 years ago

I've got a foobarUpdated subscription, but I need to authorize the users who subscribe to it as well as validate the variable (if the given id is not a valid foobar id, subscription doesn't make sense).

Is there a way to deny/cancel a subscription? I'm using withFilter but that only works for filtering after the subscription has been initiated.

anhldbk commented 7 years ago

@altschuler I think it should be the responsibility of lower middlewares as pointed out by this article of @helfer. Btw, if you're using Hapi, hapi-nes provides subscription filters that you may be interested in.

anhldbk commented 7 years ago

@altschuler , @helfer : Would you please tell me what's the difference between withFilter and resolve functions. Are they the same?

altschuler commented 7 years ago

@anhldbk The problem is not where to do authorization, but rather that if a user subscribes to something that user is not authorized to, that subscription should not be allowed, and somehow cancelled or denied. As it is, the subscription will be made, but will simply never trigger if the user does not have permission to view the results. It's a solution that works, but it feels somewhat like a hack.

As for your question, resolve enables you to manipulate what the subscription publishes, whereas subscribe in combination with withFilter enables you to filter out results that you don't want to publish under specific circumstances (eg user/state). Kind of like the difference between map and filter :)

anhldbk commented 7 years ago

@altschuler Thanks for sharing with me.

anhldbk commented 7 years ago

@altschuler What you said about authorization is related to the protocol behind, right? The specification has nothing about Authorization.

anhldbk commented 7 years ago

@altschuler Is this what you want?

const subscriptionServer = SubscriptionServer.create({
  schema: executableSchema,
  execute,
  subscribe,
  onConnect(connectionParams, webSocket) {
    const userPromise = new Promise((res, rej) => {
      if (connectionParams.jwt) {
        jsonwebtoken.verify(connectionParams.jwt, JWT_SECRET,
        (err, decoded) => {
          if (err) {
            rej('Invalid Token');
          }

          res(User.findOne({ where: { id: decoded.id, version: decoded.version } }));
        });
      } else {
        rej('No Token');
      }
    });

    return userPromise.then((user) => {
      if (user) {
        return { user: Promise.resolve(user) };
      }

      return Promise.reject('No User');
    });
  },
// to be continued

(from https://github.com/srtucker22/chatty/blob/master/server/index.js#L61)

Altiano commented 6 years ago

@anhldbk That only solve where all subscriptions required authentication

but how about just some of them are required.. then based on the code, there would be more lines to check whether or not the subscription path (e.g. foobarUpdated need to be authenticated)

In Query and Mutation, authentication could be performed in directive or resolver. It would be nice if Subscription also has a way to do authentication in directive or via a property in resolver object (i.e. the same level as subscribe and resolve)

grantwwu commented 5 years ago

https://github.com/apollographql/graphql-subscriptions/blob/master/.designs/authorization.md does this help at all? It's a very old doc, unfortunately I don't really know much about this topic.

nudabagana commented 5 years ago

Not sure, if anyone is still interested, but I found a little hackish, but working solution, for refusing connections in resolvers: first you make authorization in onConnect as pointed by @anhldbk (but don't reject the socket yet, just return the status)

onConnect(connectionParams) {
 // ... do authorization
return { authorized: false }; // or true
}

next off, when declaring your topic in subscribtion you can access the .authorized field:

  @Subscription({
    topics: ({ args, context, payload }) => {
      if (!context.authorized )
      {
          // this gives user error response and cancels subsribtion
          throw new AuthenticationError(`Unauthorized user cannot receive info from this socket`);
      }
      return SOME_TOPIC;
}
  })
  accountBalanceChangeTopic(
//...
}

throwing error inside topics ( or filters) results in socket connection being closed.

RWOverdijk commented 5 years ago

Any new info on securing specific subscriptions? In my case I want to only allow the user to subscribe to their own data (based on an owner field).

adri1wald commented 3 years ago

i have taken the following approach:

{
  onMessage: {
    subscribe: async (_, { channel }) => {
      // do you auth logic here
      if (channel !== 'happy') {
        unauthorized() // throw an ApolloError
      }
      return pubsub.asyncIterator(MESSAGE_TOPIC)
    }
  }
}

Is this valid?

spencerwilson commented 3 years ago

Not sure if they're best practices, and it's not documented, but see the following two things:

Empirically Apollo Server supports what's needed, it's just not documented well.