FINERACT-1984: OAuth2.1
This commit is contained in:
committed by
Arnold Gálovics
parent
0c0d8592bb
commit
fa9c7b9364
-182
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+5
-10
@@ -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);
|
||||
}
|
||||
|
||||
+2
-3
@@ -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.");
|
||||
}
|
||||
|
||||
+31
@@ -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
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -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) {
|
||||
|
||||
+272
@@ -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);
|
||||
}
|
||||
}
|
||||
+52
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -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;
|
||||
}
|
||||
+50
@@ -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);
|
||||
}
|
||||
}
|
||||
-78
@@ -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);
|
||||
}
|
||||
}
|
||||
+61
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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;
|
||||
|
||||
-156
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+2
-4
@@ -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());
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
|
||||
+2
-6
@@ -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() {
|
||||
|
||||
|
||||
+1
-1
@@ -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>
|
||||
+3
-1
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user