Giter Club home page Giter Club logo

Comments (31)

enisdenjo avatar enisdenjo commented on May 18, 2024 2

I personally like performing authorization on every subscription request (in onSubscribe). In cases where you're using token based auth, running a check on every operation request guarantees expiry detection or manipulation prevention.

from graphql-ws.

sibelius avatar sibelius commented on May 18, 2024 2

should this be another recipe?

from graphql-ws.

enisdenjo avatar enisdenjo commented on May 18, 2024 1

You can simply use the context and inject your context before GraphQL execution. Here's how:

import { createServer } from 'graphql-ws';

createServer(
  {
    schema,
    context: async (ctx) => {
      return await getUser(ctx.connectionParams?.authorization);
    },
  },
  {
    server,
    path: '/graphql',
  },
);

or if you want to get the context once onConnect and just reuse it, here is how you'd go by:

import { createServer } from 'graphql-ws';

const authorizedContexts: Record<string, unknown> = {};

createServer(
  {
    schema,
    context: async (ctx) => {
      return authorizedContexts[ctx.connectionParams?.authorization];
    },
    onConnect: async (ctx) => {
      if (typeof ctx.connectionParams?.authorization !== 'string'){
        return false;
      }

      const { user } = await getUser(ctx.connectionParams?.authorization);
      authorizedContexts[ctx.connectionParams.authorization] = await getContext({ user });
      return true;
    },
  },
  {
    server,
    path: '/graphql',
  },
);

from graphql-ws.

enisdenjo avatar enisdenjo commented on May 18, 2024 1

🎉 This issue has been resolved in version 1.9.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

from graphql-ws.

enisdenjo avatar enisdenjo commented on May 18, 2024 1

Ah! You're absolutely right. Completely missed that... Fixed it here 222a796.

Thanks for the insight!

from graphql-ws.

enisdenjo avatar enisdenjo commented on May 18, 2024 1

I don't know what happened, but onSubscribe never had the args argument... Fixed #36 (comment) again.

Please consult the "ws server and client auth usage with token expiration, validation and refresh" recipe instead.

Furthermore, if you still have errors being thrown - please provide the callstack too.

from graphql-ws.

ollyde avatar ollyde commented on May 18, 2024 1

@enisdenjo I just used your previous example with context. Seems to be working great. Thanks

Here's the solution we use to create the server.

import { ApolloServer, ExpressContext } from "apollo-server-express";
import { GraphQLSchema, execute, subscribe, parse } from "graphql";
import { checkJwt } from "./auth";
import http from "http";
import WebSocket, { WebSocketServer } from "ws";
import { useServer } from "graphql-ws/lib/use/ws";
import { logError } from "./logger";

export let subscriptionServer: WebSocket.Server<WebSocket.WebSocket>;

export const createSubscriptionServer = (args: {
    schema: GraphQLSchema;
    apolloServer: ApolloServer<ExpressContext>;
    httpServer: http.Server;
}) => {
    const { schema, apolloServer, httpServer } = args;

    subscriptionServer = new WebSocketServer({
        path: apolloServer.graphqlPath,
        server: httpServer,
    });

    useServer(
        {
            schema,
            execute,
            subscribe,
            context: async (ctx) => {
                const user = await checkJwt(ctx.connectionParams?.Authorization as string);
                return { user };
            },
            onError: (e) => {
                logError("socket_error", e);
            },
        },
        subscriptionServer
    );
};

from graphql-ws.

hckr avatar hckr commented on May 18, 2024 1

@OllyDixon IMO it'd be best to add an onNext callback which checks if token is still valid, as recommended above by @enisdenjo (described here: https://github.com/enisdenjo/graphql-ws#user-content-auth-token)

from graphql-ws.

enisdenjo avatar enisdenjo commented on May 18, 2024 1

I actually don't recommend doing auth on each event emission because the amount of outgoing messages can grow real quick and soon you'll find yourself fighting with performance problems. The auth recipes has that approach displayed just as examples of what's possible.

IMHO it's perfectly fine to do auth either in onConnect or in onSubscribe.

As far as your concern about token expiry goes, @hckr, you can simply start a timer in onConnect set to run after the token expires which closes the connection forcing the client to re-connect and therefore re-authenticate.

I personally do auth in onConnect with no extra steps. The WebSocket connection cannot be hijacked while active; meaning, it's safe to assume that even if the token expires, the originally authenticated user is still the same - which is enough for me.

from graphql-ws.

enisdenjo avatar enisdenjo commented on May 18, 2024

I wouldnt say so. It really is that simple and everyone has their own take on authorization. Let the minds go free 😉 .

from graphql-ws.

enisdenjo avatar enisdenjo commented on May 18, 2024

But we could showcase how to inject a custom context for GraphQL, a PR is always welcome 😄 .

from graphql-ws.

tautvilas avatar tautvilas commented on May 18, 2024

@enisdenjo first thanks for this great library.

I can not get onSubscribe context params working though. I see that the API has changed. I am trying to do this now:

onSubscribe: async (ctx, message): Promise<ExecutionArgs> => {
      // console.log('SUBSCRIBE', ctx.connectionParams);
      return { contextValue: ctx.connectionParams, schema, document: null };
    }

However if I add this code in onSubscribe the subscription itself does not happen. Am I missing something? Is there a way to inject context without passing all the ExecutionArgs? It seems a bit complicated to return whole graphql ExecutionArgs in order to pass context.

I am using 1.9.3 version of the lib

from graphql-ws.

enisdenjo avatar enisdenjo commented on May 18, 2024

Hey @tautvilas,

you're absolutely welcome!

Straight to business, you are missing the document field. TypeScript should be complaining too though...

