Skip to content

Instantly share code, notes, and snippets.

@longtomjr
Created September 11, 2020 09:44
Show Gist options
  • Save longtomjr/717053e678c542ef831a10477db23297 to your computer and use it in GitHub Desktop.
Save longtomjr/717053e678c542ef831a10477db23297 to your computer and use it in GitHub Desktop.

We had similar issues getting access control setup on a field level with nestjs. I opted to go for shield with graphql middleware, and with some workarounds it looks like it works. We are doing shield configuration separate from our models for now, but you should be able to generate it using extensions (see @j 's post above).

I will be sharing what I ran into and how I fixed it here. I can also add a PR for the documentation to add a recipe for this, but some of this is still a bit hacky. Also, if this is off topic, feel free to let me know and I will post it in a more appropriate place. I think this post is more to illustrate what we can use the current graphql-middleware for outside of most of the nestjs constructs.

Part of the reason I went with shield is because we needed the returned objects in our rules, and I wanted to use the fragment replacement feature of shield. Below is how I set that up.

import { wrapSchema } from '@graphql-tools/wrap';
import { ReplaceFieldWithFragment } from '@graphql-tools/delegate';
import { applyMiddleware } from 'graphql-middleware';
import { appPermissions } from './rules';
...
      transformSchema: schema => {
        const newSchema = applyMiddleware(schema, appPermissions);
        if (newSchema.schema && newSchema.fragmentReplacements) {
          const transforms = [
            new ReplaceFieldWithFragment(
              newSchema.schema,
              newSchema.fragmentReplacements,
            ),
          ];
          const finalSchema = wrapSchema(newSchema.schema, transforms);
          return finalSchema;
        } else {
          return newSchema;
        }
      },

This allows me to add graphql shield rules on types, along with fragments. Unfortunately the issue where guards only runs after shield execution for queries and mutations is still a problem. After trying a couple of different approaches, I ended up creating my own graphql middleware that runs the passport logic (in a similar way the AuthGuard does) before the shield middleware runs.

import { Request, Response } from 'express';
import { authenticate } from 'passport';
import { User } from 'src/common/interfaces/user.interface';
import { IMiddlewareFunction } from 'graphql-middleware';
import { GraphQLResolveInfo } from 'graphql';
import { AuthenticationError } from 'apollo-server-express';

export const gqlAuthMiddleware: IMiddlewareFunction<
  unknown,
  { req: Request; res: Response },
  unknown
> = async (resolve, root, args, context, info: GraphQLResolveInfo) => {
  const passportFn = createPassportContext(context.req, context.res);
  try {
    const user = await passportFn(
      'jwt',
      { session: false },
      (err, user, info, ctx, status) =>
        handleRequest(err, user, info, ctx, status),
    );
    context.req.user = user;
    return resolve(root, args, context, info) as unknown;
  } catch (err) {
    throw err;
  }
};

const handleRequest = (
  err,
  user: User | false,
  info: Record<string, string> | null,
  _context,
  _status,
): User => {
  if (err || !user) {
    throw (
      err || new AuthenticationError(info?.message || 'authentication failed')
    );
  }
  return user;
};

/* eslint-disable */
const createPassportContext = (request: Request, response) => (
  type,
  options,
  callback: (...args: any[]) => User,
): Promise<User> => {
  return new Promise<User>((resolve, reject) =>
    authenticate(type, options, (err, user, info, status) => {
      try {
        request.authInfo = info;
        return resolve(callback(err, user, info, status));
      } catch (err) {
        reject(err);
      }
    })(request, response, err => (err ? reject(err) : resolve())),
  );
};
/* eslint-enable */

used in the transform schema like this:

        const newSchema = applyMiddleware(
          schema,
          { Query: gqlAuthMiddleware, Mutation: gqlAuthMiddleware },
          appPermissions,
        );

I am sure some of this can be simplified, since I am not dynamically creating the middleware like the nestjs passport module does. The big downside of this approach is that the authentication logic will run on every query or mutation. This is not a problem for us but tweaking this to fit that use-case should not be too difficult.

Another issue I ran into were that shield sometimes returns an error, nested in an object, resulting in a null exception for fields where data were returned, but because of the nested error return this code did not find the error, and masked the actual error with a null returned for non-null error.

https://github.com/graphql/graphql-js/blob/156c76ed77dc0d9b2dc3c988264d44763e8334aa/src/execution/execute.js#L778-L781

I am not sure what the actual reason for this is, if it is related to how nest adds resolvers to the graphql schema, or if it is related to the cursor pagination pattern that we are using. As a workaround I added a plugin method that traverses the nested object and throws if it finds the nested error.

import { PluginDefinition } from 'apollo-server-core';
import { GraphQLNonNull } from 'graphql';

const getNestedError = (source: unknown, depth = 0): Error | null => {
  if (depth > 10) {
    return null;
  }
  if (source instanceof Error) {
    return source;
  } else if (source instanceof Array) {
    return (
      source
        .map(x => getNestedError(x, depth + 1))
        .find(x => x instanceof Error) || null
    );
  } else if (source instanceof Object) {
    return (
      Object.values(source)
        .map(x => getNestedError(x, depth + 1))
        .find(x => x instanceof Error) || null
    );
  } else {
    return null;
  }
};

export const throwNestedErrorPlugin: PluginDefinition = {
  requestDidStart() {
    return {
      executionDidStart() {
        return {
          willResolveField(ctx) {
            const returnType = ctx.info.returnType;
            const source: unknown = ctx.source;
            return (err, result) => {
              if (!result && returnType instanceof GraphQLNonNull) {
                const err = getNestedError(source);
                if (err instanceof Error) {
                  throw err;
                }
              }
            };
          },
        };
      },
    };
  },
};

and using it in the config like this

       plugins: [throwNestedErrorPlugin],

I hope this is of help to someone, and helps illustrate at least one use-case when adding this feature to nest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment