Attaching Directives To Auto-Generated Queries and Mutations

https://grandstack.io/docs/neo4j-graphql-js-middleware-authorization

Hi according to the source above, when the @hasScope directive has been enabled, the auto-generated queries and mutations will require valid JWT which with the necessary scope claim.

Since neo4j-graphql.js automatically adds Query and Mutation types to the schema, these auto-generated fields cannot be annotated by the user with directives. To enable authorization on the auto-generated queries and mutations, simply enable the hasScope directive and it will be added to the generated CRUD API with the appropriate scope for each operation

How do I figure out which scope has been added for each operation? Every time, I run a query it simply returns a 'You are not authorized for this resource' error message.

For example, I tried adding the scope 'User:Read' to the JWT that my server generates. However this does not authorize a user to query the User Object type. See example below:

.

Do you have a copy of your schema? How are you applying the directive?

I am stuck in the same spot. I manage to query when I have a simple @isAuthenticated directive on types, and when I generate my schema with:

const schema = graphql.makeAugmentedSchema({
typeDefs,
mutations: true,
config: {
auth: {
isAuthenticated: true,
hasRole: true,
},
},
});

If I generate my schema with

const schema = graphql.makeAugmentedSchema({
typeDefs,
mutations: true,
config: {
auth: {
isAuthenticated: true,
hasRole: true,
hasScope: true,
},
},
// schemaDirectives: { hasScope: MyHasScopeDirective },
});

then I cannot figure out for the life of me what scopes I need to add to my token. I have tried many combinations e.g. "Read:User", "User:Read", "User:Query", etc. to no avail. The doc doesn't say what scopes are required so I am shooting in the dark.

My workaround was to specify custom scopes in type definitions.

// For example:
type Query {
    User: [User] @hasScope(scopes:["User:read"])
}

Then I add the scope into my JWT payload..

^ good workaround

I ended up writing a custom directive:

const schema = graphql.makeAugmentedSchema({
typeDefs,
mutations: true,
config: {
auth: {
isAuthenticated: true,
hasRole: true,
hasScope: true,
},
},
schemaDirectives: { hasScope: MyHasScopeDirective },
});

My directive printed out the requested scope. Turns out the generated scopes have a space between the node label and the operation, e.g. "Actor: Read".

I also ended up keeping this custom directive and handled authorizations there because it become tedious to add so many scopes to my JWT token. So instead, I rely on roles more, and use scopes as needed.

Any chance you could post your custom directive? I'm struggling to make mine


function ownKeys(object, enumerableOnly) {
  var keys = Object.keys(object);
  if (Object.getOwnPropertySymbols) {
    var symbols = Object.getOwnPropertySymbols(object);
    if (enumerableOnly)
      symbols = symbols.filter(function (sym) {
        return Object.getOwnPropertyDescriptor(object, sym).enumerable;
      });
    keys.push.apply(keys, symbols);
  }
  return keys;
}

function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true,
    });
  } else {
    obj[key] = value;
  }
  return obj;
}

function _objectSpread(target) {
  for (var i = 1; i < arguments.length; i++) {
    var source = arguments[i] != null ? arguments[i] : {};
    if (i % 2) {
      ownKeys(source, true).forEach(function (key) {
        _defineProperty(target, key, source[key]);
      });
    } else if (Object.getOwnPropertyDescriptors) {
      Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
    } else {
      ownKeys(source).forEach(function (key) {
        Object.defineProperty(
          target,
          key,
          Object.getOwnPropertyDescriptor(source, key)
        );
      });
    }
  }
  return target;
}

class MyHasScopeDirective extends graphqltools.SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    var expectedScopes = this.args.scopes;
    var next = field.resolve; // wrap resolver with auth check

    field.resolve = function (result, args, context, info) {
      console.log(
        "Field name " +
          field.name +
          " VFD - looking for scope " +
          expectedScopes
      );

      var decoded = verifyAndDecodeToken({
        context: context,
      }); // FIXME: override with env var

      var scopes =
        decoded["Scopes"] ||
        decoded["scopes"] ||
        decoded["Scope"] ||
        decoded["scope"] ||
        [];

      // if any requested scope matches
      if (
        expectedScopes.some(function (scope) {
          return scopes.indexOf(scope) !== -1;
        }) ||
        expectedScopes.some(function (scope) {
          return scopes.indexOf("[" + field.name + "]") !== -1;
        })
      ) {
        return next(
          result,
          args,
          _objectSpread({}, context, {
            user: decoded,
          }),
          info
        );
      }

      throw new _errors.AuthorizationError({
        message: "You are not authorized for this resource",
      });
    };
  }
  visitObject(obj) {
    var fields = obj.getFields();
    var expectedScopes = this.args.scopes;

    Object.keys(fields).forEach(function (fieldName) {
      var field = fields[fieldName];
      var next = field.resolve;

      field.resolve = function (result, args, context, info) {
        console.log(
          "Field name " +
            field.name +
            " VFD - looking for scope " +
            expectedScopes
        );

        var decoded = verifyAndDecodeToken({
          context: context,
        }); // FIXME: override w/ env var

        var scopes =
          decoded["Scopes"] ||
          decoded["scopes"] ||
          decoded["Scope"] ||
          decoded["scope"] ||
          [];

        if (
          expectedScopes.some(function (role) {
            return scopes.indexOf(role) !== -1;
          })
        ) {
          return next(
            result,
            args,
            _objectSpread({}, context, {
              user: decoded,
            }),
            info
          );
        }

        throw new _errors.AuthorizationError({
          message: "You are not authorized for this resource",
        });
      };
    });
  }
}

const schema = graphql.makeAugmentedSchema({
  typeDefs,
  mutations: true,
  config: {
    auth: {
      isAuthenticated: true,
      hasRole: true,
      hasScope: true,
    },
  },
  schemaDirectives: { hasScope: MyHasScopeDirective },
});