Authorization In GraphQL Using Custom Schema Directives

With Apollo’s graphql-tools, Auth0 JWTs, and neo4j-graphql.js

--

How do I handle authorization with GraphQL?” is a question I hear frequently when talking with folks about GraphQL. The GraphQL specification makes no authoritative statements about how to implement authorization, leaving it up to the developer. While this flexibility can be an appealing part of GraphQL, building your own auth layer can also be a daunting task for developers who just want to build their application.

In this post, we show how to implement authorization in a simple fullstack GraphQL demo application by building custom schema directives using the graphql-tools library. Our example will use Auth0 as our auth service, but this approach can be used with any authentication provider and the code examples will work with any JWT-based auth service. We’ll be using neo4j-graphql.js with ApolloServer to quickly spin up a GraphQL API, but the concepts used here can be applied to any GraphQL implementation.

Demo App

Let’s imagine we’re building a web application and we have data about businesses, users, and reviews of businesses. Our authorization model is such that:

  • Aggregated data can be viewed by anyone browsing the site (even unauthenticated users)
  • Detailed data about users can be viewed only by authenticated users
  • Detailed data about businesses can be viewed only by authenticated users with “admin” privileges

Here’s the app we’ll be building, you can try it here:

Our demo app. Aggregated data is open to all users. Only logged in users can see user data. And only admin users can see business data. Try it here (although you’ll need an email address on the grandstack.io domain to get admin privileges :-)

So where do we start? I grabbed the latest version of the GRANDstack starter project, which gives us a GraphQL backend using Neo4j as the database, and a simple React frontend with material-ui for UI components.

A core part of the GRANDstack starter project is neo4j-graphql.js, which allows us to quickly spin up a GraphQL API backed by a Neo4j graph database from just GraphQL type definitions. It auto-generates CRUD resolvers that handle database calls so all we have to do is define our GraphQL schema in SDL and not worry about writing boilerplate resolvers.

We start with a simple GraphQL schema:

Then we use neo4j-graphql.js to auto-generate Query and Mutation types and resolvers based on our type definitions and Apollo Server to spin up our GraphQL server. We need to inject a Neo4j JavaScript driver instance into the context to handle our database calls. Here I’ll create a connection to a Neo4j instance hosted on Neo4j Cloud.

Spinning up a GraphQL API using neo4j-graphql.js and ApolloServer

So now we have a GraphQL API that we can query, with auto-generated queries and mutations. But this is now accessible to anyone — we need to lock down our API based on the user authorization rules described above.

Using GraphQL Playground to query our GraphQL API. All our generated queries and mutations are accessible to anyone!

Let’s start to explore some options for adding authentication and authorization to our GraphQL API.

Auth Check In Resolvers

An initial approach might be to inject an authorization token into the context object and then in our resolver verify the token and check the users' permissions. Or even better if we use some authorization middleware like passport.js that will inject a user object into the request. So an implementation of “is this request authenticated?” might look something like this:

Implementing auth check in resolvers

This approach works OK for prototyping, but has a few flaws. One flaw is that it forces us to bring authorization into our resolver implementations, rather than having a single source of truth for authorization. The other problem is that with many GraphQL engines (like neo4j-graphql.js) our resolvers are auto-generated for us and we don’t want to have to re-implement them just so we can handle authentication.

One way to solve that problem is with the use of middleware. Most of these GraphQL engines will support middleware so that any user-defined middleware can either return an error if an auth check fails, or inject an error object into the request for the GraphQL engine to handle. You can see an example of this approach in the GRANDstack docs.

A downside of this middleware approach is that it is all-or-nothing — we can error out an entire GraphQL request, but not individual fields. If a user has access to some fields, but not others we should still return the fields they are authorized to access. So there must be a better approach…

Before we explore the “better approach” for implementing authorization in our GraphQL API (spoiler alert: it involves schema directives!), let’s add our authentication service, Auth0 to our app.

Adding Auth0 And JWT Support

