CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-springframework-boot--spring-boot-starter-graphql

Starter for building GraphQL applications with Spring GraphQL

Pending
Overview
Eval results
Files

security-integration.mddocs/

Security Integration

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.

Spring MVC Security Integration

Auto-configuration for GraphQL security in servlet-based applications.

Auto-Configuration Class

@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();
    }
}

Security Configuration

@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();
    }
}

Spring WebFlux Security Integration

Auto-configuration for reactive GraphQL security.

Auto-Configuration Class

@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();
    }
}

Reactive Security Configuration

@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();
    }
}

Authentication Integration

JWT Authentication

@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());
    }
}

Accessing Security Context

@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());
    }
}

Authorization

Method-Level Security

@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());
    }
}

Custom Security Expressions

@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);
    }
}

Security Context Access

GraphQL Context Integration

@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));
    }
}

Using Context in DataFetchers

@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());
    }
}

Error Handling

Security Exception Resolution

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();
    }
}

WebSocket Security

WebSocket Authentication

@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;
            }
        };
    }
}

Subscription Security

@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);
    }
}

OAuth2 Integration

OAuth2 Resource Server

@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;
    }
}

Scope-Based Authorization

@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);
    }
}

CSRF Protection

CSRF Configuration for GraphQL

@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();
    }
}

CSRF Token in GraphQL Context

@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

docs

configuration-properties.md

core-infrastructure.md

data-integration.md

index.md

observability-integration.md

security-integration.md

testing-support.md

transport-support.md

web-integration.md

tile.json