FINERACT-1984: OAuth2.1

This commit is contained in:
CsengeSóti
2025-09-12 14:58:37 +02:00
committed by Arnold Gálovics
parent 0c0d8592bb
commit fa9c7b9364
36 changed files with 802 additions and 599 deletions
-1
View File
@@ -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{
[
@@ -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"
+11 -1
View File
@@ -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
@@ -138,6 +139,15 @@ 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
-6
View File
@@ -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'
}
@@ -19,7 +19,10 @@
package org.apache.fineract.infrastructure.core.config;
import java.io.Serial;
import java.io.Serializable;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -504,40 +507,63 @@ public class FineractProperties {
private FineractSecurityBasicAuth basicauth;
private FineractSecurityTwoFactorAuth twoFactor;
private FineractSecurityOAuth oauth;
private FineractSecurityHsts hsts;
private FineractSecurityOAuth2Properties oauth2;
public void set2fa(FineractSecurityTwoFactorAuth twoFactor) {
this.twoFactor = twoFactor;
}
}
@Getter
@Setter
public static class FineractSecurityBasicAuth {
@Getter
@Setter
public static class FineractSecurityOAuth2Properties {
private boolean enabled;
}
private boolean enabled;
private ClientProperties client;
@Getter
@Setter
public static class FineractSecurityTwoFactorAuth {
@Getter
@Setter
public static class ClientProperties implements Serializable {
private boolean enabled;
}
@Serial
private static final long serialVersionUID = 1L;
private Map<String, Registration> registrations = new HashMap<>();
@Getter
@Setter
public static class FineractSecurityOAuth {
@Getter
@Setter
public static final class Registration implements Serializable {
private boolean enabled;
}
@Serial
private static final long serialVersionUID = 1L;
private String clientId;
private List<String> scopes = new ArrayList<>();
private List<String> authorizationGrantTypes = new ArrayList<>();
private List<String> redirectUris = new ArrayList<>();
private boolean requireAuthorizationConsent = true;
}
}
}
@Getter
@Setter
public static class FineractSecurityHsts {
@Getter
@Setter
public static class FineractSecurityBasicAuth {
private boolean enabled;
private boolean enabled;
}
@Getter
@Setter
public static class FineractSecurityTwoFactorAuth {
private boolean enabled;
}
@Getter
@Setter
public static class FineractSecurityHsts {
private boolean enabled;
}
}
@Getter
@@ -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);
}
}
}
@@ -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.
@@ -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
----
@@ -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].
+2
View File
@@ -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!
@@ -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);
}
};
}
}
@@ -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);
}
@@ -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.");
}
@@ -0,0 +1,31 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.security.api;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class LoginController {
@GetMapping("/login")
public String login() {
return "login"; // resolves to src/main/resources/templates/login.html
}
}
@@ -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) {
@@ -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);
}
}
@@ -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);
}
}
}
@@ -0,0 +1,33 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.security.data;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@AllArgsConstructor
@Accessors(chain = true)
public class TenantAuthenticationDetails {
private String userName;
private String tenantId;
private String password;
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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();
}
}
}
@@ -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;
@@ -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));
}
}
}
@@ -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());
@@ -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);
}
@@ -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);
}
@@ -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() {
@@ -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)
@@ -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}
@@ -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>
@@ -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"); //
}
}
@@ -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
+4 -1
View File
@@ -35,6 +35,9 @@ configurations {
}
dependencies {
driver 'com.mysql:mysql-connector-j'
testImplementation 'org.seleniumhq.selenium:selenium-java:4.21.0'
testImplementation 'io.github.bonigarcia:webdrivermanager:5.5.1'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}
cargo {
@@ -57,7 +60,7 @@ cargo {
startStopTimeout = 240000
sharedClasspath = configurations.driver
containerProperties {
def jvmArgs = '--add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED -Dfineract.security.basicauth.enabled=false -Dfineract.security.oauth.enabled=true -Dfineract.security.2fa.enabled=false '
def jvmArgs = '--add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED -Dfineract.security.basicauth.enabled=false -Dfineract.security.oauth2.enabled=true -Dfineract.security.2fa.enabled=false '
if (project.hasProperty('dbType')) {
if ('postgresql'.equalsIgnoreCase(dbType)) {
jvmArgs += '-Dspring.datasource.hikari.driverClassName=org.postgresql.Driver -Dspring.datasource.hikari.jdbcUrl=jdbc:postgresql://localhost:5432/fineract_tenants -Dspring.datasource.hikari.username=root -Dspring.datasource.hikari.password=postgres -Dfineract.tenant.host=localhost -Dfineract.tenant.port=5432 -Dfineract.tenant.username=root -Dfineract.tenant.password=postgres'
@@ -20,9 +20,11 @@ package org.apache.fineract.oauth2tests;
import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
import com.google.common.base.Splitter;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
@@ -31,10 +33,21 @@ import io.restassured.path.json.JsonPath;
import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import jakarta.mail.MessagingException;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.springframework.lang.NonNull;
public class OAuth2AuthenticationTest {
@@ -75,49 +88,22 @@ public class OAuth2AuthenticationTest {
@Test
public void testAccessWithoutAuthentication() {
performServerGet(requestSpec, responseSpec401, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, "");
performServerGet(requestSpec, responseSpec401, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, null);
}
@Test
public void testOAuth2Login() throws IOException, MessagingException {
public void testGetOAuth2UserDetails() throws IOException, InterruptedException {
performServerGet(requestSpec, responseSpec401, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, null);
performServerGet(requestSpec, responseSpec401, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, "");
String accessToken = performServerPost(requestFormSpec, responseSpec, "http://localhost:9000/auth/realms/fineract/token",
"grant_type=client_credentials&client_id=community-app&client_secret=123123", "access_token");
assertNotNull(accessToken);
String bearerToken = performServerPost(requestFormSpec, responseSpec, "http://localhost:9000/auth/realms/fineract/token",
"grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=" + accessToken
+ "&client_id=community-app&scope=fineract",
"access_token");
assertNotNull(bearerToken);
String token = loginAndClaimToken(
"https://localhost:8443/fineract-provider/oauth2/authorize" + "?response_type=code&client_id=frontend-client"
+ "&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&scope=read&state=xyz"
+ "&code_challenge=zudG_xkz8WrPPMq2MwmFP-NRvNapCL0OD-xYWRapTsU" + "&code_challenge_method=S256",
requestFormSpec);
RequestSpecification requestSpecWithToken = new RequestSpecBuilder() //
.setContentType(ContentType.JSON) //
.addHeader("Authorization", "Bearer " + bearerToken) //
.build();
performServerGet(requestSpecWithToken, responseSpec, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, "");
}
@Test
public void testGetOAuth2UserDetails() {
performServerGet(requestSpec, responseSpec401, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, "");
String accessToken = performServerPost(requestFormSpec, responseSpec, "http://localhost:9000/auth/realms/fineract/token",
"grant_type=client_credentials&client_id=community-app&client_secret=123123", "access_token");
assertNotNull(accessToken);
String bearerToken = performServerPost(requestFormSpec, responseSpec, "http://localhost:9000/auth/realms/fineract/token",
"grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=" + accessToken
+ "&client_id=community-app&scope=fineract",
"access_token");
assertNotNull(bearerToken);
RequestSpecification requestSpecWithToken = new RequestSpecBuilder() //
.setContentType(ContentType.JSON) //
.addHeader("Authorization", "Bearer " + bearerToken) //
.addHeader("Authorization", "Bearer " + token) //
.build();
Boolean authenticationCheck = performServerGet(requestSpecWithToken, responseSpec,
@@ -149,11 +135,73 @@ public class OAuth2AuthenticationTest {
} catch (Exception e) {
Thread.sleep(3000);
}
attempt++;
} while (attempt < max_attempts);
fail(HEALTH_URL + " returned " + response.prettyPrint());
}
public String loginAndClaimToken(String url, RequestSpecification requestSpec) throws IOException {
CompletableFuture<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);
}
}
-12
View File
@@ -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"
+1 -1
View File
@@ -57,7 +57,7 @@ cargo {
startStopTimeout = 240000
sharedClasspath = configurations.driver
containerProperties {
def jvmArgs = '--add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED -Dfineract.security.basicauth.enabled=true -Dfineract.security.oauth.enabled=false -Dfineract.security.2fa.enabled=true '
def jvmArgs = '--add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED -Dfineract.security.basicauth.enabled=true -Dfineract.security.oauth2.enabled=false -Dfineract.security.2fa.enabled=true '
if (project.hasProperty('dbType')) {
if ('postgresql'.equalsIgnoreCase(dbType)) {
jvmArgs += '-Dspring.datasource.hikari.driverClassName=org.postgresql.Driver -Dspring.datasource.hikari.jdbcUrl=jdbc:postgresql://localhost:5432/fineract_tenants -Dspring.datasource.hikari.username=root -Dspring.datasource.hikari.password=postgres -Dfineract.tenant.host=localhost -Dfineract.tenant.port=5432 -Dfineract.tenant.username=root -Dfineract.tenant.password=postgres'