From 83916143300cfe291c26716c487e3bf7f5b49cd6 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Thu, 4 Dec 2025 14:50:36 +0100 Subject: [PATCH] FINERACT-2326: Add configuration options for CORS handling --- .../core/config/FineractProperties.java | 13 ++++++++ .../core/config/SecurityConfig.java | 22 ++++++++----- .../config/AuthorizationServerConfig.java | 31 ++++++++++++++++++- .../src/main/resources/application.properties | 8 +++++ .../resources/application-test.properties | 8 +++++ 5 files changed, 73 insertions(+), 9 deletions(-) diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java index 551b25242..33ea89ca1 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java @@ -519,6 +519,7 @@ public class FineractProperties { private FineractSecurityTwoFactorAuth twoFactor; private FineractSecurityHsts hsts; private FineractSecurityOAuth2Properties oauth2; + private CorsProperties cors; public void set2fa(FineractSecurityTwoFactorAuth twoFactor) { this.twoFactor = twoFactor; @@ -688,4 +689,16 @@ public class FineractProperties { } } } + + @Getter + @Setter + public static class CorsProperties { + + private boolean enabled; + private List allowedOrigins; + private List allowedMethods; + private List allowedHeaders; + private List exposedHeaders; + private boolean allowCredentials; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java index 6d6c7e508..8295023e0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java @@ -160,8 +160,8 @@ public class SecurityConfig { .requestMatchers(antMatcher("/api/**")) .access(allOf(authorizationManagers.toArray(new AuthorizationManager[0]))); // }).httpBasic((httpBasic) -> httpBasic.authenticationEntryPoint(basicAuthenticationEntryPoint())) // - .cors(Customizer.withDefaults()).csrf(AbstractHttpConfigurer::disable) // NOSONAR only creating a - // service that + .csrf(AbstractHttpConfigurer::disable) // NOSONAR only creating a + // service that // is used by non-browser clients .sessionManagement((smc) -> smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // .addFilterBefore(tenantAwareBasicAuthenticationFilter(), SecurityContextHolderFilter.class) // @@ -189,6 +189,9 @@ public class SecurityConfig { http.requiresChannel(channel -> channel.anyRequest().requiresSecure()).headers( headers -> headers.httpStrictTransportSecurity(hsts -> hsts.includeSubDomains(true).maxAgeInSeconds(31536000))); } + if (fineractProperties.getSecurity().getCors().isEnabled()) { + http.cors(Customizer.withDefaults()); + } return http.build(); } @@ -258,13 +261,16 @@ public class SecurityConfig { @Bean public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOriginPatterns(List.of("*")); - configuration.setAllowedMethods(List.of("*")); - configuration.setAllowCredentials(true); - configuration.setAllowedHeaders(List.of("*")); + CorsConfiguration config = new CorsConfiguration(); + FineractProperties.CorsProperties corsConfiguration = fineractProperties.getSecurity().getCors(); + config.setAllowedOrigins(corsConfiguration.getAllowedOrigins()); + config.setAllowedMethods(corsConfiguration.getAllowedMethods()); + config.setAllowedHeaders(corsConfiguration.getAllowedHeaders()); + config.setExposedHeaders(corsConfiguration.getExposedHeaders()); + config.setAllowCredentials(corsConfiguration.isAllowCredentials()); // if you use cookies / Authorization header + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); + source.registerCorsConfiguration("/**", config); return source; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/config/AuthorizationServerConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/config/AuthorizationServerConfig.java index d9735bb10..f2d130330 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/config/AuthorizationServerConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/config/AuthorizationServerConfig.java @@ -55,6 +55,7 @@ import org.springframework.context.annotation.Scope; import org.springframework.core.annotation.Order; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -78,6 +79,9 @@ import org.springframework.security.web.access.ExceptionTranslationFilter; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.context.SecurityContextHolderFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.OncePerRequestFilter; @Configuration @@ -121,6 +125,10 @@ public class AuthorizationServerConfig { http.securityMatcher("/swagger-ui/**", "/fineract.json", "/actuator/**", "/legacy-docs/apiLive.htm") .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()).csrf(AbstractHttpConfigurer::disable); + if (fineractProperties.getSecurity().getCors().isEnabled()) { + http.cors(Customizer.withDefaults()); + } + return http.build(); } @@ -137,6 +145,10 @@ public class AuthorizationServerConfig { .exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))) .apply(authorizationServerConfigurer); + if (fineractProperties.getSecurity().getCors().isEnabled()) { + http.cors(Customizer.withDefaults()); + } + return http.build(); } @@ -144,7 +156,6 @@ public class AuthorizationServerConfig { @Order(3) public SecurityFilterChain protectedEndpoints(HttpSecurity http) throws Exception { http - // .securityMatcher(new AntPathRequestMatcher("/api/**")) // TODO: Make it configurable .csrf(AbstractHttpConfigurer::disable).authorizeHttpRequests(auth -> { auth.anyRequest().authenticated(); @@ -171,9 +182,27 @@ public class AuthorizationServerConfig { if (fineractProperties.getSecurity().getTwoFactor().isEnabled()) { http.addFilterAfter(twoFactorAuthenticationFilter(), CorrelationHeaderFilter.class); } + if (fineractProperties.getSecurity().getCors().isEnabled()) { + http.cors(Customizer.withDefaults()); + } return http.build(); } + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + FineractProperties.CorsProperties corsConfiguration = fineractProperties.getSecurity().getCors(); + config.setAllowedOrigins(corsConfiguration.getAllowedOrigins()); + config.setAllowedMethods(corsConfiguration.getAllowedMethods()); + config.setAllowedHeaders(corsConfiguration.getAllowedHeaders()); + config.setExposedHeaders(corsConfiguration.getExposedHeaders()); + config.setAllowCredentials(corsConfiguration.isAllowCredentials()); // if you use cookies / Authorization header + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } + @Bean public OncePerRequestFilter tenantAwareAuthenticationFilter() { return new TenantAwareAuthenticationFilter(resolver(), tenantDetailsService); diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index e60d07523..0f641f19a 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -26,6 +26,14 @@ fineract.security.oauth2.enabled=${FINERACT_SECURITY_OAUTH_ENABLED:false} fineract.security.2fa.enabled=${FINERACT_SECURITY_2FA_ENABLED:false} fineract.security.hsts.enabled=${FINERACT_SECURITY_HSTS_ENABLED:false} +#CORS configuration +fineract.security.cors.enabled=${FINERACT_SECURITY_CORS_ENABLED:true} +fineract.security.cors.allowed-origins=${FINERACT_SECURITY_CORS_ALLOWED_ORIGINS:"*"} +fineract.security.cors.allowed-methods=${FINERACT_SECURITY_CORS_ALLOWED_METHODS:"*"} +fineract.security.cors.allowed-headers=${FINERACT_SECURITY_CORS_ALLOWED_HEADERS:"*"} +fineract.security.cors.exposed-headers=${FINERACT_SECURITY_CORS_EXPOSED_HEADERS:"*"} +fineract.security.cors.allow-credentials=${FINERACT_SECURITY_CORS_ALLOW_CREDENTIALS:true} + # EXAMPLE: OAuth2 client configuration (frontend-client) fineract.security.oauth2.client.registrations.frontend-client.client-id=${FINERACT_SECURITY_OAUTH2_CLIENTS_FRONTEND_ID:frontend-client} fineract.security.oauth2.client.registrations.frontend-client.scopes=${FINERACT_SECURITY_OAUTH2_CLIENTS_FRONTEND_SCOPES:read,write} diff --git a/fineract-provider/src/test/resources/application-test.properties b/fineract-provider/src/test/resources/application-test.properties index 23f34f391..a04340206 100644 --- a/fineract-provider/src/test/resources/application-test.properties +++ b/fineract-provider/src/test/resources/application-test.properties @@ -25,6 +25,14 @@ fineract.security.oauth2.enabled=false fineract.security.2fa.enabled=false fineract.security.hsts.enabled=false +#CORS configuration +fineract.security.cors.enabled=true +fineract.security.cors.allowed-origins="*" +fineract.security.cors.allowed-methods="*" +fineract.security.cors.allowed-headers="*" +fineract.security.cors.exposed-headers="*" +fineract.security.cors.allow-credentials=true + fineract.tenant.host=localhost fineract.tenant.port=3306 fineract.tenant.username=root