At Neo4j we love Auth0. We use Auth0 to handle authentication for services like Neo4j Sandbox, Neo4j Cloud, Neo4j Desktop, and our developer certification portal.

With Auth0 we can easily implement login flow in our app using OAuth identity providers or create email/password based accounts for our users. Once Auth0 handles sign-up and login, our application is passed a JSON Web Token (JWT) that contains cryptographically signed “claims” about the user, such as their user id and any scopes or roles that we want to assign to the user.

JSON Web Token (JWT)

A JSON Web Token provided by Auth0 after a user goes through the login flow for our

Here’s an example of a JWT provided by Auth0 after a user has gone through the login flow for our application. Note the “PAYLOAD: DATA” section in purple — these are the verified claims contained in the JWT. We’re specifically interested in the sub claim (the user id) and the roles assigned to the user, which are namespaced by our application domain:

"https://grandstack.io/roles": ["admin"]

Roles

We can assign roles to our user by implementing a Rule in Auth0. A Rule can be a function that is passed some metadata about the currently authenticating user and then can attach additional claims to the JWT based on this metadata. We could look up user permissions in a database or call out to a third party Identity and Access Management service to identify the appropriate roles and scopes to assign. Here we’ll just inspect the domain of the email address the user is using to authenticate. If they have a grandstack.io email address we assign them “admin” role otherwise, they get the “user” role.

An Auth0 rule to add roles to the JWT based on an authenticated user’s email address domain.

Client Side Auth0 Integration

Auth0 has lots of great guides for integrating into your client application across many languages and frameworks. I followed their React example, which involved adding a route to handle /callback and pulling in auth0.js. Once the user authenticates a JWT is sent to our application via a request to /callbackOur client application takes that token and saves it in the browser’s localStorage.

We then need to make sure we grab this token from localStorage and add it to the headers sent in every GraphQL request that is sent from our client application. Fortunately, with Apollo Link this is straightforward. Apollo Links can be chained together, so we will implement one Link to handle the standard http requests to our GraphQL service, and another Link to add the auth token as an Authorization header:

Adding our auth token JWT to each GraphQL request using Apollo Links.

Remember in our GraphQL server at this point we’re grabbing this token from the request header and passing it through to either our middleware or resolver to inspect for authorization claims, but that we wanted to try a different approach for authorization in our GraphQL service.

OK, back to our GraphQL server implementation. We had identified some approaches for authorization (token check in resolver and middleware based) and identified a few problems with each approach. How do we solve these problems?

Schema directives to the rescue!

Schema Directives In GraphQL

Directives are GraphQL’s built-in extension mechanism. We can use directives to annotate a field (or an entire type definition) to introduce custom logic implemented in the GraphQL server. Directives can be used either in a GraphQL query, or in the schema definition. Here we’ll only concern ourselves with schema directives.

As an example of the power of directives, in neo4j-graphql.js we use a @cypher schema directive to map a Cypher query to a field in GraphQL (similar to the concept of defining a computed field). Here is how we define the query to populate our “Star Summary” histogram — shown above in our demo app — using a @cypher schema directive. Whenever a GraphQL query includes the starsByCategory field, we run the attached Cypher query to resolve that field:

Using the `@cypher` schema directive to bind a field to a Cypher query.

Schema Directives For Authorization

Using schema directives to define our authorization rules allows us to declaratively state our requirements. It gives us a single place to define authorization rules — in the GraphQL schema — (rather than spreading across resolver implementations), and because we’re doing this at the type definition level these rules can still be enforced when we use auto-generated resolvers, like those provided by neo4j-graphql.js

@isAuthenticated Schema Directive

The base level of authorization is simply “is this user authenticated?”. In our demo app, we said we wanted to only show detailed user data when the request is authenticated. To define this auth rule in our schema, we add the @isAuthenticated schema directive to the User type definition, conveying that User data should only be available when the GraphQL request is authenticated.

@hasRole Schema Directive

