“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.
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:
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.
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.
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:
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
/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:
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!
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.
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
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
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
- Declare our custom directive in the schema
- Override one or more of the
visitormethods in our subclass
- Add our directive to the schema
Here’s what this looks like for
Create a subclass of
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
visitObject methods since we’ve declared that our directive can be used on both field definitions and type definitions.
Here’s our implementation of
@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)