March 01, 2018

GraphQL Directive Permissions — Authorization Made Easy

Directive permissions are a declarative way of implementing authorization rules in GraphQL servers. In this article, our guest author Dennis Walsh explains how to use them in order to protect your data.

If you're interested in writing an article for our blog as well, drop us an email.

GraphQL servers send your app into the world wearing only its birthday suit — everything is exposed. A quick introspection query reveals all possible API operations and data structures. That could change, but for now, all of your operations and data are laid bare.

Therefore, one might anticipate authentication and authorization are GraphQL first class citizens. But, neither of them are part of the official spec. That lack of direction created a lot of sleepless nights for my GraphQL server development, and it wasn’t until I watched Ryan Chenkie’s talk about directive permissions that I found a solution.

In this post, we will first talk through a naive approach to GraphQL permissions and find out about its drawbacks. Then, we’ll discover directive permissions and learn how they provide a declarative and reusable alternative.

To play around with directive permissions, check out this Launchpad demo.

Permissions in GraphQL: A Naive Approach

A Simple Example

When naively implemented, the code for permissions in your GraphQL server quickly becomes repetitive. Let’s start by quickly reviewing how most GraphQL servers implement permissions in a naive and simple fashion.

As an example project to anchor the discussion, here’s our GraphQL schema definition. The schema defines the API for car dealer software.

type Query {
  vehicles(dealership: ID!): [Vehicle!]!
}

type Mutation {
  updateVehicleAskingPrice(id: ID!, askingPrice: Int!): Vehicle
}

type Vehicle {
  id: ID!
  year: Int!
  make: String!
  model: Int!
  askingPrice: Float
  costBasis: Float
  numberOfOffers: Int
}

type User {
  id: ID!
  name: String!
  role: String!
}

Several fields should scream “malicious user fun”. To protect the API, let’s make up a few permissions rules for it:

  • updateVehicleAskingPrice should be restricted to managers
  • costBasis should be restricted to managers
  • numberOfOffers should be restricted to authenticated users

Naively Implementing Permissions in GraphQL Resolvers Creates Duplication and Mixes Concerns

Using Prisma in combination with Prisma bindings as a data access layer, here’s how the restriction for the updateVehicleAskingPrice mutation can be implemented:

const Mutation = {
  updateVehicleAskingPrice: async (parent, { id, askingPrice }, context, info) => {
    const userId = getUserId(context)
    const isRequestingUserManager = await context.db.exists.User({
      id: userId,
      role: `MANAGER`
    })
    if (isRequestingUserManager) {
      return await context.db.mutation.updateVehicle({
        where: { id },
        data: { askingPrice }
      })
    }
    throw new Error(
        `Invalid permissions, you must be a manager to update vehicle year`
    )
  }
}

We’re using the exists function on the Prisma binding instance (line 4) to ensure the requesting user has proper access rights for this mutation. If this is not the case, the mutation is not performed and will fail with an “Invalid permissions”-error instead.

Field-level constraints get even more interesting. Here is how we can protect the numberOfOffers and costBasis fields on Vehicle:

const Query = {
  vehicles: async (parent, args, context, info) => {
    const vehicles = await context.db.query.vehicles({
      where: { dealership: args.id }
    })
    const user = getUser(context)

    return vehicles.map(vehicle => ({
      ...vehicle,
      costBasis:
          user && user.role.includes(`MANAGER`) ? vehicle.costBasis : null,
      numberOfOffers: user ? vehicle.numberOfOffers : null
    }))
  }
}

With this approach, our resolvers become cluttered with redundant permission logic. As a first improvement, we can write a wrapping function that abstracts away some of the redundant authorization logic:

const Query = {
  vehicles: async (parent, args, context, info) => {
    const vehicles = await context.db.query.vehicles({
      where: { dealership: args.id }
    })
    const user = getUser(context)
    return protectFieldsByRole(
      [
        { field: `costBasis`, role: `MANAGER` },
        { field: `numberOfOffers`, role: `MANAGER` }
      ],
      vehicles,
      user
    )
  }
}

That looks a lot cleaner already. However, there is no straightforward way to figure out which fields are protected. This will always require digging into the resolver implementations and actually read the code.

A Declarative Approach to GraphQL Permissions Based on GraphQL Directives

