Skip to content
This repository has been archived by the owner on Apr 14, 2023. It is now read-only.

authorizing subscriptions -- set a default context for client #75

Closed
srtucker22 opened this issue Feb 15, 2017 · 7 comments
Closed

authorizing subscriptions -- set a default context for client #75

srtucker22 opened this issue Feb 15, 2017 · 7 comments

Comments

@srtucker22
Copy link
Contributor

My goal is simply to validate whether a user is authorized to subscribe to a given channel. For example, if I was creating a group chat app, only members of a group would be able to subscribe to onMessageAdded(groupId: Int!) for that groupId.

From my understanding from digging into the code...

On the server, I can accomplish allowing/denying subscriptions via onSubscribe with a given context:

onSubscribe(parsedMessage, baseParams, connection) {
   // do stuff with baseParams.context to validate subscription
},

On the client, I'm trying to figure out a way to set up a default context for all subscriptions, similar to how networkInterface exposes this with middleware. For example, using networkInterface, we have a redux store that sets a jwt token on authorization headers. We send out queries with auth headers if the jwt is set, and on the server side, we validate the jwt token and pass the validated user into context for the resolvers to consume.

// middleware for requests
networkInterface.use([{
  applyMiddleware(req, next) {
    if (!req.options.headers) {
      req.options.headers = {};
    }

    // get the authentication token from local storage if it exists
    const jwt = store.getState().auth.jwt;
    if (jwt) {
      req.options.headers.authorization = `Bearer ${jwt}`;
    }
    next();
  },
}]);

I'd imagine something similar would/should be possible with SubscriptionManager, where we either (1) apply context via a setupFunction using next or a Promise, or (2) expose some public function that updates the default context for all subscriptions. This seems particularly important if we're using subscribeToMore as I don't see an obvious way to pass context with this call.

Any help or suggestions appreciated!

@hmaurer
Copy link

hmaurer commented Mar 3, 2017

I think this is solved by connectionParams? c.f. #53

@srtucker22
Copy link
Contributor Author

Nope, I don't think it solves the challenge but please correct me if I'm wrong.

As I understand it:
connectionParams on the client deals with onConnect so when you make a socket connection. But won't affect anything during client.subscribe(). A common use case where this matters would be letting anyone make a connection, but only let users subscribe to events with an auth context. It would be nice to pass all subscribe requests through a function that attaches auth context like a JWT token.

@NeoPhi suggested in #78 that we could have a client.subscribe() overriding function in the addGraphQLSubscriptions helper method. Happy to implement that. I'm just wondering if this feature should only be available for people using addGraphQLSubscriptions. I guess you could write your own wrapper method for client.subscribe if you're using this package without apollo-client, so sure.

@hmaurer
Copy link

hmaurer commented Mar 3, 2017

@srtucker22 I am not sure if this was intended as such, but I found out that returning an object from onConnect assigns it as context (https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/server.ts#L177), which is then accessible in the setupFunctions (to filter events based on auth) and in the resolver for the subscription.

Also note that the context is also passed to onSubscribe (https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/server.ts#L185) if you need it there.

Edit: Oh sorry, I think I misread your message. I don't quite understand how using the connectionParams would be problematic, though? If you wanted to pass auth information on a per-subscription basis it makes sense (and you can do that), but if you want to do it automatically for all subscriptions then you might as well pass that info on connection?

@srtucker22
Copy link
Contributor Author

yeah, i'm just focussed on client side setting/updating context per call or for every call.

an example:
do you need to open and close socket connections every time you log in/out? you'd rather just update the context on an open connection.

you could pass context into every client.subscribe() call or you could wrap client.subscribe() if you were using subscriptions-transport-ws directly. But if you're using apollo-client or react-apollo, currently there isn't a way to modify context after creating a connection, and you can't pass context into subscribe. if you're using subscribeToMore context gets even more obscured.

we should just change addGraphQLSubscriptions to let you call subscribe with context. i would also advocate exposing a function in addGraphQLSubscriptions that lets you wrap subscribe to override subscribe params when each request is sent. this would mirror how networkInterface works for requests and i think would be a commonly used feature enough that it's worth including instead of having to roll your own addGraphQLSubscriptions helper.

@swhamilton
Copy link

swhamilton commented Jul 24, 2017

UPDATE:
Found this solution to achieve what I am looking for. Haven't tested it out yet but looks promising.
#171 (comment)

@srtucker22 sorry to resurrect this issue if you got it resolved. Wondering if you could share your findings. I'm trying to achieve the same auth functionality where I need to modify the context passed into the onConnect.

My scenario is that a user should be able to subscribe (passing the auth token in connectionParams), then the server onConnect reads the auth token from the connectionParams and allows the subscription if it's a valid token. If that user signs out, he should be able to login again as a different user with different connectionParams, and thus a different context available on the server.

After the user logs out, I need to allow the user to login as a different user with a different token. I see two ways to achieve this:

  1. Close the connection on logout using wsclient.client.close() or wsclient.unsubscribeAll() then create a new subscription. (Can't get this working as the connection never seems to really disconnect and "let go" of the context.)

  2. Allow any unauthed user to subscribe, but pass the token as a variable for the subscription so we can filter unauthed subscriptions from receiving authed updates.

Do you have any insight into what's the best method to achieve subscription authorization and manage changing context when changing users?

@srtucker22
Copy link
Contributor Author

@swhamilton no worries!

You're absolutely correct that those are your two main options.

Option 1:
If you don't want to worry about unauthed socket connections, closing the connection and restarting it when a new user authenticates is your best move:
Client:
Use the lazy param on SubscriptionClient to wait for a user to login, then pass that context into connectionParams. wsclient.close() and wsclient.unsubscribeAll() will get rid of all the data and disconnect, but when you use lazy=true, every time you attempt to subscribe, SubscriptionClient will attempt to reconnect!

Server:
Use onConnect to validate the user info passed into context. Afterwards, any subscriptions you want to subscribe to will pass through onOperation(message, baseParams, webSocket) where that originally supplied context is attached to baseParams (baseParams.context). So if you want to validate that a user is allowed to subscribe to something, you can use this context and the message and use whatever logic you need to determine whether the subscription is allowed.

shameless plug You can check out the subscription section of my blog post on GraphQL Authorization for an example of how this looks on the server and client.

Option 2:
Alternatively, you could always keep the connection open whether or not it's authenticated. You would probably have to do some extra work to guard against attacks like someone opening up tons of connections.

Client:
You can use the middleware feature I added to SubscriptionClient that looks just like the one in ApolloClient. E.g.:

client.use([{
   applyMiddleware(opts, next) {
     // modify options for SubscriptionClient.subscribe here
     opts.context = {jwt: 'your_jwt'};
     next();
  },
}]);

This middleware will run before each subscription call.

Server:
In onOperation, you would need to validate context supplied in the message.payload.context, and potentially alter baseParams.context based on the supplied data so your resolvers get the right context.

Let me know if that helps!

@swhamilton
Copy link

@srtucker22 wow great summary. Thanks for the insight and blog post - all of it was very helpful! I was able to get it working finally with a reset websocket function implemented here:
https://github.com/coralproject/talk/blob/32da779ac155af2547f411a81ce5d93aecdcdd5e/client/coral-framework/services/client.js#L9

but I do want to try the middleware strategy for future use.

martijnwalraven pushed a commit that referenced this issue Oct 17, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

4 participants