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:
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.
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.
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:
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)
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.
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 /callback
Our 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:
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:
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:
Then we use one or more of these roles in the directive:
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:
- Create a subclass of
SchemaDirectiveVisitor
- Declare our custom directive in the schema
- Override one or more of the
visitor
methods in our subclass - 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).
Alternatively, we could add the directive declaration to the schema 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
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:
- Announcing New Features In neo4j-graphql.js — with CodeSandbox Container example!
- Guide To Implementing Custom Schema Directives With Apollo Server
- Auth0 React Quickstart Tutorial
- Five Common GraphQL Problems And How Neo4j-GraphQL Aims To Solve Them
- neo4j-graphql.js on npm
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.