The next level of authentication is role-based. In our schema we can define the role a user must have in order to access the data. First we add an enum with our different available roles to the GraphQL schema:

Adding a Role enum for the possible user roles used to the GraphQL schema

Then we use one or more of these roles in the directive:

Using the `@hasRole` directive to express roles required to query a type.

In this case we declare that only users with the role “admin” are allowed to query Business data.

@hasScope

While we won’t actually use this directive in our demo app, it’s worth noting that we could express authorization at a more fine-grained level, the scope level. Scopes are especially useful when we need to declare authorization rules on mutations. And in fact a role could map to a group of scopes. Using a @hasScope directive to declare authorization rules for a mutation would look something like this:

In order to create a Business, the user making the request must have the user:write scope.

So we’ve now defined our authorization rules in the GraphQL schema using schema directives, but how do we actually enforce those rules? To do that we’ll need to implement the @isAuthenticated and @hasRole directives.

Implementing Custom Directives

To implement our schema directives we will make use of Apollo’s graphql-tools library, which contains a tool for implementing custom schema directives in the SchemaDirectiveVistor class. To implement a custom schema directive we will:

  1. Create a subclass of SchemaDirectiveVisitor
  2. Declare our custom directive in the schema
  3. Override one or more of the visitor methods in our subclass
  4. Add our directive to the schema

Here’s what this looks like for @isAuthenticated :

Create a subclass of SchemaDirectiveVisitor

Declare our custom directive in the schema

To do this we override the getDirectiveDeclaration method in IsAuthenticatedDirective In our directive declaration we specify the name of the directive and the valid locations where we can use it in the schema. In this case the valid locations are on a field, or object (type).

Declaring our `@isAuthenticated` directive by overriding `getDirectiveDeclaration`

Alternatively, we could add the directive declaration to the schema SDL:

Declaring `@isAuthenticated` directive using SDL

Override one or more of the visitor methods in our subclass

The “visitor” methods correspond to the valid locations that we specified in our directive declaration. You can see all the possible visitor methods in the graphql-tools docs but for our @isAuthenticated directive, we’ll need to implement both the visitFieldDirective and visitObject methods since we’ve declared that our directive can be used on both field definitions and type definitions.

Here’s our implementation of visitFieldDefinition for @isAuthenticated . We first check to make sure an authorization token has been provided as an authorization header. Then we attempt to verify the JWT, using our key. If the JWT is not properly signed then the call to jwt.verify will throw an error and we’ll throw an AuthorizationError (which is actually a nice and tidy Apollo Error)

Add our directive to the schema

Finally we must add our directive implementation to the schema. We can do this by passing a schemaDirectives object to neo4j-graphql.js’ makeAugmentedSchema — which is similar to, and actually wraps — graphql-tools’ makeExecutableSchema

Passing our custom directive classes into the `schemaDirectives` object when creating our GraphQL schema with `makeAugmentedSchema`.

And with that, we’ve significantly improved our approach to implementing authorization in our application. Give it a try here.

To recap in this post we’ve:

  • spun up a GraphQL API on top of Neo4j with just GraphQL type definitions using neo4j-graphql.js
  • declaratively expressed our authorization rules as GraphQL schema directives, @isAuthenticated and @hasRole
  • implemented these custom directives using Apollo graphql-tools SchemaDirectiveVisitor class
  • integrated Auth0 authorization as a service into our React client app

You can find the code for our demo app on Github and you can try out the demo app yourself.

We’ll be adding these features to neo4j-graphql.js soon, so you’ll be able to take advantage of these authorization schema directives without having to implement custom schema directives yourself 😄

Resources

To learn more about graphql-tools, Auth0, or neo4j-graphql.js, here are a few resources:

Thanks to Ryan Chenkie for giving a talk at GraphQL Summit 2017 on authorization with GraphQL that inspired much of the approach here. Also, while we’re talking about GraphQL Summit, come by and see us at GraphQL Summit 2018!

Questions? Comments? Please join the discussion for this post on Hacker News.

--

--