Secure your GraphQL with Spring AOP

Secure your GraphQL with Spring AOP

Michal Gebauer | November 05, 2018

As I wrote in my previous post (How GraphQL beats REST when I am hungry) GraphQL has only one endpoint and that means our security configuration can not be based on requested urls. What could be even worst, there might be some queries or mutations which should be accessible for unauthenticated users, such as login or forgot password... Therefore we lose the possibility to enjoy Spring security filter chain and we must say in general that everyone can try to call our graphQL methods (.antMatchers("/graphql").permitAll()):


@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/graphql").permitAll()
.antMatchers("/vendor/**").permitAll()
.antMatchers("/graphiql").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.passwordEncoder(passwordEncoder())
.withUser("mi3o").password("{noop}nbusr123").roles("USER").and()
.withUser("admin").password("{noop}nbusr123").roles("USER", "ADMIN");
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
view raw SecurityConfig.java hosted with ❤ by GitHub


Technically, we could write our own RequestMatcher, but for that we would need to do the query parsing and resolver mapping and that would mean doing twice the work since the same happens after requests pass the security filter.


So what now? Our graphQL endpoint is exposed to everyone but we need to protect our methods which are meant only for authenticated and authorized users. For the case of simplicity I will illustrate the situation with three methods in our meaningless Resolver:

  • unsecuredResource() can be accessed without authentication
  • securedResource() can be accessed only by authenticated users
  • securedResourceAdmin() can be called only by users with role ADMIN

Method level security

If you noticed in my SecurityConfig, I used @EnableGlobalMethodSecurity annotation and that opens for us a new world of security restrictions which we can use on methods' level. Demonstrating all features regarding this annotation is out of scope of this post, for us now is sufficient to know that we can name roles that user must have to access the method. Let's have a look at our resolver


@Component
public class ResourceResolver implements GraphQLQueryResolver {
public String securedResource() {
return "Secured resource";
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String securedResourceAdmin() {
return "Secured resource Admin";
}
public String unsecuredResource() {
return "Unsecured resource";
}
}


Note: For this simple case, where only the user's role is checked, I could use @Secured annotation but in real world application the @Pre* and @Post* are more powerful option, since they allow to use expressions (SpEl) and can access method arguments and returned values (Spring docs).


We could secure every method which requires authentication or authorization just using this annotation, but best would be to have a defensive fallback scenario. We don't want to write annotations everywhere and we don't want to have sleepless nights thinking that we might have forgotten to annotate some method - we need a reasonable default that would say: every method without specified security annotation should require by default at least authentication.


Spring AOP

If we want to play with Aspect Oriented Programming in Spring, first, we need to enable AOP somewhere in our configuration with @EnableAspectJAutoProxy annotation


@SpringBootApplication
@EnableAspectJAutoProxy
public class SpringGraphqlSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SpringGraphqlSecurityApplication.class, args);
}
}


The magic behind security done by AOP is following:

  1. We need to say that every method, which is inside class that implements (any type of) Resolver interface, should throw an Exception in case there is no authentication in security context.
  2. Note that only resolvers defined in our application should be taken into consideration, otherwise GraphQL framework's classes will be intercepted during application start up.
  3. Additionally, we can name exact methods which will be excluded from this check, in our case unsecuredResource().

@Aspect
@Component
@Order(1)
public class SecurityQraphQLAspect {
/**
* All graphQLResolver methods can be called only by authenticated user.
* Exclusions are named in Pointcut expression.
*/
@Before("allGraphQLResolverMethods() && isDefinedInApplication() && !isUnsecuredResourceMethod()")
public void doSecurityCheck() {
if (SecurityContextHolder.getContext() == null ||
SecurityContextHolder.getContext().getAuthentication() == null ||
!SecurityContextHolder.getContext().getAuthentication().isAuthenticated() ||
AnonymousAuthenticationToken.class.isAssignableFrom(SecurityContextHolder.getContext().getAuthentication().getClass())) {
throw new AccessDeniedException("User not authenticated");
}
}
/**
* Matches all beans that implement {@link com.coxautodev.graphql.tools.GraphQLResolver}
* note: {@code GraphQLMutationResolver}, {@code GraphQLQueryResolver} etc
* extend base GraphQLResolver interface
*/
@Pointcut("target(com.coxautodev.graphql.tools.GraphQLResolver)")
private void allGraphQLResolverMethods() {
}
/**
* Matches all beans in com.mi3o.springgraphqlsecurity package
* resolvers must be in this package (subpackages)
*/
@Pointcut("within(com.mi3o.springgraphqlsecurity..*)")
private void isDefinedInApplication() {
}
/**
* Exact method signature which will be excluded from security check
*/
@Pointcut("execution(public java.lang.String com.mi3o.springgraphqlsecurity.resolver.ResourceResolver.unsecuredResource())")
private void isUnsecuredResourceMethod() {
}
}


