From fa9c7b93642cada11b0b471c3acb2ce07efa44e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CsengeS=C3=B3ti?= Date: Thu, 11 Sep 2025 10:06:37 +0200 Subject: [PATCH] FINERACT-1984: OAuth2.1 --- build.gradle | 1 - .../org.apache.fineract.dependencies.gradle | 3 - fineract-client/build.gradle | 14 +- fineract-client/dependencies.gradle | 6 - .../core/config/FineractProperties.java | 68 +++-- .../OAuth2ExceptionEntryPoint.java | 48 ---- .../appendix/properties-authentication.adoc | 2 +- .../src/docs/en/chapters/security/basic.adoc | 2 +- .../src/docs/en/chapters/security/oauth.adoc | 2 +- fineract-provider/dependencies.gradle | 2 + .../core/config/OAuth2SecurityConfig.java | 182 ------------ .../core/config/SecurityConfig.java | 15 +- .../core/config/SecurityValidationConfig.java | 5 +- .../security/api/LoginController.java | 31 ++ .../security/api/UserDetailsApiResource.java | 5 +- .../config/AuthorizationServerConfig.java | 272 ++++++++++++++++++ ...neractJwtAuthenticationTokenConverter.java | 52 ++++ .../data/TenantAuthenticationDetails.java | 33 +++ .../security/filter/BusinessDateFilter.java | 50 ++++ ...InsecureTwoFactorAuthenticationFilter.java | 78 ----- .../TenantAwareAuthenticationFilter.java | 61 ++++ .../TenantAwareBasicAuthenticationFilter.java | 6 +- .../TenantAwareTenantIdentifierFilter.java | 156 ---------- .../filter/TwoFactorAuthenticationFilter.java | 6 +- ...ice.java => AuthTenantDetailsService.java} | 2 +- ...java => AuthTenantDetailsServiceJdbc.java} | 6 +- ...SpringSecurityPlatformSecurityContext.java | 8 +- .../api/SelfUserDetailsApiResource.java | 2 +- .../src/main/resources/application.properties | 14 +- .../src/main/resources/templates/login.html | 111 +++++++ .../ClasspathDuplicatesStepDefinitions.java | 4 +- .../resources/application-test.properties | 2 +- oauth2-tests/build.gradle | 5 +- .../oauth2tests/OAuth2AuthenticationTest.java | 133 ++++++--- renovate.json | 12 - twofactor-tests/build.gradle | 2 +- 36 files changed, 802 insertions(+), 599 deletions(-) delete mode 100644 fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/OAuth2ExceptionEntryPoint.java delete mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/OAuth2SecurityConfig.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/LoginController.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/config/AuthorizationServerConfig.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/converter/FineractJwtAuthenticationTokenConverter.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/TenantAuthenticationDetails.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/BusinessDateFilter.java delete mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/InsecureTwoFactorAuthenticationFilter.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TenantAwareAuthenticationFilter.java delete mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TenantAwareTenantIdentifierFilter.java rename fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/{BasicAuthTenantDetailsService.java => AuthTenantDetailsService.java} (95%) rename fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/{BasicAuthTenantDetailsServiceJdbc.java => AuthTenantDetailsServiceJdbc.java} (88%) create mode 100644 fineract-provider/src/main/resources/templates/login.html diff --git a/build.gradle b/build.gradle index f566d259b..7c9f4f4e6 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,6 @@ buildscript { jacocoVersion = '0.8.12' retrofitVersion = '2.9.0' okhttpVersion = '4.9.3' - oltuVersion = '1.0.1' fineractCustomProjects = [] fineractJavaProjects = subprojects.findAll{ [ diff --git a/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle b/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle index 4893dfe91..839330786 100644 --- a/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle +++ b/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle @@ -150,9 +150,6 @@ dependencyManagement { dependency "com.squareup.retrofit2:converter-gson:2.11.0" dependency "com.squareup.retrofit2:converter-protobuf:2.11.0" dependency 'io.reactivex.rxjava2:rxjava:2.2.21' - dependency "org.apache.oltu.oauth2:org.apache.oltu.oauth2.common:1.0.1" - dependency "org.apache.oltu.oauth2:org.apache.oltu.oauth2.client:1.0.1" - dependency "org.apache.oltu.oauth2:org.apache.oltu.oauth2.httpclient4:1.0.1" dependency "io.gsonfire:gson-fire:1.9.0" dependency "com.google.code.findbugs:jsr305:3.0.2" dependency "commons-codec:commons-codec:1.17.1" diff --git a/fineract-client/build.gradle b/fineract-client/build.gradle index a079f0a08..3017b6dfd 100644 --- a/fineract-client/build.gradle +++ b/fineract-client/build.gradle @@ -52,7 +52,8 @@ task buildJavaSdk(type: org.openapitools.generator.gradle.plugin.tasks.GenerateT useRxJava2: 'false', library: 'retrofit2', hideGenerationTimestamp: 'true', - containerDefaultToNull: 'true' + containerDefaultToNull: 'true', + oauth2Implementation: 'none' ] generateModelTests = false generateApiTests = false @@ -137,7 +138,16 @@ task cleanupGeneratedJavaFiles() { inputs.dir(tempDir) outputs.dir(targetDir) - + + //TODO: remove harcoded, autogenerated Oltu's OAuthOkHttpClient.java from the generated files + doFirst { + delete fileTree(tempDir) { + include "src/main/java/org/apache/fineract/client/auth/OAuthOkHttpClient.java" + } + delete fileTree(targetDir) { + include "src/main/java/org/apache/fineract/client/auth/OAuthOkHttpClient.java" + } + } doLast { copy { from tempDir diff --git a/fineract-client/dependencies.gradle b/fineract-client/dependencies.gradle index 2f3dbc5a8..aad45729a 100644 --- a/fineract-client/dependencies.gradle +++ b/fineract-client/dependencies.gradle @@ -35,11 +35,5 @@ dependencies { 'com.squareup.okhttp3:logging-interceptor', ) - // org.apache.oltu.oauth2 is used in org.apache.fineract.client.auth.OAuthOkHttpClient (only; can be excluded by consumers not requiring OAuth) - implementation('org.apache.oltu.oauth2:org.apache.oltu.oauth2.client') { - exclude group: 'org.apache.oltu.oauth2', module: 'org.apache.oltu.oauth2.common' - exclude group: 'org.slf4j' - } - testImplementation 'org.assertj:assertj-core' } 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 59384d005..b1855b8d5 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 @@ -19,7 +19,10 @@ package org.apache.fineract.infrastructure.core.config; +import java.io.Serial; +import java.io.Serializable; import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -504,40 +507,63 @@ public class FineractProperties { private FineractSecurityBasicAuth basicauth; private FineractSecurityTwoFactorAuth twoFactor; - private FineractSecurityOAuth oauth; private FineractSecurityHsts hsts; + private FineractSecurityOAuth2Properties oauth2; public void set2fa(FineractSecurityTwoFactorAuth twoFactor) { this.twoFactor = twoFactor; } - } - @Getter - @Setter - public static class FineractSecurityBasicAuth { + @Getter + @Setter + public static class FineractSecurityOAuth2Properties { - private boolean enabled; - } + private boolean enabled; + private ClientProperties client; - @Getter - @Setter - public static class FineractSecurityTwoFactorAuth { + @Getter + @Setter + public static class ClientProperties implements Serializable { - private boolean enabled; - } + @Serial + private static final long serialVersionUID = 1L; + private Map registrations = new HashMap<>(); - @Getter - @Setter - public static class FineractSecurityOAuth { + @Getter + @Setter + public static final class Registration implements Serializable { - private boolean enabled; - } + @Serial + private static final long serialVersionUID = 1L; + private String clientId; + private List scopes = new ArrayList<>(); + private List authorizationGrantTypes = new ArrayList<>(); + private List redirectUris = new ArrayList<>(); + private boolean requireAuthorizationConsent = true; + } + } + } - @Getter - @Setter - public static class FineractSecurityHsts { + @Getter + @Setter + public static class FineractSecurityBasicAuth { - private boolean enabled; + private boolean enabled; + } + + @Getter + @Setter + public static class FineractSecurityTwoFactorAuth { + + private boolean enabled; + } + + @Getter + @Setter + public static class FineractSecurityHsts { + + private boolean enabled; + } } @Getter diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/OAuth2ExceptionEntryPoint.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/OAuth2ExceptionEntryPoint.java deleted file mode 100644 index 451581d96..000000000 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/OAuth2ExceptionEntryPoint.java +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.infrastructure.core.exceptionmapper; - -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse; -import org.apache.fineract.infrastructure.core.exception.ErrorHandler; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.AuthenticationEntryPoint; - -@Slf4j -public class OAuth2ExceptionEntryPoint implements AuthenticationEntryPoint { - - @Override - public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) - throws ServletException { - log.warn("Exception occurred", ErrorHandler.findMostSpecificException(exception)); - ApiGlobalErrorResponse errorResponse = ApiGlobalErrorResponse.unAuthenticated(); - response.setContentType("application/json"); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - try { - ObjectMapper mapper = new ObjectMapper(); - mapper.writeValue(response.getOutputStream(), errorResponse); - } catch (Exception e) { - throw new ServletException(e); - } - } -} diff --git a/fineract-doc/src/docs/en/chapters/appendix/properties-authentication.adoc b/fineract-doc/src/docs/en/chapters/appendix/properties-authentication.adoc index 877281a07..d75fc7efd 100644 --- a/fineract-doc/src/docs/en/chapters/appendix/properties-authentication.adoc +++ b/fineract-doc/src/docs/en/chapters/appendix/properties-authentication.adoc @@ -9,7 +9,7 @@ |true |When set to true, the supported authentication method will be basic authentication. -|fineract.security.oauth.enabled +|fineract.security.oauth2.enabled |FINERACT_SECURITY_OAUTH_ENABLED |false |When set to true, the supported authentication method will be OAuth. diff --git a/fineract-doc/src/docs/en/chapters/security/basic.adoc b/fineract-doc/src/docs/en/chapters/security/basic.adoc index 94881f58b..1f52db41a 100644 --- a/fineract-doc/src/docs/en/chapters/security/basic.adoc +++ b/fineract-doc/src/docs/en/chapters/security/basic.adoc @@ -14,5 +14,5 @@ FINERACT_SECURITY_OAUTH_ENABLED=false + [source,bash] ---- -java -Dfineract.security.basicauth.enabled=true -Dfineract.security.oauth.enabled=false -jar fineract-provider.jar +java -Dfineract.security.basicauth.enabled=true -Dfineract.security.oauth2.enabled=false -jar fineract-provider.jar ---- diff --git a/fineract-doc/src/docs/en/chapters/security/oauth.adoc b/fineract-doc/src/docs/en/chapters/security/oauth.adoc index fbe0a341c..078d8231d 100644 --- a/fineract-doc/src/docs/en/chapters/security/oauth.adoc +++ b/fineract-doc/src/docs/en/chapters/security/oauth.adoc @@ -17,7 +17,7 @@ FINERACT_SERVER_OAUTH_RESOURCE_URL=http://localhost:9000/realms/fineract + [source,bash] ---- -java -Dfineract.security.basicauth.enabled=false -Dfineract.security.oauth.enabled=true -jar fineract-provider.jar +java -Dfineract.security.basicauth.enabled=false -Dfineract.security.oauth2.enabled=true -jar fineract-provider.jar ---- Here's how to test OAuth with https://www.keycloak.org[Keycloak]. diff --git a/fineract-provider/dependencies.gradle b/fineract-provider/dependencies.gradle index 6c112afc6..4b889af93 100644 --- a/fineract-provider/dependencies.gradle +++ b/fineract-provider/dependencies.gradle @@ -55,6 +55,7 @@ dependencies { 'org.springframework.boot:spring-boot-starter-web', 'org.springframework.boot:spring-boot-starter-validation', 'org.springframework.boot:spring-boot-starter-security', + "org.springframework.boot:spring-boot-starter-oauth2-authorization-server", 'org.springframework.boot:spring-boot-starter-cache', 'org.springframework.boot:spring-boot-starter-oauth2-resource-server', 'org.springframework.boot:spring-boot-starter-actuator', @@ -202,6 +203,7 @@ dependencies { implementation 'org.apache.commons:commons-math3' implementation 'io.github.classgraph:classgraph' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // testCompile dependencies are ONLY used in src/test, not src/main. // Do NOT repeat dependencies which are ALREADY in implementation or runtimeOnly! diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/OAuth2SecurityConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/OAuth2SecurityConfig.java deleted file mode 100644 index 0b7346c28..000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/OAuth2SecurityConfig.java +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.fineract.infrastructure.core.config; - -import static org.apache.fineract.infrastructure.security.vote.SelfServiceUserAuthorizationManager.selfServiceUserAuthManager; -import static org.springframework.security.authorization.AuthenticatedAuthorizationManager.fullyAuthenticated; -import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority; -import static org.springframework.security.authorization.AuthorizationManagers.allOf; -import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService; -import org.apache.fineract.infrastructure.cache.service.CacheWritePlatformService; -import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; -import org.apache.fineract.infrastructure.core.exceptionmapper.OAuth2ExceptionEntryPoint; -import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer; -import org.apache.fineract.infrastructure.security.data.FineractJwtAuthenticationToken; -import org.apache.fineract.infrastructure.security.data.PlatformRequestLog; -import org.apache.fineract.infrastructure.security.filter.InsecureTwoFactorAuthenticationFilter; -import org.apache.fineract.infrastructure.security.filter.TenantAwareTenantIdentifierFilter; -import org.apache.fineract.infrastructure.security.filter.TwoFactorAuthenticationFilter; -import org.apache.fineract.infrastructure.security.service.BasicAuthTenantDetailsService; -import org.apache.fineract.infrastructure.security.service.TenantAwareJpaPlatformUserDetailsService; -import org.apache.fineract.infrastructure.security.service.TwoFactorService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.web.ServerProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.convert.converter.Converter; -import org.springframework.http.HttpMethod; -import org.springframework.security.authorization.AuthorizationManager; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.crypto.factory.PasswordEncoderFactories; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.access.intercept.RequestAuthorizationContext; -import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; -import org.springframework.security.web.context.SecurityContextHolderFilter; - -@Configuration -@ConditionalOnProperty("fineract.security.oauth.enabled") -@EnableMethodSecurity -public class OAuth2SecurityConfig { - - @Autowired - private TenantAwareJpaPlatformUserDetailsService userDetailsService; - - @Autowired - private ServerProperties serverProperties; - - @Autowired - private FineractProperties fineractProperties; - - @Autowired - private BasicAuthTenantDetailsService basicAuthTenantDetailsService; - - @Autowired - private ToApiJsonSerializer toApiJsonSerializer; - - @Autowired - private ConfigurationDomainService configurationDomainService; - - @Autowired - private CacheWritePlatformService cacheWritePlatformService; - - @Autowired - private BusinessDateReadPlatformService businessDateReadPlatformService; - @Autowired - private ApplicationContext applicationContext; - - private static final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http // - .securityMatcher(antMatcher("/api/**")).authorizeHttpRequests((auth) -> { - List> authorizationManagers = new ArrayList<>(); - authorizationManagers.add(fullyAuthenticated()); - authorizationManagers.add(hasAuthority("TWOFACTOR_AUTHENTICATED")); - if (fineractProperties.getModule().getSelfService().isEnabled()) { - auth.requestMatchers(antMatcher(HttpMethod.POST, "/api/*/self/authentication")).permitAll() // - .requestMatchers(antMatcher(HttpMethod.POST, "/api/*/self/registration")).permitAll() // - .requestMatchers(antMatcher(HttpMethod.POST, "/api/*/self/registration/user")).permitAll(); // - authorizationManagers.add(selfServiceUserAuthManager()); - } - - auth.requestMatchers(antMatcher(HttpMethod.OPTIONS, "/api/**")).permitAll() // - .requestMatchers(antMatcher(HttpMethod.POST, "/api/*/echo")).permitAll() // - .requestMatchers(antMatcher(HttpMethod.POST, "/api/*/authentication")).permitAll() // - .requestMatchers(antMatcher(HttpMethod.POST, "/api/*/twofactor/validate")).fullyAuthenticated() // - .requestMatchers(antMatcher("/api/*/twofactor")).fullyAuthenticated() // - .requestMatchers(antMatcher("/api/**")) - .access(allOf(authorizationManagers.toArray(new AuthorizationManager[0]))); // - }).csrf(AbstractHttpConfigurer::disable) // NOSONAR only creating a service that is used by non-browser - // clients - .exceptionHandling((ehc) -> ehc.authenticationEntryPoint(new OAuth2ExceptionEntryPoint())) - .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(authenticationConverter())) - .authenticationEntryPoint(new OAuth2ExceptionEntryPoint())) // - .sessionManagement((smc) -> smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // - .addFilterAfter(tenantAwareTenantIdentifierFilter(), SecurityContextHolderFilter.class); - - if (fineractProperties.getSecurity().getTwoFactor().isEnabled()) { - http.addFilterAfter(twoFactorAuthenticationFilter(), BasicAuthenticationFilter.class); - } else { - http.addFilterAfter(insecureTwoFactorAuthenticationFilter(), BasicAuthenticationFilter.class); - } - - if (serverProperties.getSsl().isEnabled()) { - http.requiresChannel(channel -> channel.requestMatchers(antMatcher("/api/**")).requiresSecure()); - } - - if (fineractProperties.getSecurity().getHsts().isEnabled()) { - http.requiresChannel(channel -> channel.anyRequest().requiresSecure()).headers( - headers -> headers.httpStrictTransportSecurity(hsts -> hsts.includeSubDomains(true).maxAgeInSeconds(31536000))); - } - return http.build(); - } - - public TenantAwareTenantIdentifierFilter tenantAwareTenantIdentifierFilter() { - return new TenantAwareTenantIdentifierFilter(basicAuthTenantDetailsService, toApiJsonSerializer, configurationDomainService, - cacheWritePlatformService, businessDateReadPlatformService); - } - - public TwoFactorAuthenticationFilter twoFactorAuthenticationFilter() { - TwoFactorService twoFactorService = applicationContext.getBean(TwoFactorService.class); - return new TwoFactorAuthenticationFilter(twoFactorService); - } - - public InsecureTwoFactorAuthenticationFilter insecureTwoFactorAuthenticationFilter() { - return new InsecureTwoFactorAuthenticationFilter(); - } - - @Bean - public PasswordEncoder passwordEncoder() { - return PasswordEncoderFactories.createDelegatingPasswordEncoder(); - } - - private Converter authenticationConverter() { - return jwt -> { - try { - UserDetails user = userDetailsService.loadUserByUsername(jwt.getSubject()); - jwtGrantedAuthoritiesConverter.setAuthorityPrefix(""); - Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt); - return new FineractJwtAuthenticationToken(jwt, authorities, user); - } catch (UsernameNotFoundException ex) { - throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN), ex); - } - }; - } -} 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 3f723b1a3..6d6c7e508 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 @@ -43,10 +43,9 @@ import org.apache.fineract.infrastructure.instancemode.filter.FineractInstanceMo import org.apache.fineract.infrastructure.jobs.filter.LoanCOBApiFilter; import org.apache.fineract.infrastructure.jobs.filter.LoanCOBFilterHelper; import org.apache.fineract.infrastructure.security.data.PlatformRequestLog; -import org.apache.fineract.infrastructure.security.filter.InsecureTwoFactorAuthenticationFilter; import org.apache.fineract.infrastructure.security.filter.TenantAwareBasicAuthenticationFilter; import org.apache.fineract.infrastructure.security.filter.TwoFactorAuthenticationFilter; -import org.apache.fineract.infrastructure.security.service.BasicAuthTenantDetailsService; +import org.apache.fineract.infrastructure.security.service.AuthTenantDetailsService; import org.apache.fineract.infrastructure.security.service.TenantAwareJpaPlatformUserDetailsService; import org.apache.fineract.infrastructure.security.service.TwoFactorService; import org.apache.fineract.notification.service.UserNotificationService; @@ -103,7 +102,7 @@ public class SecurityConfig { @Autowired private UserNotificationService userNotificationService; @Autowired - private BasicAuthTenantDetailsService basicAuthTenantDetailsService; + private AuthTenantDetailsService basicAuthTenantDetailsService; @Autowired private BusinessDateReadPlatformService businessDateReadPlatformService; @Autowired @@ -122,7 +121,9 @@ public class SecurityConfig { .securityMatcher(antMatcher("/api/**")).authorizeHttpRequests((auth) -> { List> authorizationManagers = new ArrayList<>(); authorizationManagers.add(fullyAuthenticated()); - authorizationManagers.add(hasAuthority("TWOFACTOR_AUTHENTICATED")); + if (fineractProperties.getSecurity().getTwoFactor().isEnabled()) { + authorizationManagers.add(hasAuthority("TWOFACTOR_AUTHENTICATED")); + } if (fineractProperties.getModule().getSelfService().isEnabled()) { auth.requestMatchers(antMatcher(HttpMethod.POST, "/api/*/self/authentication")).permitAll() // .requestMatchers(antMatcher(HttpMethod.POST, "/api/*/self/registration")).permitAll() // @@ -178,8 +179,6 @@ public class SecurityConfig { } if (fineractProperties.getSecurity().getTwoFactor().isEnabled()) { http.addFilterAfter(twoFactorAuthenticationFilter(), CorrelationHeaderFilter.class); - } else { - http.addFilterAfter(insecureTwoFactorAuthenticationFilter(), CorrelationHeaderFilter.class); } if (serverProperties.getSsl().isEnabled()) { @@ -206,10 +205,6 @@ public class SecurityConfig { return new TwoFactorAuthenticationFilter(twoFactorService); } - public InsecureTwoFactorAuthenticationFilter insecureTwoFactorAuthenticationFilter() { - return new InsecureTwoFactorAuthenticationFilter(); - } - public FineractInstanceModeApiFilter fineractInstanceModeApiFilter() { return new FineractInstanceModeApiFilter(fineractProperties); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityValidationConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityValidationConfig.java index 2d6d5244c..24c5a507d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityValidationConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityValidationConfig.java @@ -29,7 +29,7 @@ public class SecurityValidationConfig { @Value("${fineract.security.basicauth.enabled}") private Boolean basicAuthEnabled; - @Value("${fineract.security.oauth.enabled}") + @Value("${fineract.security.oauth2.enabled}") private Boolean oauthEnabled; @PostConstruct @@ -41,8 +41,7 @@ public class SecurityValidationConfig { throw new IllegalArgumentException( "No authentication scheme selected. Please decide if you want to use basic OR OAuth2 authentication."); } - - if (Boolean.TRUE.equals(basicAuthEnabled) && Boolean.TRUE.equals(oauthEnabled)) { + if (basicAuthEnabled && oauthEnabled) { throw new IllegalArgumentException( "Too many authentication schemes selected. Please decide if you want to use basic OR OAuth2 authentication."); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/LoginController.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/LoginController.java new file mode 100644 index 000000000..6651bb767 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/LoginController.java @@ -0,0 +1,31 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.api; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class LoginController { + + @GetMapping("/login") + public String login() { + return "login"; // resolves to src/main/resources/templates/login.html + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/UserDetailsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/UserDetailsApiResource.java index 24a780b32..999615a36 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/UserDetailsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/UserDetailsApiResource.java @@ -53,7 +53,7 @@ import org.springframework.stereotype.Component; */ @Path("/v1/userdetails") @Component -@ConditionalOnProperty("fineract.security.oauth.enabled") +@ConditionalOnProperty("fineract.security.oauth2.enabled") @Tag(name = "Fetch authenticated user details", description = "") @RequiredArgsConstructor public class UserDetailsApiResource { @@ -87,8 +87,7 @@ public class UserDetailsApiResource { } final Collection permissions = new ArrayList<>(); - AuthenticatedOauthUserData authenticatedUserData = new AuthenticatedOauthUserData().setUsername(principal.getUsername()) - .setPermissions(permissions); + AuthenticatedOauthUserData authenticatedUserData; final Collection authorities = new ArrayList<>(authentication.getAuthorities()); for (final GrantedAuthority grantedAuthority : authorities) { 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 new file mode 100644 index 000000000..d9735bb10 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/config/AuthorizationServerConfig.java @@ -0,0 +1,272 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.config; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; +import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService; +import org.apache.fineract.infrastructure.core.config.FineractProperties; +import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder; +import org.apache.fineract.infrastructure.core.filters.CallerIpTrackingFilter; +import org.apache.fineract.infrastructure.core.filters.CorrelationHeaderFilter; +import org.apache.fineract.infrastructure.core.filters.IdempotencyStoreFilter; +import org.apache.fineract.infrastructure.core.filters.IdempotencyStoreHelper; +import org.apache.fineract.infrastructure.core.filters.RequestResponseFilter; +import org.apache.fineract.infrastructure.core.service.MDCWrapper; +import org.apache.fineract.infrastructure.instancemode.filter.FineractInstanceModeApiFilter; +import org.apache.fineract.infrastructure.jobs.filter.LoanCOBApiFilter; +import org.apache.fineract.infrastructure.jobs.filter.LoanCOBFilterHelper; +import org.apache.fineract.infrastructure.security.converter.FineractJwtAuthenticationTokenConverter; +import org.apache.fineract.infrastructure.security.data.TenantAuthenticationDetails; +import org.apache.fineract.infrastructure.security.filter.BusinessDateFilter; +import org.apache.fineract.infrastructure.security.filter.TenantAwareAuthenticationFilter; +import org.apache.fineract.infrastructure.security.filter.TwoFactorAuthenticationFilter; +import org.apache.fineract.infrastructure.security.service.AuthTenantDetailsService; +import org.apache.fineract.infrastructure.security.service.TenantAwareJpaPlatformUserDetailsService; +import org.apache.fineract.infrastructure.security.service.TwoFactorService; +import org.apache.fineract.useradministration.domain.AppUser; +import org.apache.fineract.useradministration.domain.Role; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; +import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; +import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver; +import org.springframework.security.web.SecurityFilterChain; +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.filter.OncePerRequestFilter; + +@Configuration +@EnableWebSecurity +@ConditionalOnProperty("fineract.security.oauth2.enabled") +@EnableConfigurationProperties(FineractProperties.class) +public class AuthorizationServerConfig { + + public static final String TENANT_ID = "tenantId"; + @Autowired + private TenantAwareJpaPlatformUserDetailsService userDetailsService; + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private MDCWrapper mdcWrapper; + + @Autowired(required = false) + private LoanCOBFilterHelper loanCOBFilterHelper; + + @Autowired + private IdempotencyStoreHelper idempotencyStoreHelper; + + @Autowired + private FineractRequestContextHolder fineractRequestContextHolder; + + @Autowired + private FineractProperties fineractProperties; + + @Autowired + private AuthTenantDetailsService tenantDetailsService; + + @Autowired + private BusinessDateReadPlatformService businessDateReadPlatformService; + + @Bean + @Order(1) + public SecurityFilterChain publicEndpoints(HttpSecurity http) throws Exception { + // Public endpoints: permitAll, no JWT + http.securityMatcher("/swagger-ui/**", "/fineract.json", "/actuator/**", "/legacy-docs/apiLive.htm") + .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()).csrf(AbstractHttpConfigurer::disable); + + return http.build(); + } + + @Bean + @Order(2) + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer(); + + http.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher()) // only OAuth2 endpoints + .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) + // TODO: Make it configurable + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) + .exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))) + .apply(authorizationServerConfigurer); + + return http.build(); + } + + @Bean + @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(); + if (fineractProperties.getSecurity().getTwoFactor().isEnabled()) { + auth.anyRequest().hasAuthority("TWOFACTOR_AUTHENTICATED"); + } + }).formLogin(form -> form.loginPage("/login").authenticationDetailsSource(tenantAuthDetailsSource()).permitAll()) + .oauth2ResourceServer( + resourceServer -> resourceServer.jwt(jwt -> jwt.jwtAuthenticationConverter(authenticationConverter()))) + .addFilterAfter(tenantAwareAuthenticationFilter(), SecurityContextHolderFilter.class)// + .addFilterAfter(businessDateFilter(), TenantAwareAuthenticationFilter.class) // + .addFilterAfter(requestResponseFilter(), ExceptionTranslationFilter.class) // + .addFilterAfter(correlationHeaderFilter(), RequestResponseFilter.class) // + .addFilterAfter(fineractInstanceModeApiFilter(), CorrelationHeaderFilter.class); // + if (!Objects.isNull(loanCOBFilterHelper)) { + http.addFilterAfter(loanCOBApiFilter(), FineractInstanceModeApiFilter.class) // + .addFilterAfter(idempotencyStoreFilter(), LoanCOBApiFilter.class); // + } else { + http.addFilterAfter(idempotencyStoreFilter(), FineractInstanceModeApiFilter.class); // + } + if (fineractProperties.getIpTracking().isEnabled()) { + http.addFilterAfter(callerIpTrackingFilter(), RequestResponseFilter.class); + } + if (fineractProperties.getSecurity().getTwoFactor().isEnabled()) { + http.addFilterAfter(twoFactorAuthenticationFilter(), CorrelationHeaderFilter.class); + } + return http.build(); + } + + @Bean + public OncePerRequestFilter tenantAwareAuthenticationFilter() { + return new TenantAwareAuthenticationFilter(resolver(), tenantDetailsService); + } + + @Bean + public OncePerRequestFilter businessDateFilter() { + return new BusinessDateFilter(businessDateReadPlatformService); + } + + @Bean + public BearerTokenResolver resolver() { + return new DefaultBearerTokenResolver(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + + @Bean + public RegisteredClientRepository registeredClientRepository(FineractProperties fineractProperties) { + + List clients = fineractProperties.getSecurity().getOauth2().getClient().getRegistrations().values().stream() + .map(reg -> { + return RegisteredClient.withId(UUID.randomUUID().toString()).clientId(reg.getClientId()) + .clientAuthenticationMethods(methods -> methods.add(ClientAuthenticationMethod.NONE)) + .scopes(scopes -> scopes.addAll(reg.getScopes())) + .authorizationGrantTypes(grants -> reg.getAuthorizationGrantTypes() + .forEach(grant -> grants.add(new AuthorizationGrantType(grant)))) + .redirectUris(uris -> uris.addAll(reg.getRedirectUris())) + .clientSettings( + ClientSettings.builder().requireAuthorizationConsent(reg.isRequireAuthorizationConsent()).build()) + .build(); + }).toList(); + + return new InMemoryRegisteredClientRepository(clients); + } + + @Bean + @Scope("prototype") + public AuthenticationDetailsSource tenantAuthDetailsSource() { + return request -> { + String tenantId = request.getParameter(TENANT_ID); + String username = request.getParameter(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY); // "username" + String password = request.getParameter(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY); // "password" + return new TenantAuthenticationDetails(username, tenantId, password); + }; + } + + @Bean + public OAuth2TokenCustomizer tokenCustomizer() { + return context -> { + UsernamePasswordAuthenticationToken authentication = context.getPrincipal(); + TenantAuthenticationDetails details = (TenantAuthenticationDetails) authentication.getDetails(); + AppUser appUser = (AppUser) authentication.getPrincipal(); + List roles = appUser.getRoles().stream().map(Role::getName).toList(); + List scope = appUser.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()); + context.getClaims().claim("scope", scope).claim("role", roles).claim("tenant", details.getTenantId()); + }; + } + + @Bean + public FineractJwtAuthenticationTokenConverter authenticationConverter() { + return new FineractJwtAuthenticationTokenConverter(userDetailsService); + } + + public RequestResponseFilter requestResponseFilter() { + return new RequestResponseFilter(); + } + + public LoanCOBApiFilter loanCOBApiFilter() { + return new LoanCOBApiFilter(loanCOBFilterHelper); + } + + public TwoFactorAuthenticationFilter twoFactorAuthenticationFilter() { + TwoFactorService twoFactorService = applicationContext.getBean(TwoFactorService.class); + return new TwoFactorAuthenticationFilter(twoFactorService); + } + + public FineractInstanceModeApiFilter fineractInstanceModeApiFilter() { + return new FineractInstanceModeApiFilter(fineractProperties); + } + + public IdempotencyStoreFilter idempotencyStoreFilter() { + return new IdempotencyStoreFilter(fineractRequestContextHolder, idempotencyStoreHelper, fineractProperties); + } + + public CorrelationHeaderFilter correlationHeaderFilter() { + return new CorrelationHeaderFilter(fineractProperties, mdcWrapper); + } + + public CallerIpTrackingFilter callerIpTrackingFilter() { + return new CallerIpTrackingFilter(fineractProperties); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/converter/FineractJwtAuthenticationTokenConverter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/converter/FineractJwtAuthenticationTokenConverter.java new file mode 100644 index 000000000..900a8b3c6 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/converter/FineractJwtAuthenticationTokenConverter.java @@ -0,0 +1,52 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.converter; + +import java.util.Collection; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.security.data.FineractJwtAuthenticationToken; +import org.apache.fineract.infrastructure.security.service.TenantAwareJpaPlatformUserDetailsService; +import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.NonNull; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; + +@RequiredArgsConstructor +public class FineractJwtAuthenticationTokenConverter implements Converter { + + private final TenantAwareJpaPlatformUserDetailsService userDetailsService; + + @Override + @NonNull + public FineractJwtAuthenticationToken convert(@NonNull Jwt jwt) { + try { + UserDetails user = userDetailsService.loadUserByUsername(jwt.getSubject()); + Collection authorities = new JwtGrantedAuthoritiesConverter().convert(jwt); + return new FineractJwtAuthenticationToken(jwt, authorities, user); + } catch (UsernameNotFoundException ex) { + throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN), ex); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/TenantAuthenticationDetails.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/TenantAuthenticationDetails.java new file mode 100644 index 000000000..972f3b6cf --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/TenantAuthenticationDetails.java @@ -0,0 +1,33 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.data; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@AllArgsConstructor +@Accessors(chain = true) +public class TenantAuthenticationDetails { + + private String userName; + private String tenantId; + private String password; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/BusinessDateFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/BusinessDateFilter.java new file mode 100644 index 000000000..a9d9a5ee3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/BusinessDateFilter.java @@ -0,0 +1,50 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.time.LocalDate; +import java.util.HashMap; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.springframework.lang.NonNull; +import org.springframework.web.filter.OncePerRequestFilter; + +@RequiredArgsConstructor +public class BusinessDateFilter extends OncePerRequestFilter { + + private final BusinessDateReadPlatformService businessDateReadPlatformService; + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + if (ThreadLocalContextUtil.getTenant() != null) { + HashMap businessDates = businessDateReadPlatformService.getBusinessDates(); + ThreadLocalContextUtil.setBusinessDates(businessDates); + } + + filterChain.doFilter(request, response); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/InsecureTwoFactorAuthenticationFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/InsecureTwoFactorAuthenticationFilter.java deleted file mode 100644 index 85ef766fd..000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/InsecureTwoFactorAuthenticationFilter.java +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.infrastructure.security.filter; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import org.apache.fineract.infrastructure.security.data.FineractJwtAuthenticationToken; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; - -/** - * A dummy {@link TwoFactorAuthenticationFilter} filter used when 'twofactor' environment profile is not active. - * - * This filter adds 'TWOFACTOR_AUTHENTICATED' authority to every authenticated platform user. - */ -public class InsecureTwoFactorAuthenticationFilter extends TwoFactorAuthenticationFilter { - - public InsecureTwoFactorAuthenticationFilter() { - super(null); - } - - @Override - public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { - - SecurityContext context = SecurityContextHolder.getContext(); - Authentication authentication = null; - if (context != null) { - authentication = context.getAuthentication(); - } - - // Add two-factor authenticated authority if user is authenticated - if (authentication != null && authentication.isAuthenticated()) { - List updatedAuthorities = new ArrayList<>(authentication.getAuthorities()); - updatedAuthorities.add(new SimpleGrantedAuthority("TWOFACTOR_AUTHENTICATED")); - - if (authentication instanceof UsernamePasswordAuthenticationToken) { - UsernamePasswordAuthenticationToken updatedAuthentication = new UsernamePasswordAuthenticationToken( - authentication.getPrincipal(), authentication.getCredentials(), updatedAuthorities); - context.setAuthentication(updatedAuthentication); - } else if (authentication instanceof FineractJwtAuthenticationToken) { - FineractJwtAuthenticationToken fineractJwtAuthenticationToken = (FineractJwtAuthenticationToken) authentication; - FineractJwtAuthenticationToken updatedAuthentication = new FineractJwtAuthenticationToken( - fineractJwtAuthenticationToken.getToken(), (Collection) updatedAuthorities, - (UserDetails) authentication.getPrincipal()); - context.setAuthentication(updatedAuthentication); - } - } - - chain.doFilter(req, res); - } -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TenantAwareAuthenticationFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TenantAwareAuthenticationFilter.java new file mode 100644 index 000000000..133ba1b29 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TenantAwareAuthenticationFilter.java @@ -0,0 +1,61 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.filter; + +import com.nimbusds.jwt.JWTParser; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.infrastructure.security.service.AuthTenantDetailsService; +import org.springframework.lang.NonNull; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; +import org.springframework.web.filter.OncePerRequestFilter; + +@RequiredArgsConstructor +public class TenantAwareAuthenticationFilter extends OncePerRequestFilter { + + private final BearerTokenResolver resolver; + private final AuthTenantDetailsService tenantDetailsService; + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + try { + String token = resolver.resolve(request); + String tenantId; + if (token != null) { + var jwt = JWTParser.parse(token); // not validated here! + var claims = jwt.getJWTClaimsSet(); + tenantId = (String) claims.getClaim("tenant"); + } else { + tenantId = request.getParameter("tenantId"); + } + ThreadLocalContextUtil.setTenant(tenantDetailsService.loadTenantById(tenantId, false)); + filterChain.doFilter(request, response); + } catch (Exception e) { + filterChain.doFilter(request, response); // don't block; real auth will fail later if token is bad + } finally { + ThreadLocalContextUtil.reset(); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TenantAwareBasicAuthenticationFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TenantAwareBasicAuthenticationFilter.java index 498e3b064..19e8a2f8a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TenantAwareBasicAuthenticationFilter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TenantAwareBasicAuthenticationFilter.java @@ -39,7 +39,7 @@ import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.infrastructure.security.data.PlatformRequestLog; import org.apache.fineract.infrastructure.security.exception.InvalidTenantIdentifierException; -import org.apache.fineract.infrastructure.security.service.BasicAuthTenantDetailsService; +import org.apache.fineract.infrastructure.security.service.AuthTenantDetailsService; import org.apache.fineract.notification.service.UserNotificationService; import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.security.authentication.AuthenticationManager; @@ -75,7 +75,7 @@ public class TenantAwareBasicAuthenticationFilter extends BasicAuthenticationFil private final ConfigurationDomainService configurationDomainService; private final CacheWritePlatformService cacheWritePlatformService; private final UserNotificationService userNotificationService; - private final BasicAuthTenantDetailsService basicAuthTenantDetailsService; + private final AuthTenantDetailsService basicAuthTenantDetailsService; private final BusinessDateReadPlatformService businessDateReadPlatformService; @Setter @@ -84,7 +84,7 @@ public class TenantAwareBasicAuthenticationFilter extends BasicAuthenticationFil public TenantAwareBasicAuthenticationFilter(final AuthenticationManager authenticationManager, final AuthenticationEntryPoint authenticationEntryPoint, ToApiJsonSerializer toApiJsonSerializer, ConfigurationDomainService configurationDomainService, CacheWritePlatformService cacheWritePlatformService, - UserNotificationService userNotificationService, BasicAuthTenantDetailsService basicAuthTenantDetailsService, + UserNotificationService userNotificationService, AuthTenantDetailsService basicAuthTenantDetailsService, BusinessDateReadPlatformService businessDateReadPlatformService) { super(authenticationManager, authenticationEntryPoint); this.toApiJsonSerializer = toApiJsonSerializer; diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TenantAwareTenantIdentifierFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TenantAwareTenantIdentifierFilter.java deleted file mode 100644 index a321f6180..000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TenantAwareTenantIdentifierFilter.java +++ /dev/null @@ -1,156 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.infrastructure.security.filter; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.time.LocalDate; -import java.util.HashMap; -import java.util.concurrent.atomic.AtomicBoolean; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.time.StopWatch; -import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; -import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService; -import org.apache.fineract.infrastructure.cache.domain.CacheType; -import org.apache.fineract.infrastructure.cache.service.CacheWritePlatformService; -import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; -import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; -import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer; -import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; -import org.apache.fineract.infrastructure.security.data.PlatformRequestLog; -import org.apache.fineract.infrastructure.security.exception.InvalidTenantIdentifierException; -import org.apache.fineract.infrastructure.security.service.BasicAuthTenantDetailsService; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.filter.GenericFilterBean; - -/** - * - * This filter is responsible for extracting multi-tenant from the request and setting Cross-Origin details to response. - * - * If multi-tenant are valid, the details of the tenant are stored in {@link FineractPlatformTenant} and stored in a - * {@link ThreadLocal} variable for this request using {@link ThreadLocalContextUtil}. - * - * If multi-tenant are invalid, a http error response is returned. - * - * Used to support Oauth2 authentication and the service is loaded only when "oauth" profile is active. - */ -@RequiredArgsConstructor -@Slf4j -public class TenantAwareTenantIdentifierFilter extends GenericFilterBean { - - private static final AtomicBoolean FIRST_PROCESSED_REQUEST = new AtomicBoolean(); - - private final BasicAuthTenantDetailsService basicAuthTenantDetailsService; - private final ToApiJsonSerializer toApiJsonSerializer; - private final ConfigurationDomainService configurationDomainService; - private final CacheWritePlatformService cacheWritePlatformService; - - private final BusinessDateReadPlatformService businessDateReadPlatformService; - - private static final String TENANT_ID_REQUEST_HEADER = "Fineract-Platform-TenantId"; - private static final boolean EXCEPTION_IF_HEADER_MISSING = true; - private static final String API_URI = "/api/v1/"; - - @Override - @SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT") - public void doFilter(final ServletRequest req, final ServletResponse res, final FilterChain chain) - throws IOException, ServletException { - - final HttpServletRequest request = (HttpServletRequest) req; - final HttpServletResponse response = (HttpServletResponse) res; - - final StopWatch task = new StopWatch(); - task.start(); - - try { - ThreadLocalContextUtil.reset(); - // allows for Cross-Origin - // Requests (CORs) to be performed against the platform API. - response.setHeader("Access-Control-Allow-Origin", "*"); // NOSONAR - response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); - final String reqHead = request.getHeader("Access-Control-Request-Headers"); - - if (null != reqHead && !reqHead.isEmpty()) { - response.setHeader("Access-Control-Allow-Headers", reqHead); - } - - if (!"OPTIONS".equalsIgnoreCase(request.getMethod())) { - - String tenantIdentifier = request.getHeader(TENANT_ID_REQUEST_HEADER); - if (org.apache.commons.lang3.StringUtils.isBlank(tenantIdentifier)) { - tenantIdentifier = request.getParameter("tenantIdentifier"); - } - - if (tenantIdentifier == null && EXCEPTION_IF_HEADER_MISSING) { - throw new InvalidTenantIdentifierException("No tenant identifier found: Add request header of '" - + TENANT_ID_REQUEST_HEADER + "' or add the parameter 'tenantIdentifier' to query string of request URL."); - } - - String pathInfo = request.getRequestURI(); - boolean isReportRequest = false; - if (pathInfo != null && pathInfo.contains("report")) { - isReportRequest = true; - } - final FineractPlatformTenant tenant = basicAuthTenantDetailsService.loadTenantById(tenantIdentifier, isReportRequest); - ThreadLocalContextUtil.setTenant(tenant); - HashMap businessDates = businessDateReadPlatformService.getBusinessDates(); - ThreadLocalContextUtil.setBusinessDates(businessDates); - String authToken = request.getHeader("Authorization"); - - if (authToken != null && authToken.startsWith("bearer ")) { - ThreadLocalContextUtil.setAuthToken(authToken.replaceFirst("bearer ", "")); - } - - if (!FIRST_PROCESSED_REQUEST.get()) { - final String baseUrl = request.getRequestURL().toString().replace(request.getRequestURI(), - request.getContextPath() + API_URI); - System.setProperty("baseUrl", baseUrl); - - final boolean ehcacheEnabled = configurationDomainService.isEhcacheEnabled(); - if (ehcacheEnabled) { - cacheWritePlatformService.switchToCache(CacheType.SINGLE_NODE); - } else { - cacheWritePlatformService.switchToCache(CacheType.NO_CACHE); - } - FIRST_PROCESSED_REQUEST.set(true); - } - chain.doFilter(request, response); - } - } catch (final InvalidTenantIdentifierException e) { - // deal with exception at low level - SecurityContextHolder.getContext().setAuthentication(null); - - response.addHeader("WWW-Authenticate", "Basic realm=\"" + "Fineract Platform API" + "\""); - response.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage()); - } finally { - ThreadLocalContextUtil.reset(); - task.stop(); - final PlatformRequestLog logRequest = PlatformRequestLog.from(task, request); - log.debug("{}", toApiJsonSerializer.serialize(logRequest)); - } - - } -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TwoFactorAuthenticationFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TwoFactorAuthenticationFilter.java index 6c6d382a5..5c63c3c84 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TwoFactorAuthenticationFilter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TwoFactorAuthenticationFilter.java @@ -26,7 +26,6 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; -import java.util.Collection; import java.util.List; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.security.constants.TwoFactorConstants; @@ -45,7 +44,7 @@ import org.springframework.web.filter.GenericFilterBean; /** * This filter is responsible for handling two-factor authentication. The filter is enabled when 'twofactor' environment - * profile is active, otherwise {@link InsecureTwoFactorAuthenticationFilter} is used. + * profile is active. * * This filter validates an access-token provided as a header 'Fineract-Platform-TFA-Token'. If a valid token is * provided, a 'TWOFACTOR_AUTHENTICATED' authority is added to the current authentication. If an invalid(non-existent or @@ -116,8 +115,7 @@ public class TwoFactorAuthenticationFilter extends GenericFilterBean { } else if (currentAuthentication instanceof FineractJwtAuthenticationToken) { FineractJwtAuthenticationToken fineractJwtAuthenticationToken = (FineractJwtAuthenticationToken) currentAuthentication; FineractJwtAuthenticationToken updatedAuthentication = new FineractJwtAuthenticationToken( - fineractJwtAuthenticationToken.getToken(), (Collection) updatedAuthorities, - (UserDetails) currentAuthentication.getPrincipal()); + fineractJwtAuthenticationToken.getToken(), updatedAuthorities, (UserDetails) currentAuthentication.getPrincipal()); return updatedAuthentication; } else { throw new ServletException("Unknown authentication type: " + currentAuthentication.getClass().getName()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/BasicAuthTenantDetailsService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/AuthTenantDetailsService.java similarity index 95% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/BasicAuthTenantDetailsService.java rename to fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/AuthTenantDetailsService.java index ebda0827a..d5ab73213 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/BasicAuthTenantDetailsService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/AuthTenantDetailsService.java @@ -20,7 +20,7 @@ package org.apache.fineract.infrastructure.security.service; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; -public interface BasicAuthTenantDetailsService { +public interface AuthTenantDetailsService { FineractPlatformTenant loadTenantById(String tenantId, boolean isReport); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/BasicAuthTenantDetailsServiceJdbc.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/AuthTenantDetailsServiceJdbc.java similarity index 88% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/BasicAuthTenantDetailsServiceJdbc.java rename to fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/AuthTenantDetailsServiceJdbc.java index 88256014b..10cb41636 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/BasicAuthTenantDetailsServiceJdbc.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/AuthTenantDetailsServiceJdbc.java @@ -30,16 +30,16 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; /** - * A JDBC implementation of {@link BasicAuthTenantDetailsService} for loading a tenants details by a + * A JDBC implementation of {@link AuthTenantDetailsService} for loading a tenants details by a * tenantIdentifier. */ @Service -public class BasicAuthTenantDetailsServiceJdbc implements BasicAuthTenantDetailsService { +public class AuthTenantDetailsServiceJdbc implements AuthTenantDetailsService { private final JdbcTemplate jdbcTemplate; @Autowired - public BasicAuthTenantDetailsServiceJdbc(@Qualifier("hikariTenantDataSource") final DataSource dataSource) { + public AuthTenantDetailsServiceJdbc(@Qualifier("hikariTenantDataSource") final DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/SpringSecurityPlatformSecurityContext.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/SpringSecurityPlatformSecurityContext.java index 3775c2eac..31630595f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/SpringSecurityPlatformSecurityContext.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/SpringSecurityPlatformSecurityContext.java @@ -21,6 +21,7 @@ package org.apache.fineract.infrastructure.security.service; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; +import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; @@ -29,7 +30,6 @@ import org.apache.fineract.infrastructure.security.exception.NoAuthorizationExce import org.apache.fineract.infrastructure.security.exception.ResetPasswordException; import org.apache.fineract.useradministration.domain.AppUser; import org.apache.fineract.useradministration.exception.UnAuthenticatedUserException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; @@ -40,6 +40,7 @@ import org.springframework.stereotype.Service; */ @Service +@RequiredArgsConstructor public class SpringSecurityPlatformSecurityContext implements PlatformSecurityContext { // private static final Logger LOG = @@ -50,11 +51,6 @@ public class SpringSecurityPlatformSecurityContext implements PlatformSecurityCo protected static final List EXEMPT_FROM_PASSWORD_RESET_CHECK = new ArrayList( List.of(new CommandWrapperBuilder().updateUser(null).build())); - @Autowired - SpringSecurityPlatformSecurityContext(final ConfigurationDomainService configurationDomainService) { - this.configurationDomainService = configurationDomainService; - } - @Override public AppUser authenticatedUser() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/security/api/SelfUserDetailsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/security/api/SelfUserDetailsApiResource.java index 9b05d59dc..b387c7edd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/security/api/SelfUserDetailsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/security/api/SelfUserDetailsApiResource.java @@ -37,7 +37,7 @@ import org.springframework.stereotype.Component; @Path("/v1/self/userdetails") @Component -@ConditionalOnProperty("fineract.security.oauth.enabled") +@ConditionalOnProperty("fineract.security.oauth2.enabled") @Tag(name = "Self User Details", description = "") @RequiredArgsConstructor @Conditional(SelfServiceModuleIsEnabledCondition.class) diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index 38c92e467..18aa48065 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -22,10 +22,17 @@ application.title=Apache Fineract fineract.node-id=${FINERACT_NODE_ID:1} fineract.security.basicauth.enabled=${FINERACT_SECURITY_BASICAUTH_ENABLED:true} -fineract.security.oauth.enabled=${FINERACT_SECURITY_OAUTH_ENABLED:false} +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} +# 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} +fineract.security.oauth2.client.registrations.frontend-client.authorization-grant-types=${FINERACT_SECURITY_OAUTH2_CLIENTS_FRONTEND_GRANTS:authorization_code,refresh_token} +fineract.security.oauth2.client.registrations.frontend-client.redirect-uris=${FINERACT_SECURITY_OAUTH2_CLIENTS_FRONTEND_REDIRECT:http://localhost:3000/callback} +fineract.security.oauth2.client.registrations.frontend-client.require-authorization-consent=${FINERACT_SECURITY_OAUTH2_CLIENTS_FRONTEND_CONSENT:false} + fineract.tenant.host=${FINERACT_DEFAULT_TENANTDB_HOSTNAME:localhost} fineract.tenant.port=${FINERACT_DEFAULT_TENANTDB_PORT:3306} fineract.tenant.username=${FINERACT_DEFAULT_TENANTDB_UID:root} @@ -375,11 +382,6 @@ server.tomcat.max-keep-alive-requests=${FINERACT_SERVER_TOMCAT_MAX_KEEP_ALIVE_RE server.tomcat.threads.max=${FINERACT_SERVER_TOMCAT_THREADS_MAX:200} server.tomcat.threads.min-spare=${FINERACT_SERVER_TOMCAT_THREADS_MIN_SPARE:10} server.tomcat.mbeanregistry.enabled=${FINERACT_SERVER_TOMCAT_MBEANREGISTRY_ENABLED:false} - -# OAuth authorisation server endpoint -# note some provider URLs exclude "auth/", e.g. keycloak v26 -spring.security.oauth2.resourceserver.jwt.issuer-uri=${FINERACT_SERVER_OAUTH_RESOURCE_URL:http://localhost:9000/auth/realms/fineract} - spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.mariadb.jdbc.Driver} spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:mariadb://localhost:3306/fineract_tenants} spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:root} diff --git a/fineract-provider/src/main/resources/templates/login.html b/fineract-provider/src/main/resources/templates/login.html new file mode 100644 index 000000000..63e40597d --- /dev/null +++ b/fineract-provider/src/main/resources/templates/login.html @@ -0,0 +1,111 @@ + + + + + Sign In + + + +
+ +
+ + diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classpath/ClasspathDuplicatesStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classpath/ClasspathDuplicatesStepDefinitions.java index d7a0b7bdb..f24f6d95d 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classpath/ClasspathDuplicatesStepDefinitions.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classpath/ClasspathDuplicatesStepDefinitions.java @@ -204,6 +204,8 @@ public class ClasspathDuplicatesStepDefinitions implements En { // Pentaho reports harmless duplicates || resourcePath.endsWith("overview.html") // || resourcePath.endsWith("classic-engine.properties") // - || resourcePath.endsWith("loader.properties"); // + || resourcePath.endsWith("loader.properties") // + // ProGuard configuration files are safe to have multiple versions of + || resourcePath.equals("META-INF/proguard/gson.pro"); // } } diff --git a/fineract-provider/src/test/resources/application-test.properties b/fineract-provider/src/test/resources/application-test.properties index d7c5d8fc6..3790d7d26 100644 --- a/fineract-provider/src/test/resources/application-test.properties +++ b/fineract-provider/src/test/resources/application-test.properties @@ -21,7 +21,7 @@ fineract.node-id=1 fineract.security.basicauth.enabled=true fineract.ip-tracking.enabled=false -fineract.security.oauth.enabled=false +fineract.security.oauth2.enabled=false fineract.security.2fa.enabled=false fineract.security.hsts.enabled=false diff --git a/oauth2-tests/build.gradle b/oauth2-tests/build.gradle index 3821b71a5..cc8995b19 100644 --- a/oauth2-tests/build.gradle +++ b/oauth2-tests/build.gradle @@ -35,6 +35,9 @@ configurations { } dependencies { driver 'com.mysql:mysql-connector-j' + testImplementation 'org.seleniumhq.selenium:selenium-java:4.21.0' + testImplementation 'io.github.bonigarcia:webdrivermanager:5.5.1' + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' } cargo { @@ -57,7 +60,7 @@ cargo { startStopTimeout = 240000 sharedClasspath = configurations.driver containerProperties { - def jvmArgs = '--add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED -Dfineract.security.basicauth.enabled=false -Dfineract.security.oauth.enabled=true -Dfineract.security.2fa.enabled=false ' + def jvmArgs = '--add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED -Dfineract.security.basicauth.enabled=false -Dfineract.security.oauth2.enabled=true -Dfineract.security.2fa.enabled=false ' if (project.hasProperty('dbType')) { if ('postgresql'.equalsIgnoreCase(dbType)) { jvmArgs += '-Dspring.datasource.hikari.driverClassName=org.postgresql.Driver -Dspring.datasource.hikari.jdbcUrl=jdbc:postgresql://localhost:5432/fineract_tenants -Dspring.datasource.hikari.username=root -Dspring.datasource.hikari.password=postgres -Dfineract.tenant.host=localhost -Dfineract.tenant.port=5432 -Dfineract.tenant.username=root -Dfineract.tenant.password=postgres' diff --git a/oauth2-tests/src/test/java/org/apache/fineract/oauth2tests/OAuth2AuthenticationTest.java b/oauth2-tests/src/test/java/org/apache/fineract/oauth2tests/OAuth2AuthenticationTest.java index 391ba7e03..ed2db78a2 100644 --- a/oauth2-tests/src/test/java/org/apache/fineract/oauth2tests/OAuth2AuthenticationTest.java +++ b/oauth2-tests/src/test/java/org/apache/fineract/oauth2tests/OAuth2AuthenticationTest.java @@ -20,9 +20,11 @@ package org.apache.fineract.oauth2tests; import static io.restassured.RestAssured.given; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; +import com.google.common.base.Splitter; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; import io.restassured.RestAssured; import io.restassured.builder.RequestSpecBuilder; import io.restassured.builder.ResponseSpecBuilder; @@ -31,10 +33,21 @@ import io.restassured.path.json.JsonPath; import io.restassured.response.Response; import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; -import jakarta.mail.MessagingException; import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; +import org.springframework.lang.NonNull; public class OAuth2AuthenticationTest { @@ -75,49 +88,22 @@ public class OAuth2AuthenticationTest { @Test public void testAccessWithoutAuthentication() { - performServerGet(requestSpec, responseSpec401, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, ""); + performServerGet(requestSpec, responseSpec401, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, null); } @Test - public void testOAuth2Login() throws IOException, MessagingException { + public void testGetOAuth2UserDetails() throws IOException, InterruptedException { + performServerGet(requestSpec, responseSpec401, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, null); - performServerGet(requestSpec, responseSpec401, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, ""); - - String accessToken = performServerPost(requestFormSpec, responseSpec, "http://localhost:9000/auth/realms/fineract/token", - "grant_type=client_credentials&client_id=community-app&client_secret=123123", "access_token"); - assertNotNull(accessToken); - - String bearerToken = performServerPost(requestFormSpec, responseSpec, "http://localhost:9000/auth/realms/fineract/token", - "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=" + accessToken - + "&client_id=community-app&scope=fineract", - "access_token"); - assertNotNull(bearerToken); + String token = loginAndClaimToken( + "https://localhost:8443/fineract-provider/oauth2/authorize" + "?response_type=code&client_id=frontend-client" + + "&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&scope=read&state=xyz" + + "&code_challenge=zudG_xkz8WrPPMq2MwmFP-NRvNapCL0OD-xYWRapTsU" + "&code_challenge_method=S256", + requestFormSpec); RequestSpecification requestSpecWithToken = new RequestSpecBuilder() // .setContentType(ContentType.JSON) // - .addHeader("Authorization", "Bearer " + bearerToken) // - .build(); - - performServerGet(requestSpecWithToken, responseSpec, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, ""); - } - - @Test - public void testGetOAuth2UserDetails() { - performServerGet(requestSpec, responseSpec401, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, ""); - - String accessToken = performServerPost(requestFormSpec, responseSpec, "http://localhost:9000/auth/realms/fineract/token", - "grant_type=client_credentials&client_id=community-app&client_secret=123123", "access_token"); - assertNotNull(accessToken); - - String bearerToken = performServerPost(requestFormSpec, responseSpec, "http://localhost:9000/auth/realms/fineract/token", - "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=" + accessToken - + "&client_id=community-app&scope=fineract", - "access_token"); - assertNotNull(bearerToken); - - RequestSpecification requestSpecWithToken = new RequestSpecBuilder() // - .setContentType(ContentType.JSON) // - .addHeader("Authorization", "Bearer " + bearerToken) // + .addHeader("Authorization", "Bearer " + token) // .build(); Boolean authenticationCheck = performServerGet(requestSpecWithToken, responseSpec, @@ -149,11 +135,73 @@ public class OAuth2AuthenticationTest { } catch (Exception e) { Thread.sleep(3000); } + attempt++; } while (attempt < max_attempts); fail(HEALTH_URL + " returned " + response.prettyPrint()); } + public String loginAndClaimToken(String url, RequestSpecification requestSpec) throws IOException { + CompletableFuture futureToken = new CompletableFuture<>(); + HttpServer server = HttpServer.create(new InetSocketAddress(3000), 0); + server.createContext("/callback", exchange -> { + try { + String token = claimTokenOnCallback(requestSpec, exchange); + futureToken.complete(token); // complete future with value + exchange.sendResponseHeaders(200, 0); + } catch (Exception e) { + futureToken.completeExceptionally(e); // propagate exception + } finally { + exchange.close(); + } + }); + server.start(); + WebDriver driver = getWebDriver(); + try { + driver.get(url); + driver.findElement(By.name("username")).sendKeys("mifos"); + driver.findElement(By.name("password")).sendKeys("password"); + driver.findElement(By.name("tenantId")).sendKeys("default"); + driver.findElement(By.cssSelector("button[type='submit'], input[type='submit']")).click(); + return futureToken.get(5, TimeUnit.SECONDS); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + driver.quit(); + server.stop(0); + } + } + + @NonNull + private static WebDriver getWebDriver() { + ChromeOptions options = new ChromeOptions(); + options.addArguments("--headless"); // run in headless mode + options.addArguments("--no-sandbox"); + options.addArguments("--disable-dev-shm-usage"); + options.addArguments("--ignore-certificate-errors"); + return new ChromeDriver(options); + } + + private String claimTokenOnCallback(RequestSpecification requestSpec, HttpExchange exchange) { + String query = exchange.getRequestURI().getQuery(); + String code = null; + if (query != null) { + for (String param : Splitter.on("&").split(query)) { + List keyValue = Splitter.on("=").splitToList(param); + if (keyValue.size() == 2) { + String key = keyValue.getFirst(); + if ("code".equals(key)) { + code = URLDecoder.decode(keyValue.getLast(), StandardCharsets.UTF_8); + } + } + } + } + Map formParams = Map.of("grant_type", "authorization_code", "code", code, "redirect_uri", + "http://localhost:3000/callback", "client_id", "frontend-client", "code_verifier", + "gyQBFpozcvcosvPt7m9Q1A4SqSf1yJtPIERruioHLjQ"); + return performServerPost(requestSpec, responseSpec, "/fineract-provider/oauth2/token", formParams, "access_token"); + } + @SuppressWarnings("unchecked") private static T performServerGet(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, final String getURL, final String jsonAttributeToGetBack) { @@ -164,11 +212,10 @@ public class OAuth2AuthenticationTest { return (T) JsonPath.from(json).get(jsonAttributeToGetBack); } - @SuppressWarnings("unchecked") public static T performServerPost(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, - final String putURL, final String formBody, final String jsonAttributeToGetBack) { - final String json = given().spec(requestSpec).body(formBody).expect().spec(responseSpec).log().ifError().when().post(putURL) - .andReturn().asString(); - return (T) JsonPath.from(json).get(jsonAttributeToGetBack); + final String putURL, final Map formBody, final String jsonAttributeToGetBack) { + final String response = given().spec(requestSpec).header("Content-Type", "application/x-www-form-urlencoded").formParams(formBody) + .expect().spec(responseSpec).log().ifError().when().post(putURL).andReturn().asString(); + return (T) JsonPath.from(response).get(jsonAttributeToGetBack); } } diff --git a/renovate.json b/renovate.json index 3f1cf72a9..6851acc61 100644 --- a/renovate.json +++ b/renovate.json @@ -23,18 +23,6 @@ "matchPackageNames": ["org.glassfish.jaxb:jaxb-runtime"], "allowedVersions": "<=2.3.6" }, - { - "matchPackageNames": ["org.apache.oltu.oauth2:org.apache.oltu.oauth2.common"], - "allowedVersions": "<=1.0.1" - }, - { - "matchPackageNames": ["org.apache.oltu.oauth2:org.apache.oltu.oauth2.client"], - "allowedVersions": "<=1.0.1" - }, - { - "matchPackageNames": ["org.apache.oltu.oauth2:org.apache.oltu.oauth2.httpclient4"], - "allowedVersions": "<=1.0.1" - }, { "matchPackageNames": ["com.sun.mail:jakarta.mail", "com.icegreen:greenmail-junit5"], "allowedVersions": "<=2.0.1" diff --git a/twofactor-tests/build.gradle b/twofactor-tests/build.gradle index 718eb1a9a..1e1382333 100644 --- a/twofactor-tests/build.gradle +++ b/twofactor-tests/build.gradle @@ -57,7 +57,7 @@ cargo { startStopTimeout = 240000 sharedClasspath = configurations.driver containerProperties { - def jvmArgs = '--add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED -Dfineract.security.basicauth.enabled=true -Dfineract.security.oauth.enabled=false -Dfineract.security.2fa.enabled=true ' + def jvmArgs = '--add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED -Dfineract.security.basicauth.enabled=true -Dfineract.security.oauth2.enabled=false -Dfineract.security.2fa.enabled=true ' if (project.hasProperty('dbType')) { if ('postgresql'.equalsIgnoreCase(dbType)) { jvmArgs += '-Dspring.datasource.hikari.driverClassName=org.postgresql.Driver -Dspring.datasource.hikari.jdbcUrl=jdbc:postgresql://localhost:5432/fineract_tenants -Dspring.datasource.hikari.username=root -Dspring.datasource.hikari.password=postgres -Dfineract.tenant.host=localhost -Dfineract.tenant.port=5432 -Dfineract.tenant.username=root -Dfineract.tenant.password=postgres'