Hey there,
I'm wondering what is the suggested way to authenticate subscriptions in Apollo Server 2 on a per-subscription level. For example, once a client is authenticated and has a WS connection, if the client requests to listen for new messages in a group, I want to validate that the user making the request has access to the group. I would really only want to run this authentication the first time the subscription request is made.
It seems to me like this logic belongs in the subscribe function in my resolvers. subscribe expects (...args) => AsyncIterator<any>, but I'd really want to return a Promise<AsyncIterator> where the Promise would reject if authentication failed. For example:
Subscription: {
messageAdded: {
subscribe: withFilter(
(payload, args, ctx) => canUserListenToMessages(payload, args, ctx)
.then(() => pubsub.asyncIterator(MESSAGE_ADDED_TOPIC)),
(payload, args, ctx) => filterSomeMessages(payload, args, ctx),
),
},
...
However, since subscribe requires an AsyncIterator, I have to write a classier version of something like this:
Subscription: {
messageAdded: {
subscribe: withFilter(
(payload, args, ctx) => {
const asyncIterator = pubsub.asyncIterator(MESSAGE_ADDED_TOPIC);
const authPromise = canUserListenToMessages(payload, args, ctx);
return {
next() {
return authPromise.then(() => asyncIterator.next());
},
return() {
return authPromise.then(() => asyncIterator.return());
},
throw(error) {
return asyncIterator.throw(error);
},
[$$asyncIterator]() {
return asyncIterator;
},
};
},
(payload, args, ctx) => filterSomeMessages(payload, args, ctx),
),
},
...
This seems weird and a lot of effort for what I would imagine is a common use case, so I'm betting I'm doing something wrong or don't understand something about how AsyncIterators work.
Would really appreciate your feedback here as I'm trying to update my tutorial to use Apollo Server v2 and reflect best practices!
/label question
@srtucker22 That's great point! You're still able to configure the onConnect and onOperation functions that you use in the tutorial. These are passed to the subscriptions option inside of the ApolloServer constructor. Totally need to document it, so thank you for bringing it up!
This is now documented more fully with #1263, which can be found https://deploy-preview-1263--apollo-server-docs.netlify.com/docs/apollo-server/ until it is fully published
Thanks, Evans.
But I don't think this documentation addresses the specific question I had about authorizing individual subscription requests.
For subscription auth, you want to first make sure a user is authenticated and set context on the WS. That is accomplished in onConnect and this documentation makes that nice and clear.
But once a client has made a WS connection, you want to limit the things they can subscribe to. For example, if User A tried to subscribe to notifications for User B, you'd want to throw an error. subscribe/withFilter don't have any built-in mechanisms to make this check easy when the user requests to subscribe to something.
My hacky solution originally was to use onOperation to parse the request and run auth logic. I think a better solution is to handle it in the resolvers, but this is still non-trivial currently AFAIK. I have to create an AsyncIterator wrapper that checks auth on the first next() call. Is this really the best way to solve that issue?
I think it could be really slick if instead of subscribe requiring an AsyncIterator that it accept an AsyncIterator or Promise<AsyncIterator>, then I could write something like this:
Subscription: {
messageAdded: {
subscribe: withFilter(
(payload, args, ctx) => canUserListenToMessages(payload, args, ctx)
.then(() => pubsub.asyncIterator(MESSAGE_ADDED_TOPIC)),
(payload, args, ctx) => filterSomeMessages(payload, args, ctx),
),
},
...
Or if pubsub.asyncIterator or something like a pubsub.asyncIteratorWithAuth could accept a Promise that lets devs pass auth validation logic.
What do you think?
@srtucker22 I think you need to reopen this as an issue to get attention from the developers, because "closed" means the case is already resolved.
Hey @evans
I just had a quick dig around the code and it looks like you can't configure the onOperation function through the subscriptions options, unless I am missing something?
Just getting started with Subscriptions and running into this problem of authorizing (and even validating) Subscription requests. With @tanekim77, I'm curious why this issue closed, as I don't see anything in the linked Apollo Server documentation that addresses authorization, only authentication, which is on a per-connection basis, while authorization (and validation) should occur on a per-Subscription basis. With Queries and Mutations, both can be handled easily since their Resolver functions can return a Promise, so any asynchronous authorization or validation can be handled and the Promise rejected if they fail or fulfilled if they pass. As a result, I agree with @srtucker22 that Subscriptions should accept a Promise<AsyncIterator> (or Promise<withFilter> and opened an Idea Discussion thread with that recommendation.
Why is this issue still closed? The feature requested is pretty much essential for production use of subscription, and provided answer doesn't solved the issue at all.
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.
Just as a note, our current thoughts on the near-term future of subscriptions in Apollo Server are at https://github.com/apollographql/apollo-server/issues/4354#issuecomment-784570753
Thanks @spencerwilson for your contributions here!
Most helpful comment
Thanks, Evans.
But I don't think this documentation addresses the specific question I had about authorizing individual subscription requests.
For subscription auth, you want to first make sure a user is authenticated and set context on the WS. That is accomplished in
onConnectand this documentation makes that nice and clear.But once a client has made a WS connection, you want to limit the things they can subscribe to. For example, if User A tried to subscribe to notifications for User B, you'd want to throw an error.
subscribe/withFilterdon't have any built-in mechanisms to make this check easy when the user requests to subscribe to something.My hacky solution originally was to use
onOperationto parse the request and run auth logic. I think a better solution is to handle it in the resolvers, but this is still non-trivial currently AFAIK. I have to create anAsyncIteratorwrapper that checks auth on the firstnext()call. Is this really the best way to solve that issue?I think it could be really slick if instead of
subscriberequiring anAsyncIteratorthat it accept anAsyncIteratororPromise<AsyncIterator>, then I could write something like this:Or if
pubsub.asyncIteratoror something like apubsub.asyncIteratorWithAuthcould accept aPromisethat lets devs pass auth validation logic.What do you think?