This is an automated email from the ASF dual-hosted git repository.

arnold pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new fa9c7b9364 FINERACT-1984: OAuth2.1
fa9c7b9364 is described below

commit fa9c7b93642cada11b0b471c3acb2ce07efa44e2
Author: CsengeSóti <[email protected]>
AuthorDate: Thu Sep 11 10:06:37 2025 +0200

    FINERACT-1984: OAuth2.1
---
 build.gradle                                       |   1 -
 .../groovy/org.apache.fineract.dependencies.gradle |   3 -
 fineract-client/build.gradle                       |  14 +-
 fineract-client/dependencies.gradle                |   6 -
 .../core/config/FineractProperties.java            |  68 ++++--
 .../exceptionmapper/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 --------------
 .../infrastructure/core/config/SecurityConfig.java |  15 +-
 .../core/config/SecurityValidationConfig.java      |   5 +-
 .../LoginController.java}                          |  13 +-
 .../security/api/UserDetailsApiResource.java       |   5 +-
 .../security/config/AuthorizationServerConfig.java | 272 +++++++++++++++++++++
 .../FineractJwtAuthenticationTokenConverter.java   |  52 ++++
 .../TenantAuthenticationDetails.java}              |  15 +-
 .../security/filter/BusinessDateFilter.java        |  50 ++++
 .../InsecureTwoFactorAuthenticationFilter.java     |  78 ------
 .../filter/TenantAwareAuthenticationFilter.java    |  61 +++++
 .../TenantAwareBasicAuthenticationFilter.java      |   6 +-
 .../filter/TenantAwareTenantIdentifierFilter.java  | 156 ------------
 .../filter/TwoFactorAuthenticationFilter.java      |   6 +-
 ...sService.java => AuthTenantDetailsService.java} |   2 +-
 ...Jdbc.java => AuthTenantDetailsServiceJdbc.java} |   6 +-
 .../SpringSecurityPlatformSecurityContext.java     |   8 +-
 .../security/api/SelfUserDetailsApiResource.java   |   2 +-
 .../src/main/resources/application.properties      |  14 +-
 .../src/main/resources/templates/login.html        | 111 +++++++++
 .../ClasspathDuplicatesStepDefinitions.java        |   4 +-
 .../src/test/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, 758 insertions(+), 607 deletions(-)

diff --git a/build.gradle b/build.gradle
index f566d259b9..7c9f4f4e64 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 4893dfe919..8393307867 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 a079f0a086..3017b6dfda 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 2f3dbc5a8a..aad45729a1 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 59384d0052..b1855b8d5e 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 {
+
+                @Serial
+                private static final long serialVersionUID = 1L;
+                private Map<String, Registration> registrations = new 
HashMap<>();
+
+                @Getter
+                @Setter
+                public static final class Registration implements Serializable 
{
+
+                    @Serial
+                    private static final long serialVersionUID = 1L;
+                    private String clientId;
+                    private List<String> scopes = new ArrayList<>();
+                    private List<String> authorizationGrantTypes = new 
ArrayList<>();
+                    private List<String> redirectUris = new ArrayList<>();
+                    private boolean requireAuthorizationConsent = true;
+                }
+            }
+        }
 