Complete and valid execution arguments are expected from you when returning custom ones from the onSubscribe. Validation and all necessary steps for preparing the exec args are on your side, so make sure that the returned args have everything the execution part needs.

from graphql-ws.

enisdenjo avatar enisdenjo commented on May 18, 2024

This is a conscious decision because it allows you to apply optimizations (like executing some rules exclusively or trusting the client implicitly) or simply have control of when the validation happens when you introduce a custom implementation.

from graphql-ws.

enisdenjo avatar enisdenjo commented on May 18, 2024

If you find it helpful, there is a "Server usage with custom dynamic GraphQL arguments and validation" recipe in the readme. It should help you get started.

from graphql-ws.

tautvilas avatar tautvilas commented on May 18, 2024

Thanks for quick response!

I have tried to replicate the recipe you mentioned, but now I am stuck on the part that msg format is not the same like in recipe. In my case the msg is:

{
  id: '8b5311ab-f385-4614-92dd-d657d661b0a9',
  type: 'subscribe',
  payload: { query: 'subscription { verificationPassed }' }
}

in the recipe should have these fields:

const args = {
        schema,
        contextValue: getDynamicContext(ctx, msg),
        operationName: msg.payload.operationName,
        document: parse(msg.payload.operationName),
        variableValues: msg.payload.variables,
      };

why could I be missing the operationName?

from graphql-ws.

enisdenjo avatar enisdenjo commented on May 18, 2024

Well, you have no operationName field in the message payload. 😄

from graphql-ws.

tautvilas avatar tautvilas commented on May 18, 2024

I think I finally got it. The reason I got really confused because there is a bug in the recipe (I think). It should be:

operationName: msg.payload.operationName,
document: parse(msg.payload.query),

The recipe says that operationName has to be parsed and not query

from graphql-ws.

ollyde avatar ollyde commented on May 18, 2024

None of the solutions here seem to pass the TypeScript test, seems they are returning the wrong information. Did the API change? Could someone provide a full example; I can't seem to find it :-/

For example where are you getting getDynamicContext from?

Any ideas here?

Screenshot 2022-04-14 at 17 07 26

Screenshot 2022-04-14 at 17 07 31

from graphql-ws.

enisdenjo avatar enisdenjo commented on May 18, 2024

@OllyDixon the onSubscribe API never changed, it returns either an array of GraphQLErrors or the ExecutionArgs, you're not returning either.

from graphql-ws.

enisdenjo avatar enisdenjo commented on May 18, 2024

For example where are you getting getDynamicContext from?

That's just an example of a function you can implement that returns the context dynamically from the arguments you pass in.

from graphql-ws.

ollyde avatar ollyde commented on May 18, 2024

@enisdenjo thanks for the speedy reply, out full example doesn't seem to work though :-/

 // onConnect: async (connection) => {
  // const auth = connection?.connectionParams!["Authorization"] ?? "";
  // if (!auth) {
  //     return {};
  // }
  // // @ts-ignore
  // const user = await checkJwt(auth);
  // return { user };
  // },
  // @ts-ignore
  onSubscribe: async (ctx, msg, args) => {
      const auth = ctx?.connectionParams!["Authorization"] ?? "";
      const user = await checkJwt(auth);

      const newArgs: any = {
          schema,
          contextValue: ctx,
          operationName: msg.payload.operationName,
          document: parse(msg.payload.operationName),
          variableValues: msg.payload.variables,
          user,
      };

      return newArgs;
  },

from graphql-ws.

enisdenjo avatar enisdenjo commented on May 18, 2024

@OllyDixon what does "doesn't seem to work" mean?

P.S. my example in #36 (comment) was wrong - it's fixed now.

from graphql-ws.

ollyde avatar ollyde commented on May 18, 2024

@enisdenjo it throws an error

 onSubscribe: async (ctx, msg, args) => {
                const user = await checkJwt(ctx.connectionParams?.Authorization);
                return { ...args, contextValue: { user } };
            },

Internal error occurred during message handling. Please check your implementation. TypeError: Cannot read properties of undefined (reading 'definitions')

from graphql-ws.

hckr avatar hckr commented on May 18, 2024

@OllyDixon In your example, is context function called on client connection or on each subscription?

from graphql-ws.

ollyde avatar ollyde commented on May 18, 2024

@OllyDixon In your example, is context function called on client connection or on each subscription?

We only subscribe once and send "events" to the client. Each event as a "type" and "object" that is parsed.

I wouldn't recommend having multiple subscriptions.

from graphql-ws.

hckr avatar hckr commented on May 18, 2024

@OllyDixon Right. But my specific question is does your code example handle the case in which a JWT token was valid when the client subscribed and then is expired after, let's say, 30 minutes – is the client disconnected / re-authenticated in any way?

from graphql-ws.

ollyde avatar ollyde commented on May 18, 2024

@hckr Interesting edge case. The client will just reconnect then, once per hour blip. We have an event bus API that handles each device and potential missed socket events.

from graphql-ws.

hckr avatar hckr commented on May 18, 2024

@OllyDixon But AFAIR the [malicious] client doesn't have to disconnect and the session (context) will remain until the socket is closed.

Note that the context function is invoked on each operation only once. Meaning, for subscriptions, only at the point of initialising the subscription; not on every subscription event emission.
(https://github.com/enisdenjo/graphql-ws/blob/d47e1bb5ff5589bf49587e70b446a2cab01688d1/docs/interfaces/server.ServerOptions.md#context)

from graphql-ws.

ollyde avatar ollyde commented on May 18, 2024

@hckr interesting, guess we'll need to disconnect/connect each hour hehe :-D

from graphql-ws.

ollyde avatar ollyde commented on May 18, 2024

@hckr thanks, will do :-)

from graphql-ws.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.