Important: Did you notice the @Order(1) annotation in my aspect? We must ensure that our security aspect is triggered the very first prior any other interceptor if we want to have consistent API (our Exception needs to be the first one to be thrown in case user is not authenticated).


Custom annotation

The above explanation should give you the overview of the AOP security idea but we can go one step further and simplify it a bit. In current solution the unsecured method specification is wired and hard-coded in aspect class and is based on exact method signature. Let's create our own annotation that can be reused on any method without further fiddling with pointcut expressions


/**
* Marking annotation that will switch off security check for given method.
* Works only for methods defined in GraphQL Resolvers
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Unsecured {
}
view raw Unsecured.java hosted with ❤ by GitHub


Now we need to update our aspect class a bit and exclude methods which are annotated with @Unsecured


@Aspect
@Component
@Order(1)
public class SecurityQraphQLAspect {
/**
* All graphQLResolver methods can be called only by authenticated user.
* @Unsecured annotated methods are excluded
*/
@Before("allGraphQLResolverMethods() && isDefinedInApplication() && !isMethodAnnotatedAsUnsecured()")
public void doSecurityCheck() {
// same as shown previously
}
/**
* Any method annotated with @Unsecured
*/
@Pointcut("@annotation(com.mi3o.springgraphqlsecurity.config.Unsecured)")
private void isMethodAnnotatedAsUnsecured() {
}
}


Having our own annotation on method is cleaner solution, since everyone can see directly in the code that given method is not protected, instead of having it buried somewhere in the configuration. As a result, our resolvers will look similarly to the following one


@Component
public class ResourceResolver implements GraphQLQueryResolver {
// This method requires authenticated user by default
public String securedResource() {
return "Secured resource";
}
// This method requires user with role ADMIN
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String securedResourceAdmin() {
return "Secured resource Admin";
}
// This method can be called by unauthenticated user
@Unsecured
public String unsecuredResource() {
return "Unsecured resource";
}
}


That's it. Every method in our resolvers (except explicitly annotated ones) is now secured and requires authentication. Additional rules can be implemented using @Pre @Post annotations or programmatically e.g. using hasPermission SpEl method.


And finally, working with AOP and interceptors is safest when covered by integration tests :-)


@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SpringGraphqlSecurityApplicationTests {
@Autowired
private ResourceResolver resourceResolver;
@Test
public void unsecured_resource_ok() {
resourceResolver.unsecuredResource();
}
@Test(expected = AccessDeniedException.class)
public void secured_unauthorized_access_throws_exception() {
resourceResolver.securedResource();
}
@Test
@WithMockUser(username = "mi3o")
public void secured_ok() {
resourceResolver.securedResource();
}
@Test(expected = AccessDeniedException.class)
public void admin_unauthorized_access_throws_exception() {
resourceResolver.securedResourceAdmin();
}
@WithMockUser(username = "mi3o")
@Test(expected = AccessDeniedException.class)
public void without_admin_role_throws_exception() {
resourceResolver.securedResourceAdmin();
}
@WithMockUser(username = "admin", roles = "ADMIN")
@Test
public void admin_role_ok() {
resourceResolver.securedResourceAdmin();
}
}


You can find full working example in my github repository: spring-graphql-security.

Thank you for reading.

Do you like this post? Share it ;-)

My recent posts:

Throwing a party without a container

OK, so there we have some Servlet container, some EJB container, Spring container, this container, that container... Cool, everyone uses container…

How GraphQL beats REST when I am hungry

REST web services proved to be solid standard for communication and became easy way to expose server interfaces to clients. To introduce them into…

My offered Courses

Java

Java's been here for ages and is mature, stable language with huge community, standards and third party libs.

Javascript

Javascript is becoming the most popular programming language. Since Nodejs introduction it expanded everywhere.

Spring

Java enterprise framework which helps you to create full production ready application in amazingly short time.

React

A javascript library made by Facebook and used by half of the modern pages on the internet. It is my personal favourite for frontend development.

jQuery

A library which used to be the only choice for front-end development in javascript. It is still used by plenty of (not only legacy) applications.