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(); | |
} | |
} |
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 authenticationsecuredResource()
can be accessed only by authenticated userssecuredResourceAdmin()
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:
- 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. - 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.
- 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 { | |
} |
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.