Using GraphQL schema directives for role based authorization
Tags: graphql, express, javascript, apollo
Published: 2019-03-19 11:43:57


When working on a graphql based api backend using node.js, if you stumble across role based authorization there are plenty ways of authorizing the logged in user.

You could keep the authorization logic completely separate from graphql(in a controller), you could write the logic in the resolvers itself(increases the amount of code in resolvers) or to keep the code clean and understandable, write GraphQL custom schema directives.

So here is how you would write a custom schema directive in graphql, for authorizing particular roles.

```javascript
//HasRoleDirective.js

import { SchemaDirectiveVisitor } from "apollo-server-express";
import {
  GraphQLDirective,
  DirectiveLocation,
  GraphQLList,
  defaultFieldResolver
} from "graphql";
import { ensureAuthenticated } from "../controllers/authController";
import { AuthorizationError } from "../errors";

class HasRoleDirective extends SchemaDirectiveVisitor {
  static getDirectiveDeclaration(directiveName, schema) {
    return new GraphQLDirective({
      name: "hasRole",
      locations: [DirectiveLocation.FIELD_DEFINITION],
      args: {
        roles: {
          type: new GraphQLList(schema.getType("Role"))
        }
      }
    });
  }
  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field;
    const roles = this.args.roles;
    field.resolve = async function(...args) {
      const [, , context] = args;
      await ensureAuthenticated(context);
      const userRoles = context.me.role;

      if (roles.some(role => userRoles.indexOf(role) !== -1)) {
        const result = await resolve.apply(this, args);
        return result;
      }
      throw new AuthorizationError({
        message: "You are not authorized for this resource"
      });
    };
  }
}
export default HasRoleDirective;

```

First we declare the directive name and the valid arguments that it accepts, when accepting arguments.
later in the field definition `visitFieldDefinition(field)`, where the logic is to be described, we accept arguments, extract contexrt from the args, the function call `ensureAuthenticated(context)` is to veirfy the jwtToken from the context, I have included the user's role in the jwt token.
So the `HasRole` directive is declared and ready to use. To use you need to pass the directives to your graphql config and declare it in the typeDefinition as follows

```javascript
// GraphQL Config
const schemaDirectives = { hasRole: HasRoleDirective };
const server = new ApolloServer({
  typeDefs,
  resolvers,
  schemaDirectives,
  ...context
});

//typedefinitions
import { gql } from "apollo-server-express";
export default gql`
  directive @hasRole(roles: [String!]) on FIELD_DEFINITION | FIELD
  scalar Date

  type Query {
    _: String
  }
  type Mutation {
    _: String
  }
`;

```
this way you will be able to use the custom schema directive in your typeDefs
example on how to use the custom schema directive:
```javascript
import { gql } from "apollo-server-express";

export default gql`
  extend type Query {
    businesses: [Business!] @hasRole(roles: [THIS_SUPER_ADMIN])
    business(id: ID!): Business @hasRole(roles: [THIS_ADMIN, THIS_SUPER_ADMIN])
  }
  extend type Mutation {
    businessUpdate(name: String!): Business!
      @hasRole(roles: [THIS_ADMIN, THIS_SUPER_ADMIN])
  }
  type Business {
    id: ID!
    name: String!
  }
`;
```

Another example, to verify if a user is authenticated
```javascript
//AuthDirective.js
import { SchemaDirectiveVisitor } from "apollo-server-express";
import { defaultFieldResolver } from "graphql";
import { ensureAuthenticated } from "../controllers/authController";

class AuthDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field;

    field.resolve = async function(...args) {
      const [, , context] = args;
      await ensureAuthenticated(context);
      return resolve.apply(this, args);
    };
  }
}
export default AuthDirective;
//passing to graphql config
const schemaDirectives = { auth: AuthDirective };
const server = new ApolloServer({
  typeDefs,
  resolvers,
  schemaDirectives,
  ...context
});

//declaration in typeDefinitions
import { gql } from "apollo-server-express";
export default gql`
  directive @auth on FIELD_DEFINITION
  scalar Date

  type Query {
    _: String
  }
  type Mutation {
    _: String
  }
`;
//typedefinitions usage
`extend type Query {
    payments(
      customer: ID
      status: String
      cursor: String
      limit: Int
    ): PaymentCollection! @auth
  }
  `
```
If you need to apply more than one directive to a query/mutation NOTE: the direction you write directives in graphql queries is from right to left, the right most directive is resolved first then the one to its left is resloved.
so say if you have this
```javascript
`extend type Query {
    payments(
      customer: ID
      status: String
      cursor: String
      limit: Int
    ): PaymentCollection! @hasRole(roles: [THIS_ADMIN, THIS_SUPER_ADMIN]) @auth
  }`
```
the auth directive is resolved first and the hasRoles is resolved after the auth directive. Say the auth directive fails the hasRoles directive is never reached.
***
I'm still learning a lot in GraphQL, if there are any improvements or mistakes in the code above please do drop a comment,
its always better to learn from the mistakes we make :)
***