Embedding Permission Directives in the Schema Definition

Contrast the above schema and resolver implementation with this schema:

directive @isAuthenticated on FIELD | FIELD_DEFINITION
directive @hasRole(role: String) on FIELD | FIELD_DEFINITION

...

type Mutation {
    updateAskingPrice(id: ID!, newPrice: Float!): Vehicle! @hasRole(role: "MANAGER")
}

...

type Vehicle {
  id: ID!
  year: Int!
  make: String!
  model: Int!
  askingPrice: Float
  costBasis: Float @hasRole(role: “MANAGER”)
  numberOfOffers: Int @isAuthenticated
}

At a glance, we know what is protected and what is not. In my book, that’s a win! 🍻 How do we get to such magic? You guessed it — directive resolvers.

Realizing Directive Resolvers Into Permissions is Straightforward

You can think of directive resolvers as resolver middleware. An incoming request first hits the directive resolver. If it passes (i.e. next() is invoked), the request continues to the actual field resolver. Here’s how we check for an authorized role inside a directive resolver:

const directiveResolvers = {
  ...,
  hasRole: (next, source, {role}, ctx) => {
    const user = getUser()
    if (role === user.role) return next();
    throw new Error(`Must have role: ${role}, you have role: ${user.role}`)
  },
  ...
}

Enforcing permission rules with GraphQL directives allows to remove any authorization logic from the field resolvers:

const Query = {
  vehicles: (parent, args, context, info) =>
      context.db.query.vehicles({ where: { dealership: args.id } })
}

Note that setting up directiveResolvers with graphql-tools is straightforward:

export const schema = makeExecutableSchema({
  typeDefs,
  resolvers,
  directiveResolvers
});

Changing Directive Permissions Takes Little Work

Now, let’s change the requirements:

  • numberOfOffers should be restricted to managers (formerly just authenticated)
  • askingPrice should be restricted to authenticated users (formerly unauthenticated)

Here, hold my beer…

type Vehicle {
  id: ID!
  year: Int!
  make: String!
  model: Int!
  askingPrice: Float @isAuthenticated
  costBasis: Float @hasRole(role: “MANAGER”)
  numberOfOffers: Int @hasRole(role: “MANAGER”)
}

…Done! We can update our permissions simply by adjusting the directives in the schema definition, without touching the actual implementation. The generic directive resolvers take care of enforcing the rules.

Demo: Experimenting With Directive Permissions

The best way to learn is experimentation, so I made a Launchpad demo to play around with permissions, queries, and users.

For instance, try running this mutation as different users:

mutation {
  updateAskingPrice(id: "1", newPrice: 10000) {
    id
    year
    make
    model
    askingPrice
  }
}

Change the directives on the schema to see how the permissions change. Note that Launchpad’s GraphiQL shows errors first, scroll down to see data.

I also made a demo repo combining Prisma, Auth0, and directive permissions.

Potential Drawbacks & Alternatives

Some people argue against “polluting” the schema with information that goes beyond the actual GraphQL schema definition. I appreciate that argument. For a different approach that doesn’t touch your schema definition, check out GraphQL Shield by Matic Zavadlal. It even has a few ingenious tricks like caching permission functions per request.

Directive Permissions Provide Clear and Easy Authorization

Permissions in GraphQL can be difficult at first. With RESTful APIs, authorization is implemented by protecting the individual API endpoints. This approach does not work for GraphQL because there’s only a single endpoint exposed by the server.

Therefore, authorization in GraphQL requires a major shift in thinking. The official GraphQL spec doesn’t provide any guidance and best practices for implementing permissions are still emerging. One of them being directive permissions which we covered in this article.

To understand directive permissions, we first took at look at the cumbersome resolver implementation when going for a naive approach. We then learned about directive permissions and how they provide a declarative alternative by extending the GraphQL schema definition with dedicated GraphQL directives.

I believe this emerging directive permissions pattern finally gives GraphQL a clear and declarative path to securing data. It does so by separating permissions and data into their respective layers with a concise and expressive syntax.

To get some practical experience with the examples explained in this article, check out the live demo on Launchpad or the code in this GitHub repository.


I like blogging about new software patterns and intellectual property law. If we share those interests, please consider following me here and on Twitter @LawJolla.

Comments

Comments

Don’t miss the next post!