Starter for building GraphQL applications with Spring GraphQL
—
Spring Boot GraphQL Starter provides comprehensive integration with Spring Security for authentication, authorization, and secure GraphQL operations. The integration supports both servlet-based (Spring MVC) and reactive (Spring WebFlux) security configurations.
Auto-configuration for GraphQL security in servlet-based applications.
@AutoConfiguration(after = { GraphQlWebMvcAutoConfiguration.class, SecurityAutoConfiguration.class })
@ConditionalOnClass({ GraphQL.class, EnableWebSecurity.class })
@ConditionalOnBean({ WebGraphQlHandler.class, SecurityFilterChain.class })
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class GraphQlWebMvcSecurityAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public SecurityDataFetcherExceptionResolver securityDataFetcherExceptionResolver() {
return new SecurityDataFetcherExceptionResolver();
}
}@Configuration
@EnableWebSecurity
public class GraphQlSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/graphql").authenticated()
.requestMatchers("/graphiql").hasRole("DEVELOPER")
.anyRequest().permitAll()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
)
.build();
}
}Auto-configuration for reactive GraphQL security.
@AutoConfiguration(after = { GraphQlWebFluxAutoConfiguration.class, ReactiveSecurityAutoConfiguration.class })
@ConditionalOnClass({ GraphQL.class, EnableWebFluxSecurity.class })
@ConditionalOnBean({ WebGraphQlHandler.class, SecurityWebFilterChain.class })
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public class GraphQlWebFluxSecurityAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public SecurityDataFetcherExceptionResolver securityDataFetcherExceptionResolver() {
return new SecurityDataFetcherExceptionResolver();
}
}@Configuration
@EnableWebFluxSecurity
public class ReactiveGraphQlSecurityConfig {
@Bean
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange(auth -> auth
.pathMatchers("/graphql").authenticated()
.pathMatchers("/graphiql").hasRole("DEVELOPER")
.anyExchange().permitAll()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.build();
}
}@Controller
public class SecureBookController {
@QueryMapping
@PreAuthorize("hasRole('USER')")
public List<Book> books(Authentication authentication) {
String username = authentication.getName();
return bookService.findByUser(username);
}
@MutationMapping
@PreAuthorize("hasRole('ADMIN') or @bookService.isOwner(#id, authentication.name)")
public Book updateBook(@Argument String id, @Argument BookInput input, Authentication auth) {
return bookService.update(id, input, auth.getName());
}
}@Controller
public class UserController {
@QueryMapping
public User currentUser(@AuthenticationPrincipal JwtAuthenticationToken token) {
String userId = token.getTokenAttributes().get("sub").toString();
return userService.findById(userId);
}
@QueryMapping
public List<Book> myBooks(SecurityContext securityContext) {
Authentication auth = securityContext.getAuthentication();
return bookService.findByOwner(auth.getName());
}
}@Controller
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class BookController {
@QueryMapping
@PreAuthorize("hasRole('USER')")
public List<Book> books() {
return bookService.findAll();
}
@QueryMapping
@PreAuthorize("hasRole('ADMIN') or @bookService.canView(#id, authentication.name)")
public Book bookById(@Argument String id) {
return bookService.findById(id);
}
@MutationMapping
@PreAuthorize("hasRole('ADMIN')")
public Book deleteBook(@Argument String id) {
return bookService.delete(id);
}
@SubscriptionMapping
@PreAuthorize("hasRole('USER')")
public Flux<BookEvent> bookUpdates(Authentication authentication) {
return bookEventService.getEventsForUser(authentication.getName());
}
}@Component("bookSecurity")
public class BookSecurityExpressions {
public boolean canViewBook(String bookId, String username) {
Book book = bookService.findById(bookId);
return book.isPublic() || book.getOwner().equals(username);
}
public boolean canEditBook(String bookId, String username) {
Book book = bookService.findById(bookId);
return book.getOwner().equals(username);
}
}
@Controller
public class BookController {
@QueryMapping
@PreAuthorize("@bookSecurity.canViewBook(#id, authentication.name)")
public Book bookById(@Argument String id) {
return bookService.findById(id);
}
}@Component
public class SecurityContextWebGraphQlInterceptor implements WebGraphQlInterceptor {
@Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, WebGraphQlInterceptorChain chain) {
return ReactiveSecurityContextHolder.getContext()
.cast(SecurityContext.class)
.map(SecurityContext::getAuthentication)
.doOnNext(auth -> {
request.configureExecutionInput((executionInput, builder) ->
builder.graphQLContext(context ->
context.put("authentication", auth)
.put("principal", auth.getPrincipal())
)
);
})
.then(chain.next(request));
}
}@Controller
public class BookController {
@SchemaMapping(typeName = "Book", field = "canEdit")
public boolean canEdit(Book book, DataFetchingEnvironment environment) {
Authentication auth = environment.getGraphQLContext().get("authentication");
return book.getOwner().equals(auth.getName());
}
@QueryMapping
public List<Book> personalizedBooks(DataFetchingEnvironment environment) {
Authentication auth = environment.getGraphQLContext().get("authentication");
return bookService.findPersonalizedFor(auth.getName());
}
}public class SecurityDataFetcherExceptionResolver implements DataFetcherExceptionResolver {
@Override
public Mono<List<GraphQLError>> resolveException(DataFetcherExceptionResolverEnvironment env) {
Throwable exception = env.getException();
if (exception instanceof AccessDeniedException) {
return Mono.just(List.of(
GraphqlErrorBuilder.newError(env)
.message("Access denied")
.errorType(ErrorType.DataFetchingException)
.extensions(Map.of("code", "ACCESS_DENIED"))
.build()
));
}
if (exception instanceof AuthenticationException) {
return Mono.just(List.of(
GraphqlErrorBuilder.newError(env)
.message("Authentication required")
.errorType(ErrorType.DataFetchingException)
.extensions(Map.of("code", "UNAUTHENTICATED"))
.build()
));
}
return Mono.empty();
}
}@Configuration
public class GraphQlWebSocketSecurityConfig {
@Bean
public HandshakeInterceptor authHandshakeInterceptor() {
return new HttpSessionHandshakeInterceptor() {
@Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
// Extract JWT from query parameter or header
String token = extractTokenFromRequest(request);
if (token != null && jwtTokenValidator.validate(token)) {
attributes.put("authentication", createAuthenticationFromToken(token));
return super.beforeHandshake(request, response, wsHandler, attributes);
}
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return false;
}
};
}
}@Controller
public class SubscriptionController {
@SubscriptionMapping
@PreAuthorize("hasRole('USER')")
public Flux<BookEvent> bookUpdates(Authentication authentication) {
return bookEventService.getEventsForUser(authentication.getName())
.filter(event -> hasPermissionToView(event, authentication));
}
@SubscriptionMapping
@PreAuthorize("@subscriptionSecurity.canSubscribeToAuthor(#authorId, authentication.name)")
public Flux<BookEvent> authorBookUpdates(@Argument String authorId) {
return bookEventService.getEventsByAuthor(authorId);
}
}@Configuration
public class OAuth2GraphQlConfig {
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri("https://auth.example.com/.well-known/jwks.json")
.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthorityPrefix("ROLE_");
authoritiesConverter.setAuthoritiesClaimName("roles");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter;
}
}@Controller
public class OAuth2BookController {
@QueryMapping
@PreAuthorize("hasAuthority('SCOPE_read:books')")
public List<Book> books() {
return bookService.findAll();
}
@MutationMapping
@PreAuthorize("hasAuthority('SCOPE_write:books')")
public Book addBook(@Argument BookInput input) {
return bookService.create(input);
}
@MutationMapping
@PreAuthorize("hasAuthority('SCOPE_admin') or (hasAuthority('SCOPE_write:books') and @bookService.isOwner(#id, authentication.name))")
public Book updateBook(@Argument String id, @Argument BookInput input) {
return bookService.update(id, input);
}
}@Configuration
public class GraphQlCsrfConfig {
@Bean
public CsrfTokenRepository csrfTokenRepository() {
CookieCsrfTokenRepository repository = CookieCsrfTokenRepository.withHttpOnlyFalse();
repository.setCookieName("XSRF-TOKEN");
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf
.csrfTokenRepository(csrfTokenRepository())
.csrfTokenRequestHandler(new XorCsrfTokenRequestAttributeHandler())
.ignoringRequestMatchers("/graphql") // Handle CSRF in GraphQL interceptor
)
.build();
}
}@Component
public class CsrfWebGraphQlInterceptor implements WebGraphQlInterceptor {
@Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, WebGraphQlInterceptorChain chain) {
return Mono.fromCallable(() -> {
// Validate CSRF token for mutations
if (isMutation(request)) {
validateCsrfToken(request);
}
return request;
})
.then(chain.next(request));
}
private boolean isMutation(WebGraphQlRequest request) {
return request.getDocument().contains("mutation");
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-springframework-boot--spring-boot-starter-graphql