-        private boolean enabled;
-    }
+        @Getter
+        @Setter
+        public static class FineractSecurityBasicAuth {
 
-    @Getter
-    @Setter
-    public static class FineractSecurityOAuth {
+            private boolean enabled;
+        }
 
-        private boolean enabled;
-    }
+        @Getter
+        @Setter
+        public static class FineractSecurityTwoFactorAuth {
 
-    @Getter
-    @Setter
-    public static class FineractSecurityHsts {
+            private boolean enabled;
+        }
 
-        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 451581d966..0000000000
--- 
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 877281a07c..d75fc7efdf 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 94881f58bb..1f52db41a5 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 fbe0a341cb..078d8231d7 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 6c112afc65..4b889af935 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 0b7346c28a..0000000000
--- 
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<PlatformRequestLog> 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<AuthorizationManager<RequestAuthorizationContext>> 
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<Jwt, FineractJwtAuthenticationToken> 
authenticationConverter() {
-        return jwt -> {
-            try {
-                UserDetails user = 
userDetailsService.loadUserByUsername(jwt.getSubject());
-                jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
-                Collection<GrantedAuthority> 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 3f723b1a31..6d6c7e5084 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<AuthorizationManager<RequestAuthorizationContext>> 
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 2d6d5244c6..24c5a507d2 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/service/BasicAuthTenantDetailsService.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/LoginController.java
similarity index 69%
copy from 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/BasicAuthTenantDetailsService.java
copy to 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/LoginController.java
index ebda0827a4..6651bb7673 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/api/LoginController.java
@@ -16,11 +16,16 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.infrastructure.security.service;
+package org.apache.fineract.infrastructure.security.api;
 
-import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
 
-public interface BasicAuthTenantDetailsService {
+@Controller
+public class LoginController {
 
-    FineractPlatformTenant loadTenantById(String tenantId, boolean isReport);
+    @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 24a780b320..999615a364 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<String> permissions = new ArrayList<>();
-        AuthenticatedOauthUserData authenticatedUserData = new 
AuthenticatedOauthUserData().setUsername(principal.getUsername())
-                .setPermissions(permissions);
+        AuthenticatedOauthUserData authenticatedUserData;
 
         final Collection<GrantedAuthority> 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 0000000000..d9735bb104
--- /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<RegisteredClient> 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<HttpServletRequest, 
TenantAuthenticationDetails> 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<JwtEncodingContext> tokenCustomizer() {
+        return context -> {
+            UsernamePasswordAuthenticationToken authentication = 
context.getPrincipal();
+            TenantAuthenticationDetails details = 
(TenantAuthenticationDetails) authentication.getDetails();
+            AppUser appUser = (AppUser) authentication.getPrincipal();
+            List<String> roles = 
appUser.getRoles().stream().map(Role::getName).toList();
+            List<String> 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 0000000000..900a8b3c6a
--- /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<Jwt, 
FineractJwtAuthenticationToken> {
+
+    private final TenantAwareJpaPlatformUserDetailsService userDetailsService;
+
+    @Override
+    @NonNull
+    public FineractJwtAuthenticationToken convert(@NonNull Jwt jwt) {
+        try {
+            UserDetails user = 
userDetailsService.loadUserByUsername(jwt.getSubject());
+            Collection<GrantedAuthority> 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/service/BasicAuthTenantDetailsService.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/TenantAuthenticationDetails.java
similarity index 70%
copy from 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/BasicAuthTenantDetailsService.java
copy to 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/TenantAuthenticationDetails.java
index ebda0827a4..972f3b6cf3 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/data/TenantAuthenticationDetails.java
@@ -16,11 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.infrastructure.security.service;
+package org.apache.fineract.infrastructure.security.data;
 
-import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.experimental.Accessors;
 
-public interface BasicAuthTenantDetailsService {
+@Data
+@AllArgsConstructor
+@Accessors(chain = true)
+public class TenantAuthenticationDetails {
 
-    FineractPlatformTenant loadTenantById(String tenantId, boolean isReport);
+    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 0000000000..a9d9a5ee3d
--- /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<BusinessDateType, LocalDate> 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 85ef766fd6..0000000000
--- 
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<GrantedAuthority> 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<GrantedAuthority>) 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 0000000000..133ba1b29f
--- /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 498e3b0644..19e8a2f8a3 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<PlatformRequestLog> 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 a321f6180b..0000000000
--- 
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<PlatformRequestLog> 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<BusinessDateType, LocalDate> 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 6c6d382a5c..5c63c3c842 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<GrantedAuthority>) 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 ebda0827a4..d5ab73213c 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 88256014b7..10cb416362 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
  * <code>tenantIdentifier</code>.
  */
 @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 3775c2eacc..31630595f3 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<CommandWrapper> 
EXEMPT_FROM_PASSWORD_RESET_CHECK = new ArrayList<CommandWrapper>(
             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 9b05d59dc1..b387c7edd0 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 38c92e4678..18aa480651 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 0000000000..63e40597d0
--- /dev/null
+++ b/fineract-provider/src/main/resources/templates/login.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org";>
+<head>
+    <meta charset="UTF-8" />
+    <title>Sign In</title>
+    <style>
+        /* Reset basic styling */
+        body, html {
+            margin: 0;
+            padding: 0;
+            height: 100%;
+            font-family: Arial, sans-serif;
+            background-color: #f5f7fa; /* light neutral background */
+        }
+
+        /* Centering container */
+        .container {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            height: 100%;
+            padding: 20px;
+        }
+
+        /* Form card styling */
+        .login-card {
+            background-color: #ffffff; /* white form background */
+            padding: 40px 30px;
+            border-radius: 8px;
+            box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
+            width: 100%;
+            max-width: 380px;
+            text-align: center;
+        }
+
+        h1 {
+            margin-bottom: 24px;
+            font-size: 1.8rem;
+            color: #333333; /* dark grey text */
+        }
+
+        /* Label styling */
+        .login-card label {
+            display: block;
+            text-align: left;
+            margin-bottom: 6px;
+            font-weight: bold;
+            color: #555555;
+        }
+
+        /* Input styling */
+        .login-card input[type="text"],
+        .login-card input[type="password"] {
+            width: 100%;
+            padding: 10px 12px;
+            margin-bottom: 16px;
+            font-size: 1rem;
+            border: 1px solid #cccccc;
+            border-radius: 4px;
+            box-sizing: border-box;
+            transition: border-color 0.2s ease-in-out;
+        }
+
+        .login-card input:focus {
+            outline: none;
+            border-color: #0085ff; /* brighter blue accent */
+            box-shadow: 0 0 4px rgba(0,133,255,0.3);
+        }
+
+        /* Button styling */
+        .login-card button[type="submit"] {
+            width: 100%;
+            padding: 12px;
+            font-size: 1rem;
+            font-weight: bold;
+            color: #ffffff;
+            background-color: #0066cc; /* primary blue accent */
+            border: none;
+            border-radius: 4px;
+            cursor: pointer;
+            transition: background-color 0.2s ease-in-out;
+        }
+
+        .login-card button[type="submit"]:hover {
+            background-color: #005bb5;
+        }
+    </style>
+</head>
+<body>
+<div class="container">
+    <div class="login-card">
+        <h1>Sign In</h1>
+        <form th:action="@{/login}" method="post">
+            <div>
+                <label for="tenantId">Tenant ID</label>
+                <input type="text" id="tenantId" name="tenantId" required />
+            </div>
+            <div>
+                <label for="username">Username</label>
+                <input type="text" id="username" name="username" 
autocomplete="username" required />
+            </div>
+            <div>
+                <label for="password">Password</label>
+                <input type="password" id="password" name="password" 
autocomplete="current-password" required />
+            </div>
+            <button type="submit">Sign In</button>
+        </form>
+    </div>
+</div>
+</body>
+</html>
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 d7a0b7bdb3..f24f6d95d2 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 d7c5d8fc6d..3790d7d26b 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 3821b71a5c..cc8995b19b 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.enab [...]
+            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.ena [...]
             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 391ba7e03f..ed2db78a22 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);
-
-        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);
+        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) //
+                .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<String> 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<String> 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<String, String> 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> 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> 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<String, String> 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 3f1cf72a94..6851acc619 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 718eb1a9a4..1e13823338 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.enabl [...]
+            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.enab [...]
             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'

Reply via email to