FINERACT-1984: OAuth2.1
This commit is contained in:
committed by
Arnold Gálovics
parent
0c0d8592bb
commit
fa9c7b9364
@@ -22,7 +22,6 @@ buildscript {
|
||||
jacocoVersion = '0.8.12'
|
||||
retrofitVersion = '2.9.0'
|
||||
okhttpVersion = '4.9.3'
|
||||
oltuVersion = '1.0.1'
|
||||
fineractCustomProjects = []
|
||||
fineractJavaProjects = subprojects.findAll{
|
||||
[
|
||||
|
||||
@@ -150,9 +150,6 @@ dependencyManagement {
|
||||
dependency "com.squareup.retrofit2:converter-gson:2.11.0"
|
||||
dependency "com.squareup.retrofit2:converter-protobuf:2.11.0"
|
||||
dependency 'io.reactivex.rxjava2:rxjava:2.2.21'
|
||||
dependency "org.apache.oltu.oauth2:org.apache.oltu.oauth2.common:1.0.1"
|
||||
dependency "org.apache.oltu.oauth2:org.apache.oltu.oauth2.client:1.0.1"
|
||||
dependency "org.apache.oltu.oauth2:org.apache.oltu.oauth2.httpclient4:1.0.1"
|
||||
dependency "io.gsonfire:gson-fire:1.9.0"
|
||||
dependency "com.google.code.findbugs:jsr305:3.0.2"
|
||||
dependency "commons-codec:commons-codec:1.17.1"
|
||||
|
||||
@@ -52,7 +52,8 @@ task buildJavaSdk(type: org.openapitools.generator.gradle.plugin.tasks.GenerateT
|
||||
useRxJava2: 'false',
|
||||
library: 'retrofit2',
|
||||
hideGenerationTimestamp: 'true',
|
||||
containerDefaultToNull: 'true'
|
||||
containerDefaultToNull: 'true',
|
||||
oauth2Implementation: 'none'
|
||||
]
|
||||
generateModelTests = false
|
||||
generateApiTests = false
|
||||
@@ -138,6 +139,15 @@ task cleanupGeneratedJavaFiles() {
|
||||
inputs.dir(tempDir)
|
||||
outputs.dir(targetDir)
|
||||
|
||||
//TODO: remove harcoded, autogenerated Oltu's OAuthOkHttpClient.java from the generated files
|
||||
doFirst {
|
||||
delete fileTree(tempDir) {
|
||||
include "src/main/java/org/apache/fineract/client/auth/OAuthOkHttpClient.java"
|
||||
}
|
||||
delete fileTree(targetDir) {
|
||||
include "src/main/java/org/apache/fineract/client/auth/OAuthOkHttpClient.java"
|
||||
}
|
||||
}
|
||||
doLast {
|
||||
copy {
|
||||
from tempDir
|
||||
|
||||
@@ -35,11 +35,5 @@ dependencies {
|
||||
'com.squareup.okhttp3:logging-interceptor',
|
||||
)
|
||||
|
||||
// org.apache.oltu.oauth2 is used in org.apache.fineract.client.auth.OAuthOkHttpClient (only; can be excluded by consumers not requiring OAuth)
|
||||
implementation('org.apache.oltu.oauth2:org.apache.oltu.oauth2.client') {
|
||||
exclude group: 'org.apache.oltu.oauth2', module: 'org.apache.oltu.oauth2.common'
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
|
||||
testImplementation 'org.assertj:assertj-core'
|
||||
}
|
||||
|
||||
+47
-21
@@ -19,7 +19,10 @@
|
||||
|
||||
package org.apache.fineract.infrastructure.core.config;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -504,40 +507,63 @@ public class FineractProperties {
|
||||
|
||||
private FineractSecurityBasicAuth basicauth;
|
||||
private FineractSecurityTwoFactorAuth twoFactor;
|
||||
private FineractSecurityOAuth oauth;
|
||||
private FineractSecurityHsts hsts;
|
||||
private FineractSecurityOAuth2Properties oauth2;
|
||||
|
||||
public void set2fa(FineractSecurityTwoFactorAuth twoFactor) {
|
||||
this.twoFactor = twoFactor;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class FineractSecurityBasicAuth {
|
||||
@Getter
|
||||
@Setter
|
||||
public static class FineractSecurityOAuth2Properties {
|
||||
|
||||
private boolean enabled;
|
||||
}
|
||||
private boolean enabled;
|
||||
private ClientProperties client;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class FineractSecurityTwoFactorAuth {
|
||||
@Getter
|
||||
@Setter
|
||||
public static class ClientProperties implements Serializable {
|
||||
|
||||
private boolean enabled;
|
||||
}
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
private Map<String, Registration> registrations = new HashMap<>();
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class FineractSecurityOAuth {
|
||||
@Getter
|
||||
@Setter
|
||||
public static final class Registration implements Serializable {
|
||||
|
||||
private boolean enabled;
|
||||
}
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
private String clientId;
|
||||
private List<String> scopes = new ArrayList<>();
|
||||
private List<String> authorizationGrantTypes = new ArrayList<>();
|
||||
private List<String> redirectUris = new ArrayList<>();
|
||||
private boolean requireAuthorizationConsent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class FineractSecurityHsts {
|
||||
@Getter
|
||||
@Setter
|
||||
public static class FineractSecurityBasicAuth {
|
||||
|
||||
private boolean enabled;
|
||||
private boolean enabled;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class FineractSecurityTwoFactorAuth {
|
||||
|
||||
private boolean enabled;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class FineractSecurityHsts {
|
||||
|
||||
private boolean enabled;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.core.exceptionmapper;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
|
||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
|
||||
@Slf4j
|
||||
public class OAuth2ExceptionEntryPoint implements AuthenticationEntryPoint {
|
||||
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
|
||||
throws ServletException {
|
||||
log.warn("Exception occurred", ErrorHandler.findMostSpecificException(exception));
|
||||
ApiGlobalErrorResponse errorResponse = ApiGlobalErrorResponse.unAuthenticated();
|
||||
response.setContentType("application/json");
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.writeValue(response.getOutputStream(), errorResponse);
|
||||
} catch (Exception e) {
|
||||
throw new ServletException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
|true
|
||||
|When set to true, the supported authentication method will be basic authentication.
|
||||
|
||||
|fineract.security.oauth.enabled
|
||||
|fineract.security.oauth2.enabled
|
||||
|FINERACT_SECURITY_OAUTH_ENABLED
|
||||
|false
|
||||
|When set to true, the supported authentication method will be OAuth.
|
||||
|
||||
@@ -14,5 +14,5 @@ FINERACT_SECURITY_OAUTH_ENABLED=false
|
||||
+
|
||||
[source,bash]
|
||||
----
|
||||
java -Dfineract.security.basicauth.enabled=true -Dfineract.security.oauth.enabled=false -jar fineract-provider.jar
|
||||
java -Dfineract.security.basicauth.enabled=true -Dfineract.security.oauth2.enabled=false -jar fineract-provider.jar
|
||||
----
|
||||
|
||||
@@ -17,7 +17,7 @@ FINERACT_SERVER_OAUTH_RESOURCE_URL=http://localhost:9000/realms/fineract
|
||||
+
|
||||
[source,bash]
|
||||
----
|
||||
java -Dfineract.security.basicauth.enabled=false -Dfineract.security.oauth.enabled=true -jar fineract-provider.jar
|
||||
java -Dfineract.security.basicauth.enabled=false -Dfineract.security.oauth2.enabled=true -jar fineract-provider.jar
|
||||
----
|
||||
|
||||
Here's how to test OAuth with https://www.keycloak.org[Keycloak].
|
||||
|
||||
@@ -55,6 +55,7 @@ dependencies {
|
||||
'org.springframework.boot:spring-boot-starter-web',
|
||||
'org.springframework.boot:spring-boot-starter-validation',
|
||||
'org.springframework.boot:spring-boot-starter-security',
|
||||
"org.springframework.boot:spring-boot-starter-oauth2-authorization-server",
|
||||
'org.springframework.boot:spring-boot-starter-cache',
|
||||
'org.springframework.boot:spring-boot-starter-oauth2-resource-server',
|
||||
'org.springframework.boot:spring-boot-starter-actuator',
|
||||
@@ -202,6 +203,7 @@ dependencies {
|
||||
implementation 'org.apache.commons:commons-math3'
|
||||
|
||||
implementation 'io.github.classgraph:classgraph'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
|
||||
// testCompile dependencies are ONLY used in src/test, not src/main.
|
||||
// Do NOT repeat dependencies which are ALREADY in implementation or runtimeOnly!
|
||||
|
||||
-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
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ configurations {
|
||||
}
|
||||
dependencies {
|
||||
driver 'com.mysql:mysql-connector-j'
|
||||
testImplementation 'org.seleniumhq.selenium:selenium-java:4.21.0'
|
||||
testImplementation 'io.github.bonigarcia:webdrivermanager:5.5.1'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
|
||||
}
|
||||
|
||||
cargo {
|
||||
@@ -57,7 +60,7 @@ cargo {
|
||||
startStopTimeout = 240000
|
||||
sharedClasspath = configurations.driver
|
||||
containerProperties {
|
||||
def jvmArgs = '--add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED -Dfineract.security.basicauth.enabled=false -Dfineract.security.oauth.enabled=true -Dfineract.security.2fa.enabled=false '
|
||||
def jvmArgs = '--add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED -Dfineract.security.basicauth.enabled=false -Dfineract.security.oauth2.enabled=true -Dfineract.security.2fa.enabled=false '
|
||||
if (project.hasProperty('dbType')) {
|
||||
if ('postgresql'.equalsIgnoreCase(dbType)) {
|
||||
jvmArgs += '-Dspring.datasource.hikari.driverClassName=org.postgresql.Driver -Dspring.datasource.hikari.jdbcUrl=jdbc:postgresql://localhost:5432/fineract_tenants -Dspring.datasource.hikari.username=root -Dspring.datasource.hikari.password=postgres -Dfineract.tenant.host=localhost -Dfineract.tenant.port=5432 -Dfineract.tenant.username=root -Dfineract.tenant.password=postgres'
|
||||
|
||||
+90
-43
@@ -20,9 +20,11 @@ package org.apache.fineract.oauth2tests;
|
||||
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.builder.RequestSpecBuilder;
|
||||
import io.restassured.builder.ResponseSpecBuilder;
|
||||
@@ -31,10 +33,21 @@ import io.restassured.path.json.JsonPath;
|
||||
import io.restassured.response.Response;
|
||||
import io.restassured.specification.RequestSpecification;
|
||||
import io.restassured.specification.ResponseSpecification;
|
||||
import jakarta.mail.MessagingException;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.chrome.ChromeDriver;
|
||||
import org.openqa.selenium.chrome.ChromeOptions;
|
||||
import org.springframework.lang.NonNull;
|
||||
|
||||
public class OAuth2AuthenticationTest {
|
||||
|
||||
@@ -75,49 +88,22 @@ public class OAuth2AuthenticationTest {
|
||||
|
||||
@Test
|
||||
public void testAccessWithoutAuthentication() {
|
||||
performServerGet(requestSpec, responseSpec401, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, "");
|
||||
performServerGet(requestSpec, responseSpec401, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOAuth2Login() throws IOException, MessagingException {
|
||||
public void testGetOAuth2UserDetails() throws IOException, InterruptedException {
|
||||
performServerGet(requestSpec, responseSpec401, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, null);
|
||||
|
||||
performServerGet(requestSpec, responseSpec401, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, "");
|
||||
|
||||
String accessToken = performServerPost(requestFormSpec, responseSpec, "http://localhost:9000/auth/realms/fineract/token",
|
||||
"grant_type=client_credentials&client_id=community-app&client_secret=123123", "access_token");
|
||||
assertNotNull(accessToken);
|
||||
|
||||
String bearerToken = performServerPost(requestFormSpec, responseSpec, "http://localhost:9000/auth/realms/fineract/token",
|
||||
"grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=" + accessToken
|
||||
+ "&client_id=community-app&scope=fineract",
|
||||
"access_token");
|
||||
assertNotNull(bearerToken);
|
||||
String token = loginAndClaimToken(
|
||||
"https://localhost:8443/fineract-provider/oauth2/authorize" + "?response_type=code&client_id=frontend-client"
|
||||
+ "&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&scope=read&state=xyz"
|
||||
+ "&code_challenge=zudG_xkz8WrPPMq2MwmFP-NRvNapCL0OD-xYWRapTsU" + "&code_challenge_method=S256",
|
||||
requestFormSpec);
|
||||
|
||||
RequestSpecification requestSpecWithToken = new RequestSpecBuilder() //
|
||||
.setContentType(ContentType.JSON) //
|
||||
.addHeader("Authorization", "Bearer " + bearerToken) //
|
||||
.build();
|
||||
|
||||
performServerGet(requestSpecWithToken, responseSpec, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, "");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetOAuth2UserDetails() {
|
||||
performServerGet(requestSpec, responseSpec401, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, "");
|
||||
|
||||
String accessToken = performServerPost(requestFormSpec, responseSpec, "http://localhost:9000/auth/realms/fineract/token",
|
||||
"grant_type=client_credentials&client_id=community-app&client_secret=123123", "access_token");
|
||||
assertNotNull(accessToken);
|
||||
|
||||
String bearerToken = performServerPost(requestFormSpec, responseSpec, "http://localhost:9000/auth/realms/fineract/token",
|
||||
"grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=" + accessToken
|
||||
+ "&client_id=community-app&scope=fineract",
|
||||
"access_token");
|
||||
assertNotNull(bearerToken);
|
||||
|
||||
RequestSpecification requestSpecWithToken = new RequestSpecBuilder() //
|
||||
.setContentType(ContentType.JSON) //
|
||||
.addHeader("Authorization", "Bearer " + bearerToken) //
|
||||
.addHeader("Authorization", "Bearer " + token) //
|
||||
.build();
|
||||
|
||||
Boolean authenticationCheck = performServerGet(requestSpecWithToken, responseSpec,
|
||||
@@ -149,11 +135,73 @@ public class OAuth2AuthenticationTest {
|
||||
} catch (Exception e) {
|
||||
Thread.sleep(3000);
|
||||
}
|
||||
attempt++;
|
||||
} while (attempt < max_attempts);
|
||||
|
||||
fail(HEALTH_URL + " returned " + response.prettyPrint());
|
||||
}
|
||||
|
||||
public String loginAndClaimToken(String url, RequestSpecification requestSpec) throws IOException {
|
||||
CompletableFuture<String> futureToken = new CompletableFuture<>();
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress(3000), 0);
|
||||
server.createContext("/callback", exchange -> {
|
||||
try {
|
||||
String token = claimTokenOnCallback(requestSpec, exchange);
|
||||
futureToken.complete(token); // complete future with value
|
||||
exchange.sendResponseHeaders(200, 0);
|
||||
} catch (Exception e) {
|
||||
futureToken.completeExceptionally(e); // propagate exception
|
||||
} finally {
|
||||
exchange.close();
|
||||
}
|
||||
});
|
||||
server.start();
|
||||
WebDriver driver = getWebDriver();
|
||||
try {
|
||||
driver.get(url);
|
||||
driver.findElement(By.name("username")).sendKeys("mifos");
|
||||
driver.findElement(By.name("password")).sendKeys("password");
|
||||
driver.findElement(By.name("tenantId")).sendKeys("default");
|
||||
driver.findElement(By.cssSelector("button[type='submit'], input[type='submit']")).click();
|
||||
return futureToken.get(5, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
driver.quit();
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static WebDriver getWebDriver() {
|
||||
ChromeOptions options = new ChromeOptions();
|
||||
options.addArguments("--headless"); // run in headless mode
|
||||
options.addArguments("--no-sandbox");
|
||||
options.addArguments("--disable-dev-shm-usage");
|
||||
options.addArguments("--ignore-certificate-errors");
|
||||
return new ChromeDriver(options);
|
||||
}
|
||||
|
||||
private String claimTokenOnCallback(RequestSpecification requestSpec, HttpExchange exchange) {
|
||||
String query = exchange.getRequestURI().getQuery();
|
||||
String code = null;
|
||||
if (query != null) {
|
||||
for (String param : Splitter.on("&").split(query)) {
|
||||
List<String> keyValue = Splitter.on("=").splitToList(param);
|
||||
if (keyValue.size() == 2) {
|
||||
String key = keyValue.getFirst();
|
||||
if ("code".equals(key)) {
|
||||
code = URLDecoder.decode(keyValue.getLast(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Map<String, String> formParams = Map.of("grant_type", "authorization_code", "code", code, "redirect_uri",
|
||||
"http://localhost:3000/callback", "client_id", "frontend-client", "code_verifier",
|
||||
"gyQBFpozcvcosvPt7m9Q1A4SqSf1yJtPIERruioHLjQ");
|
||||
return performServerPost(requestSpec, responseSpec, "/fineract-provider/oauth2/token", formParams, "access_token");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T performServerGet(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
|
||||
final String getURL, final String jsonAttributeToGetBack) {
|
||||
@@ -164,11 +212,10 @@ public class OAuth2AuthenticationTest {
|
||||
return (T) JsonPath.from(json).get(jsonAttributeToGetBack);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T performServerPost(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
|
||||
final String putURL, final String formBody, final String jsonAttributeToGetBack) {
|
||||
final String json = given().spec(requestSpec).body(formBody).expect().spec(responseSpec).log().ifError().when().post(putURL)
|
||||
.andReturn().asString();
|
||||
return (T) JsonPath.from(json).get(jsonAttributeToGetBack);
|
||||
final String putURL, final Map<String, String> formBody, final String jsonAttributeToGetBack) {
|
||||
final String response = given().spec(requestSpec).header("Content-Type", "application/x-www-form-urlencoded").formParams(formBody)
|
||||
.expect().spec(responseSpec).log().ifError().when().post(putURL).andReturn().asString();
|
||||
return (T) JsonPath.from(response).get(jsonAttributeToGetBack);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,18 +23,6 @@
|
||||
"matchPackageNames": ["org.glassfish.jaxb:jaxb-runtime"],
|
||||
"allowedVersions": "<=2.3.6"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["org.apache.oltu.oauth2:org.apache.oltu.oauth2.common"],
|
||||
"allowedVersions": "<=1.0.1"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["org.apache.oltu.oauth2:org.apache.oltu.oauth2.client"],
|
||||
"allowedVersions": "<=1.0.1"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["org.apache.oltu.oauth2:org.apache.oltu.oauth2.httpclient4"],
|
||||
"allowedVersions": "<=1.0.1"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["com.sun.mail:jakarta.mail", "com.icegreen:greenmail-junit5"],
|
||||
"allowedVersions": "<=2.0.1"
|
||||
|
||||
@@ -57,7 +57,7 @@ cargo {
|
||||
startStopTimeout = 240000
|
||||
sharedClasspath = configurations.driver
|
||||
containerProperties {
|
||||
def jvmArgs = '--add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED -Dfineract.security.basicauth.enabled=true -Dfineract.security.oauth.enabled=false -Dfineract.security.2fa.enabled=true '
|
||||
def jvmArgs = '--add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED -Dfineract.security.basicauth.enabled=true -Dfineract.security.oauth2.enabled=false -Dfineract.security.2fa.enabled=true '
|
||||
if (project.hasProperty('dbType')) {
|
||||
if ('postgresql'.equalsIgnoreCase(dbType)) {
|
||||
jvmArgs += '-Dspring.datasource.hikari.driverClassName=org.postgresql.Driver -Dspring.datasource.hikari.jdbcUrl=jdbc:postgresql://localhost:5432/fineract_tenants -Dspring.datasource.hikari.username=root -Dspring.datasource.hikari.password=postgres -Dfineract.tenant.host=localhost -Dfineract.tenant.port=5432 -Dfineract.tenant.username=root -Dfineract.tenant.password=postgres'
|
||||
|
||||
Reference in New Issue
Block a user