I have a project in the university and I just have the problem that permitAll is not working properly in the security filter chain.
I have two filter chains, the first is for the UI with keycloak and the second is for public access for “customer” endpoints with apiKey and some should be accessible to everyone, such as for email verification.
I’ve been sitting on this problem for a few days and can’t really find a solution and I hope someone can help me.
Spring Boot version: 3.0.5
Spring Boot Security dependecies:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>6.1.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
My endpoints, which are validated via apiKey, work as expected, but I don’t know if it’s the best solution.
ApiFilter:
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
logger.info("ApiKeyFilter invoked for request: " + request.getRequestURI());
if (ApiContext.isApi()) {
String requestApiKey = request.getHeader("X-API-KEY");
String requestApiSecret = request.getHeader("X-SECRET-KEY");
if (requestApiKey == null || requestApiSecret == null) {
throw new BadCredentialsException("BadCredentials");
}
Optional<ApiInformation> apiInformationOptional = this.apiInformationRepository.findByApiKey(requestApiKey);
if (!apiInformationOptional.isPresent()) {
throw new BadCredentialsException("BadCredentials");
}
ApiInformation apiInformation = apiInformationOptional.get();
if (!apiInformation.getApiKey().equals(requestApiKey) || !apiInformation.getSecretKey().equals(requestApiSecret)) {
throw new BadCredentialsException("BadCredentials");
}
Optional<TenantInformation> tenantInformationOptional = this.tenantInformationRepository.findByOrganization(apiInformation.getOrganization());
if (!tenantInformationOptional.isPresent()) {
throw new BadCredentialsException("BadCredentials");
}
TenantInformation tenantInformation = tenantInformationOptional.get();
Authentication authentication = new UsernamePasswordAuthenticationToken(apiInformation.getApiKey(), null, new ArrayList<>());
SecurityContextHolder.getContext().setAuthentication(authentication);
TenantContext.setCurrentTenant(tenantInformation.getTenantId());
} else if (ApiContext.isActuator()) {
Authentication authentication = new UsernamePasswordAuthenticationToken("GenericUser", null, new ArrayList<>());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
FilterChains:
@Bean
@Order(1)
@DependsOn("corsConfigurationSource")
public SecurityFilterChain apiServerFilterChain(
HttpSecurity http,
@Qualifier("corsConfigurationSource") CorsConfigurationSource corsConfigurationSource
) throws Exception {
http.authorizeHttpRequests((authorize) ->
authorize.requestMatchers(
GenericAbstractControllerInterface.PUBLIC_API_DOC_BASE_URI + "/**",
GenericAbstractControllerInterface.API_PUBLIC_URI + "/**",
).permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(apiFilter, ChannelProcessingFilter.class)
.addFilterBefore(apiKeyFilter, UsernamePasswordAuthenticationFilter.class)
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.httpBasic(withDefaults())
.cors(cors -> {
cors.configurationSource(corsConfigurationSource);
})
.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
@Bean
@Order(0)
public SecurityFilterChain resourceServerFilterChain(
HttpSecurity http,
@Qualifier("corsConfigurationSource") CorsConfigurationSource corsConfigurationSource
) throws Exception {
List<Subscription> subscriptions = subscriptionRepository.findAll();
List<String> allRoles = subscriptions.stream()
.map(subscription -> subscription.getRoles().split(","))
.flatMap(Arrays::stream)
.collect(Collectors.toList());
allRoles.add("ORGANIZATION_ADMIN");
allRoles.add("ORGANIZATION_USER");
http.authorizeHttpRequests(
authorizeRequests -> {
try {
authorizeRequests.requestMatchers(
GenericAbstractControllerInterface.API_PUBLIC_URI + "/**"
).permitAll()
.anyRequest().authenticated().and().oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthConverter);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
);
http.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()).disable());
http.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.httpBasic(withDefaults());
http.cors(cors -> cors.configurationSource(corsConfigurationSource));
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowCredentials(true);
configuration.addAllowedOriginPattern("*");
configuration.setAllowedMethods(Arrays.asList(
HttpMethod.GET.name(),
HttpMethod.POST.name(),
HttpMethod.PUT.name(),
HttpMethod.DELETE.name(),
HttpMethod.OPTIONS.name()
));
configuration.setAllowedHeaders(Arrays.asList("Content-Type", "Authorization", "Access-Control-Allow-Methods", "X-TENANT-ID", "X-API-KEY", "X-ACTUATOR", "X-GUI"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
The ApiFilter just checks where the request comes from and sets a global variable to validate and check other processes.
It sets e.g. if the request goes to the public Api to PUBLIC_API, GUI or API
My endpoints, which are validated via apiKey, work as expected, but I don’t know if it’s the best solution.
ApiFilter:
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
logger.info("ApiKeyFilter invoked for request: " + request.getRequestURI());
if (ApiContext.isApi()) {
String requestApiKey = request.getHeader("X-API-KEY");
String requestApiSecret = request.getHeader("X-SECRET-KEY");
if (requestApiKey == null || requestApiSecret == null) {
throw new BadCredentialsException("BadCredentials");
}
Optional<ApiInformation> apiInformationOptional = this.apiInformationRepository.findByApiKey(requestApiKey);
if (!apiInformationOptional.isPresent()) {
throw new BadCredentialsException("BadCredentials");
}
ApiInformation apiInformation = apiInformationOptional.get();
if (!apiInformation.getApiKey().equals(requestApiKey) || !apiInformation.getSecretKey().equals(requestApiSecret)) {
throw new BadCredentialsException("BadCredentials");
}
Optional<TenantInformation> tenantInformationOptional = this.tenantInformationRepository.findByOrganization(apiInformation.getOrganization());
if (!tenantInformationOptional.isPresent()) {
throw new BadCredentialsException("BadCredentials");
}
TenantInformation tenantInformation = tenantInformationOptional.get();
Authentication authentication = new UsernamePasswordAuthenticationToken(apiInformation.getApiKey(), null, new ArrayList<>());
SecurityContextHolder.getContext().setAuthentication(authentication);
TenantContext.setCurrentTenant(tenantInformation.getTenantId());
} else if (ApiContext.isActuator()) {
Authentication authentication = new UsernamePasswordAuthenticationToken("GenericUser", null, new ArrayList<>());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
FilterChains:
@Bean
@Order(1)
@DependsOn("corsConfigurationSource")
public SecurityFilterChain apiServerFilterChain(
HttpSecurity http,
@Qualifier("corsConfigurationSource") CorsConfigurationSource corsConfigurationSource
) throws Exception {
http.authorizeHttpRequests((authorize) ->
authorize.requestMatchers(
GenericAbstractControllerInterface.PUBLIC_API_DOC_BASE_URI + "/**",
GenericAbstractControllerInterface.API_PUBLIC_URI + "/**",
).permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(apiFilter, ChannelProcessingFilter.class)
.addFilterBefore(apiKeyFilter, UsernamePasswordAuthenticationFilter.class)
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.httpBasic(withDefaults())
.cors(cors -> {
cors.configurationSource(corsConfigurationSource);
})
.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
@Bean
@Order(0)
public SecurityFilterChain resourceServerFilterChain(
HttpSecurity http,
@Qualifier("corsConfigurationSource") CorsConfigurationSource corsConfigurationSource
) throws Exception {
List<Subscription> subscriptions = subscriptionRepository.findAll();
List<String> allRoles = subscriptions.stream()
.map(subscription -> subscription.getRoles().split(","))
.flatMap(Arrays::stream)
.collect(Collectors.toList());
allRoles.add("ORGANIZATION_ADMIN");
allRoles.add("ORGANIZATION_USER");
http.authorizeHttpRequests(
authorizeRequests -> {
try {
authorizeRequests.requestMatchers(
GenericAbstractControllerInterface.API_PUBLIC_URI + "/**"
).permitAll()
.anyRequest().authenticated().and().oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthConverter);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
);
http.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()).disable());
http.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.httpBasic(withDefaults());
http.cors(cors -> cors.configurationSource(corsConfigurationSource));
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowCredentials(true);
configuration.addAllowedOriginPattern("*");
configuration.setAllowedMethods(Arrays.asList(
HttpMethod.GET.name(),
HttpMethod.POST.name(),
HttpMethod.PUT.name(),
HttpMethod.DELETE.name(),
HttpMethod.OPTIONS.name()
));
configuration.setAllowedHeaders(Arrays.asList("Content-Type", "Authorization", "Access-Control-Allow-Methods", "X-TENANT-ID", "X-API-KEY", "X-ACTUATOR", "X-GUI"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
The ApiFilter just checks where the request comes from and sets a global variable to validate and check other processes.
It sets e.g. if the request goes to the public Api to PUBLIC_API, GUI or API