django / channels

Developer-friendly asynchrony for Django

Home Page:https://channels.readthedocs.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

access connectionParams from Apollo client in Django channels custom middleware.

btribouillet opened this issue · comments

Hi,

This is a feature request unless there is a way I'm not aware of doing it.

So far it is easy to access the query string from the scope argument in the middleware. But Apollo client (GraphQL client) doesn't allow injecting URL params during subscriptions.

Use case:

  • Apollo client is provided to the SPA (vue3 in this case)
  • User sign in, a token is provided
  • token is injected in connectionParams as Apollo (can't inject it in the URI as it was instantiated and provided on the first load).
  • token is passed to Django channels Middleware, the user is retrieved and injected in the scope.

I really wish to be able to pass the token in the query string / URL params but it seems only the connectionParams is reevaluated

Example of how the connectionParams is passed on the Apollo client:

const wsLink = new WebSocketLink({
  uri: process.env.VUE_APP_WS_GRAPHQL_URL || "ws://0.0.0.0:8000/ws/graphql/",
  options: {
    reconnect: true,
    lazy: true,
    connectionParams: async () => {
      const { value: authStr } = await Storage.get({ key: "auth" });

      let token;
      if (authStr) {
        const auth = JSON.parse(authStr);
        token = auth.token;

        return {
          token: token,
        };
      }

      return {};
    },
  },
});

Full code of the described issue on StackOverflow

Closing for now as I managed to inject the token as a query string in the WebsocketLink instance.

const authLink = setContext(async (_: any, { headers }: any) => {
  const { value: authStr } = await Storage.get({ key: "auth" });

  let token;
  if (authStr) {
    const auth = JSON.parse(authStr);
    token = auth.token;
  }

  const wsUri =
    process.env.VUE_APP_WS_GRAPHQL_URL || "ws://0.0.0.0:8000/ws/graphql/";

  // Inject token as query string / URL params on the WebSocketLink instance.
  // @ts-ignore
  wsLink.subscriptionClient.url = `${wsUri}?token=${token}`;

  // return the headers to the context so HTTP link can read them
  return {
    headers: {
      ...headers,
      authorization: token ? `JWT ${token}` : null,
    },
  };
});

So this solution is not consistent so I'm reopening the issue. And honestly, it felt wrong.

The only solution I have for now is to keep the logic inside the consumer and inject the user into the context from there.
I feel it would be much nicer to have it on the middleware level. What do you think?

from tinga.schema import schema
import channels_graphql_ws
from channels.db import database_sync_to_async
from django.contrib.auth.models import AnonymousUser
from graphql_jwt.utils import jwt_decode
from core.models import User
from channels_graphql_ws.scope_as_context import ScopeAsContext


@database_sync_to_async
def get_user(email):
    try:
        user = User.objects.get(email=email)
        return user

    except User.DoesNotExist:
        return AnonymousUser()


class MyGraphqlWsConsumer(channels_graphql_ws.GraphqlWsConsumer):
    """Channels WebSocket consumer which provides GraphQL API."""
    schema = schema

    # Uncomment to send keepalive message every 42 seconds.
    # send_keepalive_every = 42

    # Uncomment to process requests sequentially (useful for tests).
    # strict_ordering = True

    async def on_connect(self, payload):
        """New client connection handler."""
        # You can `raise` from here to reject the connection.
        print("New client connected!")

        # Create object-like context (like in `Query` or `Mutation`)
        # from the dict-like one provided by the Channels.
        context = ScopeAsContext(self.scope)

        if 'token' in payload:
            # Decode the token
            decoded_data = jwt_decode(payload['token'])

            # Inject the user
            context.user = await get_user(email=decoded_data['email'])

        else:
            context.user = AnonymousUser