This is the third episode of the series GraphQL, from Theory to Real-World with Spring boot. The episodes are designed to be consumed independently, so feel free to start from what interest ou the most.
Overview
In the previous article, we’ve exposed a set of resources via a GraphQL API. Now we want to secure them to prevent unauthenticated and unauthorized access.
TL;DR
Check the corresponding video.
NOTE: Reading the previous article IS NOT mandatory, they are designed to be consumed independently by just checking out the codebase on GitHub.
Spring security offers us the WebSecurityConfigurerAdapter::configure
method to configure our app security. It gives us a way to define the access level of some resources based on URLs. Unfortunately, this is not a mechanism that fits well with the GraphQL protocol as it does with REST APIs.
Actually, graphQL is using only one endpoint, usually /graphql.
Hence we cannot secure our resources based on URLs. As we want some resources to be either accessed only by administrators, or by authenticated users or even opened to the public.
But it becomes possible if we use annotations on resolvers methods, coupled with Aspect-Oriented Programming (AOP), and that’s what we are going to do.
So, at the end of this article, you will know how to:
Set up an aspect with Spring AOP,
Use aspect-oriented programming coupled with custom annotations to secure your resources,
Write tests to check your GraphQL API
Project Setup
Clone the code base that we had in the previous article:
git clone -n https://github.com/zero-filtre/springboot-graphql-error-handling.git && cd springboot-graphql-error-handling/
git checkout bd7a0b5c039c610de9c8355be0d1c5f5e5484655
git switch -c my_experimental_branch
We will make the resources:
createUser(username:string, password:string)
public,
getUser(username:string, password:string)
available for authenticated users only, and we will create an additional resource:
deleteUser(username:string)
, to be available for admin users only.
Add the following dependencies to the pom.xml
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
...
Then we add the additional deleteUser
resource which matches the following resolver method in UserMutation.java
:
public User deleteUser(String username) {
return userService.deleteUser(username);
}
Don’t forget to define the UserService::delete
method, which just removes the user corresponding to that username from the in-memory List<User> users
and returns information about the deleted user.
Add deleteUser resource spec to the schema definition file: src/main/resources/UserQL.graphqls
:
type Mutation {
...
deleteUser(username: String!): User
}
Securing the resources
The strategy consists of:
Authorizing all requests related to GraphQL:
/graphql
Then, thanks to custom annotations, specify, at the method level in resolvers, which methods are public, which ones should be accessed only by authenticated users, and which ones should only be accessed by users with specific roles such as ADMIN.
Whilst with aspects, we will check if our annotations are applied or not and throw exceptions accordingly.
Let’s create the security configuration file: SecurityConfig.java
This config tells our app to authorize all requests related to GraphQL so that we can check them at the resolver level with annotations.
We will have two annotations:
@AdminSecured
appended to admin resources, and
@Unsecured
appended to public resources.
All other endpoints will, by default, need authenticated users access.
It’s always better to forget to make a resource unsecured than to forget to secure it 😁
AdminSecured.java
*/**
* This annotation will trigger Admin security check for
* the GraphQL resolver method where it is marked.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AdminSecured {}
Unsecured.java
*/**
* This annotation will disable security check for
* the GraphQL resolver method where it is marked.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Unsecured {}
Now go on and annotate the resolver methods accordingly:
@Unsecured
public User createUser(String username, String password) {
return userService.createUser(username, password);
}
@AdminSecured
public User deleteUser(String username) {
return userService.deleteUser(username);
}
UserQuery::getUser
will be set as secured by default.
Let’s define the aspect that will scan these annotations.
First, we need to Enable AOP in the main application class,
by adding @EnableAspectJAutoProxy
annotation like so:
@SpringBootApplication
@EnableAspectJAutoProxy
public class SampleGraphqlErrorHandlingApplication {
public static void main(String[] args) { SpringApplication.run(SampleGraphqlErrorHandlingApplication.class, args);
}
}
Then the aspect class… But wait! ☝, What’s an aspect? 🤔
Aspects are related to Aspect-Oriented Programming (AOP) which gives you a way to encapsulate or modularize a behavior that is transversal and repeated across several types of objects and that is, normally, not easy to encapsulate in its own module.
Usually, those behaviors (logging, caching, transaction management…) are not related to business logic and they are so-called aspects.
An aspect is a common feature that’s typically scattered across methods, classes, object hierarchies, or even entire object models [3] .
Example:
AOP provides you with programmatic control to specify that you want calls:
To all methods annotated
@Timed
to trigger a timer before executing the actual body of that code,To all methods classes in the package
xxx.bank.account
to trigger a specific additional security check,etc
Without repeating yourself 😎
We are going to do something similar with our Aspect.
Yes, you have guessed it right, our aspect is about security check based on annotations.
SecurityGraphQLAspect.java
Don’t worry, It seems complicated but it’s actually pretty simple.
Let’s go through each part of it to understand what it’s being done here.
First, As we defined this spring component as @Aspect
, it becomes an interceptor and we want it to be the first to be triggered, before any other interceptor, since it is our security guarantor. That’s why we use @Order(1)
The first method doSecurityCheck
is annotated with @Before
which is an Advice, which means, according to AOP terminology: action taken by the aspect at a specific point during the execution of the program.
The @Before
advice lets the action being executed before the specified PointCuts, which helps to categorize elements of the program that the aspect will watch.
doSecurityCheck
is executed before all GraphQL resolvers methods that are defined within our project and that are not annotated as unsecured. These categories are then defined with the help of @PointCut
.
doSecurityCheck
checks the Spring security context to make sure the user is authenticated.
Similarly: doAdminSecurityCheck
is executed before *all methods annotated as AdminSecured. *
doAdminSecurityCheck
checks the Spring security context to make sure the user has the ADMIN role.
This is the code for the referenced UnauthenticatedAccessException.java
:
public class UnauthenticatedAccessException extends RuntimeException implements GraphQLError {
public UnauthenticatedAccessException(String msg) {
super(msg);
}
@Override
public List<SourceLocation> getLocations() { return null; }
@Override
public ErrorClassification getErrorType() { return null; }
}
That’s it! 🏁
Now let’s start the app, launch http://locahost:8080/graphiql and:
- Create a user:
Everything works as expected ✌🏾.
- Delete that user:
We do not have the right to do that as expected ✌🏾.
- Get users:
We need to log in first as expected ✌🏾.
Let’s test the remaining cases programmatically so that you can automate it in a pipeline and earn some time.
GraphQLSecurityTests.java
Notice that we restart the context after each test method with @DirtiesContext
to make sure every method runs in a clean, not polluted context. But you should not abuse this, as it increases considerably your tests execution time.
Run the test and should have a 100% success.
Now our endpoints are secured 😎.
Conclusion
GraphQL APIs are great but are different in so many ways from REST APIs, and the way to handle the security is no exception.
That’s why in this article, we saw how to use aspect-oriented programming coupled with custom annotations to secure our resources. Additionally, we also wrote tests to check the whole stuff.
I hope you enjoyed it.
In the next article, we will describe a strategy and apply a strategy to write fast, bulletproof tests for your GraphQL APIs with Spring boot.
Don’t forget, Feedback is a gift 🎁!
References:
[1]https://docs.spring.io/spring/docs/2.0.x/reference/aop.html
[2]https://mi3o.com/spring-graphql-security/
[3]https://docs.jboss.org/aop/1.0/aspect-framework/userguide/en/html/what.html
Top comments (0)