graphQL custom directives

GraphQL Custom Directives

Within this article we will explore how to implement custom GraphQL directives to protect content from exposure through your API.

GraphQL (https://graphql.org/) is a modern way to conceive the interaction between browsers and web applications. Owing to its features, being strongly typed and yet so flexible, with built-in schema introspection capabilities, it’s a really good choice for implementing a replacement to conventional RESTful approach, or even for flanking a pre-existing API with the target of widening the API offer of your application server.

We’re not giving an introductory tutorial on GraphQL here, though; we assume a basic knowledge of GraphQL, as well as a solid knowledge of Java Spring Boot.

Anyways, even if you’re new to these concepts, we’ll try to keep things simple as much as possible, so to let you enjoy the wonders of GraphQL.

Also, make sure to check out Andrea Gazzarini’s outstanding article “GraphQL, REST: Take the best of both” to gain a wider perspective on introducing GraphQL into your web API. 

GraphQL Specs

The GraphQL specification says:

Generally speaking, directives provide a way to describe alternate runtime execution and type validation behavior in a GraphQL document.

It means that directives provide for altering GraphQL’s execution behavior, as well as describing additional information for types, fields, fragments and operations.

We will focus on how to use them in order to add visibility restrictions on certain fields.

The Schema

Consider the following GraphQL schema:

				
					type User {
    id: ID!
    username: String!
    email: String!
    password: String
    role: String!
}

type Employee {
    id: ID!
    firstName: String!
    lastName: String!
    salary: Float
}

type Query {
    user(id: ID!): User
    employee(id: ID!): Employee
}
				
			

It is a very simple schema, but looking a bit better we can spot a couple of things.

First, the field password within the User type appears in query responses and it shouldn’t, not even to administrative or super users, neither if the field is encrypted.

Similarly, the salary field in the Employee type should not be exposed to every user performing a query, but to administrators only.

This is where directives come into play. Let’s add two custom directives to our schema: @sc_hidden and @sc_required_role:

				
					directive @sc_hidden on FIELD_DEFINITION
directive @sc_required_role(role: String!) on FIELD_DEFINITION

type User {
    id: ID!
    username: String!
    email: String!
    password: String @sc_hidden
    role: String!
}

type Employee {
    id: ID!
    firstName: String! @deprecated
    lastName: String!
    salary: Float @sc_required_role(role: "administrator")
}

type Query {
    user(id: ID!): User
    employee(id: ID!): Employee
}
				
			

The first thing we can notice is that directives must be declared before they are used within the schema. As a good rule of thumb, position their declarations at the beginning.

Both directives are tied to the FIELD_DEFINITION location; this determines where the directive can be used within the schema, and in fact you can see we decorated the password and the salary fields in the respective types with the two directives.

The @sc_hidden directive we define here will prevent a field value to be sent out in query responses.

This shows us a first alteration of the GraphQL schema behavior; this use case starts showing us exactly what directives are for.

Directives can even accept arguments, pretty much like functions do, as we can see into @sc_required_role: the behavior we want to add to the schema is the decorated field’s value being accessible if and only if the user has the proper authorization role.

Cool, isn’t it? Well, it’s not all there yet, though. Defining the directives within the schema is not enough, now we must implement the associated behavior into our application.

Before proceeding, however, a word on directive names. We are using what the GraphQL specification calls custom directives. Thus, it is considered good practice to prefix their names with a short string giving readers the context of such a directive. For example, for Facebook GraphQL directives they may have been using fc_. Similarly, we picked the prefix sc_ to indicate SpazioCodice.

Oh, and the underscore has a meaning as well: by the actual specification, built-in directives cannot use underscores in their names; this  concurs in letting the next programmer reading the schema clearly understand they are custom directives and not built-in ones.

For a complete list of GraphQL directives’ features, see https://spec.graphql.org/October2021/#sec-Type-System.Directives.

Setup The Project

We are going to create our project as a Spring Boot application, based on Maven. You can as well create it from scratch using Spring Initializer (https://start.spring.io/).

We used Spring Boot 2.6.4 and we relied on the following dependencies in our pom.xml:

After configuring the POM, we will prepare a new file schema.graphqls in src/main/resources with the content indicated earlier (the one with directives declaration). In fact, graphql-java will search for schema definitions in that directory first. However, keep in mind the schema can as well be defined in a Maven module per se, and included as a dependency in the current project. 

It’s a very good and clean solution when your schema grows in complexity and you need to break it down in more than one single, monolithic file; having too many files in your resources directory, indeed, can be cluttering.

The Data Fetchers

As an initial step of our implementation we will create the data fetchers, one of the most important concepts for a GraphQL server, for that matter.

A DataFetcher fetches the data for one field while the query is executed. For simplicity, in this example, we gathered all the data fetchers in a single class:

				
					@Component
public class GraphQLDataFetchers {

    static final List USERS = asList(
            Map.of(
                    "id", "1",
                    "username", "user1",
                    "email", "user1@mycompany.com",
                    "password", "Y6HhM|8?9,%X{w@dczg`",
                    "role", "user"),
            Map.of(
                    "id", "2",
                    "username", "user2",
                    "email", "user2@mycompany.com",
                    "password", "_3KDuf5c6tgFGqmOueaF",
                    "role", "administrator")
    );

    static final List

 EMPLOYEES = asList(
            Map.of("id", "12345",
                    "firstName", "Jean",
                    "lastName", "Kowalski",
                    "salary", 52000.00F),
            Map.of("id", "23456",
                "firstName", "Martin",
                "lastName", "Brewert",
                "salary", 51000.00F),
            Map.of("id", "34567",
                "firstName", "Monique",
                "lastName", "Duval",
                "salary", 54000.00F)
        );

    public DataFetcher

 user() {
        return dataFetchingEnvironment -> {
            var userId = dataFetchingEnvironment.getArgument("id");
            return USERS
                    .stream()
                    .filter(user -> user.get("id").equals(userId))
                    .findFirst()
                    .orElse(null);
        };
    }

    public DataFetcher

 employee() {
        return dataFetchingEnvironment -> {
            var employeeId = dataFetchingEnvironment.getArgument("id");
            return EMPLOYEES
                    .stream()
                    .filter(employee -> employee.get("id").equals(employeeId))
                    .findFirst()
                    .orElse(null);
        };
    }
}
				
			

Yeah, I can hear you: “how about a database?”. Indeed, we are getting our users and employees from  static lists within the class.

This is one of the advantages of GraphQL: it doesn’t impose at all where the data comes from. The data can come from a database, an in-memory map, or a remote service. It doesn’t matter, as long as the needed data can be fetched.

On top of that, recall that in our schema we defined user() and employee() as members of the Query supertype. Since they are defined as methods in our class, though, one could be tempted to consider them as methods or functions of Query

It’s worth noting that it’s not like that. GraphQL does not distinguish between fields and functions within the schema, they are all treated just as fields.

Implementing The Schema Loader

Right after defining the data fetchers, it is time to start with a schema loader implementation.

				
					@Service
public class GraphQLService {

    @Autowired
    GraphQLDataFetchers graphQLDataFetchers;


    private GraphQL graphQL;

    @Bean
    public GraphQL graphQL() {
        return graphQL;
    }

    @PostConstruct
    public void init() throws IOException {
        URL url = Resources.getResource("schema.graphqls");
        String sdl = Resources.toString(url, Charsets.UTF_8);
        GraphQLSchema graphQLSchema = buildSchema(sdl);
        this.graphQL = GraphQL.newGraphQL(graphQLSchema).build();
    }

    private GraphQLSchema buildSchema(String sdl) {
        TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
        RuntimeWiring runtimeWiring = buildWiring();
        SchemaGenerator schemaGenerator = new SchemaGenerator();
        return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
    }

    private RuntimeWiring buildWiring() {
        return RuntimeWiring.newRuntimeWiring()
                .directive("sc_required_role", new RequiredRoleDirective())
                .directive("sc_hidden", new HiddenDirective())
                .type(newTypeWiring("Query")
                        .dataFetcher("user", graphQLDataFetchers.user()))
                .type(newTypeWiring("Query")
                        .dataFetcher("employee", graphQLDataFetchers.employee()))
                .build();
    }
}
				
			

We packaged the schema loader in a dedicated service, annotating the class with @Service. The init() method here is responsible for properly building the new GraphQL object that will be available throughout the whole application. The object is bound to the schema built with the buildSchema() method, which performs a formal validation on the provided schema and makes it executable. init() is needed to be invoked as soon as the GraphQLService object has been constructed, thus we annotated the method with @PostConstruct.

The buildSchema() method, in turn, calls the buildWiring() method, responsible for delivering a RuntimeWiring object.
The RuntimeWiring object is a specification of data fetchers, type resolvers and custom scalars that are needed to wire together a working GraphQLSchema object. In synthesis, it pulls together all the several parts composing the schema as it has been defined and as we expect it to be.
In fact, in our buildWiring() method, we wire together the directives we defined and our two DataFetcher’s, all conveniently wrapped in a builder interface.

As a bottom line, we could say the schema loader is the backbone of the GraphQL part of an application.

Authentication + Authorization

In order to support our example from now on, we would need to add authentication functionality. Which, by the way, would add a lot of boilerplate code to our tiny little world.

In fact, under normal circumstances, an object of some type, representing the authorization details coming from the authentication phase, should be injected into the GraphQLContext. Usually, such an object is available into the web request as an attribute (e.g. webRequest.getAttribute("<attribute name>", RequestAttributes.SCOPE_REQUEST)).

As a side note, GraphQLContext can be considered as a map containing key values, useful when executing data fetchers.

However, for the sake of simplicity within our example, we are using the AuthHelper class to provide that object:

				
					@Getter
@Setter
public class AuthHelper {
    private User authenticatedUser;

    public boolean userHasRole(final String role) {
        return authenticatedUser.getRole().equals(role);
    }

    public AuthHelper(final String username) {
        switch (username) {
            case "user1":
                authenticatedUser = User.builder()
                        .username("user1")
                        .email("user1@mycompany.com")
                        .role("user").build();
                break;
            case "user2":
                authenticatedUser = User.builder()
                        .username("user2")
                        .email("user2@mycompany.com")
                        .role("administrator").build();
        }
    }
}
				
			
The class is very simple, indeed. It instantiates a regular or an administrator user basing on the requested username. Besides that, the class exposes the ability to determine whether the user is given a specific role, with the userHasRole() method.
Anyways, for the application to be able to use this mechanism (even if trivial), we must refer to the proper way we would use to inject the authorization object into the GraphQLContext: using (as a Bean) a GraphQL ExecutionInputCustomizer.

For that purpose, we add a GraphQLConfiguration class, exposing the method returning the customizer:

With this class, loaded as a @Configuration object by Spring Boot, we can provide an execution input customizer Bean that injects an “auth” property into the GraphQL context. Remember that “user1” as a username in our example identifies the regular user. NOTE: To test different behaviors related to different roles we can change the username here to “user2” and restart the application.

				
					@Configuration
public class GraphQLConfiguration {
    @Bean
    public ExecutionInputCustomizer executionInputCustomizer() {
        return (executionInput, webRequest) -> {
            executionInput.getGraphQLContext()
                // Here, the Authorization object that is put into the
                // GraphQLContext should come from a WebRequest's attribute:
                // 
                // .put("auth", webRequest.getAttribute("attribute name",
                //           RequestAttributes.SCOPE_REQUEST))
                // However, for the sake of simplicity within our example,
                // we are using the AuthHelper class to provide that object.
                .put("auth", new AuthHelper("user1"));
            return CompletableFuture.completedFuture(executionInput);
        };
    }
}
				
			

Directives Implementation

And finally, here we go: we’re implementing our custom directives.

Let’s start slow and easy with the simpler directive of ours, HiddenDirective.

We recall from our schema definition that @sc_hidden aims to protect those fields that must not be seen by anyone, regardless the role.
And, on top of that, the binding between. the @sc_hidden name and our HiddenDirective class happens into the GraphQLService::buildWiring() method:
				
					return RuntimeWiring.newRuntimeWiring()
        .directive("sc_required_role", new RequiredRoleDirective())
				
			

Now, let’s take a look at the directive class:

				
					public class HiddenDirective implements SchemaDirectiveWiring {

    @Override
    public GraphQLFieldDefinition onField(
            SchemaDirectiveWiringEnvironment environment) {
        GraphQLFieldDefinition field = environment.getElement();
        GraphQLFieldsContainer parentType = environment.getFieldsContainer();

        DataFetcher hiddenFieldDataFetcher = context -> null;

        environment.getCodeRegistry().dataFetcher(parentType, field, hiddenFieldDataFetcher);
        return field;
    }
}
				
			

In order to be treated as a GraphQL directive, the class must implement the SchemaDirectiveWiring interface. The documentation tells us that a SchemaDirectiveWiring is responsible for enhancing a runtime element based on directives placed on that element in the schema. It can enhance that GraphQL runtime element and add new behavior. We relied on this feature, by using it to change a field’s data fetcher. 

To do that, we have overridden the onField() method.

In the first place, we get the field definition object by means of the SchemaDirectiveEnvironment‘s method getElement(). Look at how this method is general in its nature by depending on the used generics.

We then extract the field’s container with the environment’s getFieldsContainer() method. We will need the container object later.

Subsequently, we come to the real core and purpose of the directive, instantiating an object implementing the DataFetcher interface, hiddenFieldDataFetcher. The long-winded way to achieve that data fetcher would have been:

				
					DataFetcher hiddenFieldDataFetcher = new DataFetcher() {
    @Override
    public Object get(DataFetchingEnvironment context) {
        return null;
    }
};
				
			

The get() method in there, although receiving a complete DataFetchingEnvironment (i.e. all the context we need to fetch the value), only returns null, just as the @sc_hidden directive requires.

Anyways, looking a bit better at the construct, we realize it can be resolved to a lambda: 

				
					DataFetcher<?> hiddenFieldDataFetcher = context -> null;
				
			
Finally, we register the newly created DataFetcher with the environment, returning the wanted field in the end.

We can see that, in its structure, a custom directive is quite easy to implement, once we get acquainted with the objects provided by the GraphQL framework.

Let’s implement the remaining directive with the RequiredRoleDirective class.
				
					public class RequiredRoleDirective implements SchemaDirectiveWiring {

    @Override
    public GraphQLFieldDefinition onField(
            SchemaDirectiveWiringEnvironment environment) {

        var requiredAuthRole =
                Optional.of(((StringValue) environment.getDirective()
                        .getArgument("role")
                        .getArgumentValue()
                        .getValue()).getValue())
                        .filter(role -> List.of("user", "administrator").contains(role))
                        .map(String::valueOf)
                        .orElseThrow(() -> new GraphQLException("Unknown role argument"));

        GraphQLFieldDefinition field = environment.getElement();
        var parentType = environment.getFieldsContainer();

        DataFetcher originalDataFetcher =
                environment.getCodeRegistry().getDataFetcher(parentType, field);
        DataFetcher authDataFetcher = context -> {
            var contextMap = context.getGraphQlContext();
            AuthHelper authContext = contextMap.get("auth");

            return (authContext.userHasRole(requiredAuthRole))
                    ? originalDataFetcher.get(context)
                    : null;
        };

        environment.getCodeRegistry().dataFetcher(parentType, field, authDataFetcher);
        return field;
    }
}
				
			

In principle, there are two main differences with the previous HiddenDirective.

One, we need to get the role argument value, here represented by the requiredAuthRole variable.

To get to it, we need to pass through the directive definition and get the value for the argument, as it has been defined within the schema. 

However, we must also ensure that who protected the field in the schema did use one of the allowed role names. For our example, we decided those role names to be “user” or “administrator”. In synthesis, if who defined the schema didn’t use one of those roles for the role argument of the @sc_required_role directive, we fail in processing the request with an exception.

We are forced to take care of that aspect, as the schema loader performs a formal validation of the schema ,from a syntactic point of view.

Two, we need a sensibly more articulated behavior for the DataFetcher.
We start by caching the original data fetcher. Then, within the DataFetcher::get() method, we extract from the GraphQL context the AuthHelper object previously injected.
The rest is pure logic; we check whether the user has the required role, and in that case we can return the field value, otherwise null will be returned to protect the value from unwanted eyes.

Summary

The source code for the complete example used in this article is available on SpazioCodice’s GitHub repository https://github.com/spaziocodice/blog-graphql-directives-101.

GraphQL directives represent a really good and structured way to protect content in our GraphQL based web applications, but we must not forget they can do much more. From values formatting to data transformation, literally anything we can do with data provided by data fetchers can be a subject to GraphQL directives.

I hope this GraphQL directives appetizer did turn on your interest, stay tuned on spaziocodice.com for more articles on programming.

Happy coding!

Share this post

Leave a Reply