GraphQL authorization with SPQR and custom annotations
Here at Trabe, we have been using GraphQL for a while. We started our dive in this technology with pure JavaScript projects but, after realizing how well GraphQL and React play together, we decided to adopt it for all new projects involving a React UI.
Since a good number of our projects are based on a server-side Java stack, we spent some time benchmarking the available GraphQL libraries for the Java ecosystem. We finally decided to go with GraphQL SPQR because we feel it fits better in our workflow: it’s easy to use and configure, based on annotations and plays nicely with Spring Boot.
GraphQL SPQR (SPQR from now on) provides us with a set of annotations to easily generate our schema, but, as a project in its early life stages, it doesn’t provide support for all the authorization and authentication needs that we can possibly have.
Authorization vs Authentication
Similar words, different concerns. It is a classic question answered plenty of times all around the internet, but still, one that needs a short reminder to avoid misconceptions:
Authentication is the process of ascertaining that somebody really is who they claim to be.
Authorization refers to rules that determine who is allowed to do what. E.g. Adam may be authorized to create and delete databases, while Usama is only authorised to read.
In our scenario authentication is handled using Spring Security. At the end of the day, the Spring Boot starter for SPQR creates a regular controller so we can use the regular Spring Security stuff to manage authentication.
We could also think about using spring security support for authorization but it just doesn’t fulfill our needs. We need to send extensive information about errors to our GraphQL clients and that cannot be done easily without using a custom solution.
The anatomy of a GraphQL response
Every GraphQL response is a map, containing a set of keys described in the specification. When an error happens, the map should include a key errors
containing the list of errors that happened when trying to fulfill the request. The keys that can be present in an error are also defined in the specification.
A typical error looks like this:
The GraphQL specification provides us with a mechanism to include additional information: we can add theextensions
key and include a map of custom values in our errors.
Sending GraphQL errors to the client
Now we know that the specification allows servers to add custom fields to GraphQL errors. We just need to know how to do that using SPQR.
SPQR uses GraphQL Java under the hood. SPQR generates the schema for us, allowing us to use a code first approach and to develop GraphQL APIs rapidly, but the execution of the queries and mutations is handled directly by GraphQL Java. As a result, error handling is also performed by GraphQL Java.
When you throw an exception in a resolver method, that exception will be handled by the ExecutionStrategy
(part of GraphQL Java). You can fully customize your ExecutionStrategy
to do whatever needs to be done with your exceptions, but the authors of GraphQL Java have done a pretty good job with the default implementations and have provided support for providing custom errors in an easy way.
GraphQL Java provides an interface GraphQLError
that can be implemented by our exceptions:
All the default implementations of ExecutionStrategy
(synchronous, asynchronous and batched) use SimpleDataFetcherExceptionHandler
to handle the exceptions.
This class ends up creating a ExceptionWhileDataFetching
which uses the information contained in your GraphQLError
to compose the error response:
This behavior allows us to generate custom, well-formed GraphQL errors just throwing certain exceptions. This doesn’t seem like that big of a deal but, combined with some AOP magic, it opens up a world of error management possibilities.
Custom annotation for authorization
Let’s start with an example of a typical SPQR resolver:
As you can see, we include a context object in our resolver. We assume that we have a ServletContextFactory
in our project creating that context object for every request.
This context object contains information about the current user of the application (for example the context object could be created using the information contained in a JWT token).
We need to define a custom annotation to be used whenever we need our authorization logic to be executed:
And annotate our example resolver:
Our resolver is done. The only missing piece in the puzzle is the aspect itself.
AOP for authorization
Authorization is a complicated topic. The authorization logic can be diverse depending on the scenario. Let’s assume for this story that in our scenario we need to check the value of some field of the object retrieved using the service method.
For this scenario, we can perform the authorization using an Around advice:
Let’s take a look at the key points of this aspect:
- We first define the
Pointcuts
needed for our aspect. - Then we use those pointcuts to define an
Around
advice that will fire for every public method annotated with@CheckPrivileges
and@GraphQLQuery
or@GraphQLMutation
. - The authorization logic itself, in this case, is simple: we execute the advised method and then check the value of a given field.
Of course, we are still missing some pieces of the puzzle. In our example we use a custom exception:
This exception implements GraphQLError
. This is the key point that allows us to customize the GraphQL error with our custom extensions.
As you can see, the constructor for the exception receives an ErrorInformation
enum. This design allows us to use the same exception for different kinds of errors.
The implementation of the enum, in this case, will be:
With all the pieces in place, the error response generated by SimpleDataFetcherExceptionHandler
will include two keys inside the extensions
map:
- a field with the error type.
- a field with the key of the message that our UI is going to show to the user when the error happens.
Wrapping up
We have developed an AOP based solution to generate custom GraphQL errors for SPQR resolvers. This solution is not limited to authorization and can be used for all kinds of error generation scenarios, allowing us to keep our code focused on the main concern and extract error generation to a collection of purpose-specific aspects.