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,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