Skip to content

Instantly share code, notes, and snippets.

@michalgebauer
Last active October 13, 2021 15:02
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save michalgebauer/5efb8d0d588b4ea3960759ed050ac277 to your computer and use it in GitHub Desktop.
Save michalgebauer/5efb8d0d588b4ea3960759ed050ac277 to your computer and use it in GitHub Desktop.
@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";
}
}
@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";
}
}
@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();
}
}
@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() {
}
}
@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() {
}
}
@SpringBootApplication
@EnableAspectJAutoProxy
public class SpringGraphqlSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SpringGraphqlSecurityApplication.class, args);
}
}
@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();
}
}
/**
* 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 {
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment