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'