FINERACT-1932: Fineract modularization - fineract-security

This commit is contained in:
Jose Alberto Hernandez
2026-02-12 14:01:38 +01:00
committed by Aleksandar Vidakovic
parent f504281067
commit 356dd07ca0
62 changed files with 216 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
/**
* 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.
*/
description = 'Fineract Security'
apply plugin: 'java'
apply plugin: 'eclipse'
configurations {
providedRuntime // needed for Spring Boot executable WAR
providedCompile
compile() {
exclude module: 'hibernate-entitymanager'
exclude module: 'hibernate-validator'
exclude module: 'activation'
exclude module: 'bcmail-jdk14'
exclude module: 'bcprov-jdk14'
exclude module: 'bctsp-jdk14'
exclude module: 'c3p0'
exclude module: 'stax-api'
exclude module: 'jaxb-api'
exclude module: 'jaxb-impl'
exclude module: 'jboss-logging'
exclude module: 'itext-rtf'
exclude module: 'classworlds'
}
runtime
}
apply from: 'dependencies.gradle'
// Configuration for the modernizer plugin
// https://github.com/andygoossens/gradle-modernizer-plugin
modernizer {
ignoreClassNamePatterns = [
'.*AbstractPersistableCustom',
'.*EntityTables',
'.*domain.*'
]
}
// If we are running Gradle within Eclipse to enhance classes with OpenJPA,
// set the classes directory to point to Eclipse's default build directory
if (project.hasProperty('env') && project.getProperty('env') == 'eclipse') {
sourceSets.main.java.outputDir = new File(rootProject.projectDir, "fineract-security/bin/main")
}
eclipse {
project {
buildCommand([ LaunchConfigHandle: "<project>/.externalToolBuilders/OpenJPA Enhance Builder.launch" ], 'org.eclipse.ui.externaltools.ExternalToolBuilder')
}
}
if (!(project.hasProperty('env') && project.getProperty('env') == 'dev')) {
sourceSets {
test {
java {
exclude '**/core/boot/tests/**'
}
}
}
}
+98
View File
@@ -0,0 +1,98 @@
/**
* 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.
*/
dependencies {
// Never use "compile" scope, but make all dependencies either 'implementation', 'runtimeOnly' or 'testCompile'.
// Note that we never use 'api', because Fineract at least currently is a simple monolithic application ("WAR"), not a library.
// We also (normally should have) no need to ever use 'compileOnly'.
// Core module dependency
implementation(project(path: ':fineract-core'))
// PostgreSQL driver for SQL injection prevention utilities
implementation 'org.postgresql:postgresql'
// Spring Boot Security dependencies
implementation(
'org.springframework.boot:spring-boot-starter-web',
'org.springframework.boot:spring-boot-starter-security',
'org.springframework.boot:spring-boot-starter-oauth2-authorization-server',
'org.springframework.boot:spring-boot-starter-oauth2-resource-server',
'org.springframework.boot:spring-boot-starter-oauth2-client',
'org.springframework.boot:spring-boot-starter-cache',
'jakarta.ws.rs:jakarta.ws.rs-api',
'org.glassfish.jersey.media:jersey-media-multipart',
'com.google.guava:guava',
'com.google.code.gson:gson',
'org.apache.commons:commons-lang3',
'com.jayway.jsonpath:json-path',
'com.github.spotbugs:spotbugs-annotations',
'io.swagger.core.v3:swagger-annotations-jakarta',
'org.springdoc:springdoc-openapi-starter-webmvc-ui',
'org.mapstruct:mapstruct',
)
// JPA for security entities
implementation ('org.springframework.boot:spring-boot-starter-data-jpa') {
exclude group: 'org.hibernate'
}
implementation('org.eclipse.persistence:org.eclipse.persistence.jpa') {
exclude group: 'org.eclipse.persistence', module: 'jakarta.persistence'
}
// OWASP ESAPI for security utilities
implementation('org.owasp.esapi:esapi') {
exclude group: 'org.slf4j'
exclude group: 'log4j'
exclude group: 'commons-logging'
exclude group: 'commons-collections'
exclude group: 'commons-lang'
exclude group: 'spotbugs-annotations'
exclude group: 'xml-apis'
}
// Email support for OTP delivery
implementation ('org.apache.commons:commons-email') {
exclude group: 'com.sun.mail', module: 'javax.mail'
exclude group: 'javax.activation', module: 'activation'
}
implementation 'com.sun.mail:jakarta.mail'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
annotationProcessor 'org.mapstruct:mapstruct-processor'
// Test dependencies
testImplementation( 'io.github.classgraph:classgraph' )
testImplementation ('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'com.jayway.jsonpath', module: 'json-path'
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
exclude group: 'jakarta.activation'
exclude group: 'javax.activation'
exclude group: 'org.skyscreamer'
}
testImplementation ('org.mockito:mockito-inline')
testImplementation 'org.springframework.security:spring-security-test'
}
@@ -0,0 +1,156 @@
/**
* 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 com.google.gson.Gson;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.constants.TwoFactorConstants;
import org.apache.fineract.infrastructure.security.data.AuthenticatedUserData;
import org.apache.fineract.infrastructure.security.service.SpringSecurityPlatformSecurityContext;
import org.apache.fineract.portfolio.client.service.ClientReadPlatformService;
import org.apache.fineract.useradministration.data.RoleData;
import org.apache.fineract.useradministration.domain.AppUser;
import org.apache.fineract.useradministration.domain.Role;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
@Component
@ConditionalOnProperty("fineract.security.basicauth.enabled")
@Path("/v1/authentication")
@Tag(name = "Authentication HTTP Basic", description = "An API capability that allows client applications to verify authentication details using HTTP Basic Authentication.")
@RequiredArgsConstructor
public class AuthenticationApiResource {
@Value("${fineract.security.2fa.enabled}")
private boolean twoFactorEnabled;
public static class AuthenticateRequest {
public String username;
public String password;
}
@Qualifier("customAuthenticationProvider")
private final DaoAuthenticationProvider customAuthenticationProvider;
private final ToApiJsonSerializer<AuthenticatedUserData> apiJsonSerializerService;
private final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext;
private final ClientReadPlatformService clientReadPlatformService;
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Verify authentication", description = "Authenticates the credentials provided and returns the set roles and permissions allowed.")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = AuthenticationApiResourceSwagger.PostAuthenticationRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = AuthenticationApiResourceSwagger.PostAuthenticationResponse.class)))
@ApiResponse(responseCode = "400", description = "Unauthenticated. Please login")
public String authenticate(@Parameter(hidden = true) final String apiRequestBodyAsJson,
@QueryParam("returnClientList") @DefaultValue("false") boolean returnClientList) {
// TODO FINERACT-819: sort out Jersey so JSON conversion does not have
// to be done explicitly via GSON here, but implicit by arg
AuthenticateRequest request = new Gson().fromJson(apiRequestBodyAsJson, AuthenticateRequest.class);
if (request == null) {
throw new IllegalArgumentException(
"Invalid JSON in BODY (no longer URL param; see FINERACT-726) of POST to /authentication: " + apiRequestBodyAsJson);
}
if (request.username == null || request.password == null) {
throw new IllegalArgumentException("Username or Password is null in JSON (see FINERACT-726) of POST to /authentication: "
+ apiRequestBodyAsJson + "; username=" + request.username + ", password=" + request.password);
}
final Authentication authentication = new UsernamePasswordAuthenticationToken(request.username, request.password);
final Authentication authenticationCheck = this.customAuthenticationProvider.authenticate(authentication);
final Collection<String> permissions = new ArrayList<>();
AuthenticatedUserData authenticatedUserData = new AuthenticatedUserData().setUsername(request.username).setPermissions(permissions);
if (authenticationCheck.isAuthenticated()) {
final Collection<GrantedAuthority> authorities = new ArrayList<>(authenticationCheck.getAuthorities());
for (final GrantedAuthority grantedAuthority : authorities) {
permissions.add(grantedAuthority.getAuthority());
}
final byte[] base64EncodedAuthenticationKey = Base64.getEncoder()
.encode((request.username + ":" + request.password).getBytes(StandardCharsets.UTF_8));
final AppUser principal = (AppUser) authenticationCheck.getPrincipal();
final Collection<RoleData> roles = new ArrayList<>();
final Set<Role> userRoles = principal.getRoles();
for (final Role role : userRoles) {
roles.add(role.toData());
}
final Long officeId = principal.getOffice().getId();
final String officeName = principal.getOffice().getName();
final Long staffId = principal.getStaffId();
final String staffDisplayName = principal.getStaffDisplayName();
final EnumOptionData organisationalRole = principal.organisationalRoleData();
boolean isTwoFactorRequired = this.twoFactorEnabled
&& !principal.hasSpecificPermissionTo(TwoFactorConstants.BYPASS_TWO_FACTOR_PERMISSION);
Long userId = principal.getId();
if (this.springSecurityPlatformSecurityContext.doesPasswordHasToBeRenewed(principal)) {
authenticatedUserData = new AuthenticatedUserData().setUsername(request.username).setUserId(userId)
.setBase64EncodedAuthenticationKey(new String(base64EncodedAuthenticationKey, StandardCharsets.UTF_8))
.setAuthenticated(true).setShouldRenewPassword(true).setTwoFactorAuthenticationRequired(isTwoFactorRequired);
} else {
authenticatedUserData = new AuthenticatedUserData().setUsername(request.username).setOfficeId(officeId)
.setOfficeName(officeName).setStaffId(staffId).setStaffDisplayName(staffDisplayName)
.setOrganisationalRole(organisationalRole).setRoles(roles).setPermissions(permissions).setUserId(principal.getId())
.setAuthenticated(true)
.setBase64EncodedAuthenticationKey(new String(base64EncodedAuthenticationKey, StandardCharsets.UTF_8))
.setTwoFactorAuthenticationRequired(isTwoFactorRequired)
.setClients(returnClientList ? clientReadPlatformService.retrieveUserClients(userId) : null);
}
}
return this.apiJsonSerializerService.serialize(authenticatedUserData);
}
}
@@ -0,0 +1,76 @@
/**
* 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 io.swagger.v3.oas.annotations.media.Schema;
import java.util.Collection;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.useradministration.data.RoleData;
/**
* Created by sanyam on 13/8/17.
*/
public final class AuthenticationApiResourceSwagger {
private AuthenticationApiResourceSwagger() {
}
@Schema(description = "PostAuthenticationRequest")
public static final class PostAuthenticationRequest {
private PostAuthenticationRequest() {
}
@Schema(required = true, example = "mifos")
public String username;
@Schema(required = true, example = "password")
public String password;
}
@Schema(description = "PostAuthenticationResponse")
public static final class PostAuthenticationResponse {
private PostAuthenticationResponse() {
}
@Schema(example = "mifos")
public String username;
@Schema(example = "1")
public Long userId;
@Schema(example = "bWlmb3M6cGFzc3dvcmQ=")
public String base64EncodedAuthenticationKey;
@Schema(example = "true")
public boolean authenticated;
@Schema(example = "1")
public Long officeId;
@Schema(example = "Head Office")
public String officeName;
@Schema(example = "1")
public Long staffId;
@Schema(example = "Director, Program")
public String staffDisplayName;
public EnumOptionData organisationalRole;
public Collection<RoleData> roles;
@Schema(example = "ALL_FUNCTIONS")
public Collection<String> permissions;
}
}
@@ -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
}
}
@@ -0,0 +1,107 @@
/**
* 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 io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.UriInfo;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.data.AccessTokenData;
import org.apache.fineract.infrastructure.security.data.OTPDeliveryMethod;
import org.apache.fineract.infrastructure.security.data.OTPMetadata;
import org.apache.fineract.infrastructure.security.data.OTPRequest;
import org.apache.fineract.infrastructure.security.domain.TFAccessToken;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.security.service.TwoFactorService;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
@Path("/v1/twofactor")
@Component
@ConditionalOnProperty("fineract.security.2fa.enabled")
@Tag(name = "Two Factor", description = "")
@RequiredArgsConstructor
public class TwoFactorApiResource {
private final ToApiJsonSerializer<OTPMetadata> otpRequestSerializer;
private final ToApiJsonSerializer<OTPDeliveryMethod> otpDeliveryMethodSerializer;
private final ToApiJsonSerializer<AccessTokenData> accessTokenSerializer;
private final DefaultToApiJsonSerializer<Map<String, Object>> toApiJsonSerializer;
private final PlatformSecurityContext context;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final TwoFactorService twoFactorService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
public String getOTPDeliveryMethods(@Context final UriInfo uriInfo) {
AppUser user = context.authenticatedUser();
List<OTPDeliveryMethod> otpDeliveryMethods = twoFactorService.getDeliveryMethodsForUser(user);
return this.otpDeliveryMethodSerializer.serialize(otpDeliveryMethods);
}
@POST
@Produces({ MediaType.APPLICATION_JSON })
public String requestToken(@QueryParam("deliveryMethod") final String deliveryMethod,
@QueryParam("extendedToken") @DefaultValue("false") boolean extendedAccessToken, @Context final UriInfo uriInfo) {
final AppUser user = context.authenticatedUser();
final OTPRequest request = twoFactorService.createNewOTPToken(user, deliveryMethod, extendedAccessToken);
return this.otpRequestSerializer.serialize(request.getMetadata());
}
@Path("validate")
@POST
@Produces({ MediaType.APPLICATION_JSON })
public String validate(@QueryParam("token") final String token) {
final AppUser user = context.authenticatedUser();
TFAccessToken accessToken = twoFactorService.createAccessTokenFromOTP(user, token);
return accessTokenSerializer.serialize(accessToken.toTokenData());
}
@Path("invalidate")
@POST
@Produces({ MediaType.APPLICATION_JSON })
public String updateConfiguration(final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().invalidateTwoFactorAccessToken().withJson(apiRequestBodyAsJson)
.build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
}
@@ -0,0 +1,69 @@
/**
* 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 jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.security.service.TwoFactorConfigurationService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
@Path("/v1/twofactor/configure")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Component
@ConditionalOnProperty("fineract.security.2fa.enabled")
@RequiredArgsConstructor
public class TwoFactorConfigurationApiResource {
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "TWOFACTOR_CONFIG";
private final PlatformSecurityContext context;
private final TwoFactorConfigurationService configurationService;
private final DefaultToApiJsonSerializer<Map<String, Object>> toApiJsonSerializer;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
public String retrieveAll() {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
Map<String, Object> configurationMap = configurationService.retrieveAll();
return toApiJsonSerializer.serialize(configurationMap);
}
@PUT
public String updateConfiguration(final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateTwoFactorConfiguration().withJson(apiRequestBodyAsJson)
.build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
}
@@ -0,0 +1,125 @@
/**
* 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 io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.constants.TwoFactorConstants;
import org.apache.fineract.infrastructure.security.data.AuthenticatedOauthUserData;
import org.apache.fineract.infrastructure.security.data.FineractJwtAuthenticationToken;
import org.apache.fineract.infrastructure.security.service.SpringSecurityPlatformSecurityContext;
import org.apache.fineract.useradministration.data.RoleData;
import org.apache.fineract.useradministration.domain.AppUser;
import org.apache.fineract.useradministration.domain.Role;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
/*
* Implementation of Oauth2 authentication APIs, loaded only when "oauth" profile is enabled.
*/
@Path("/v1/userdetails")
@Component
@ConditionalOnProperty("fineract.security.oauth2.enabled")
@Tag(name = "Fetch authenticated user details", description = "")
@RequiredArgsConstructor
public class UserDetailsApiResource {
private final ToApiJsonSerializer<AuthenticatedOauthUserData> apiJsonSerializerService;
private final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext;
@Value("${fineract.security.2fa.enabled}")
private boolean twoFactorEnabled;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Fetch authenticated user details\n", description = "checks the Authentication and returns the set roles and permissions allowed.")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = UserDetailsApiResourceSwagger.GetUserDetailsResponse.class)))
public String fetchAuthenticatedUserData() {
final SecurityContext context = SecurityContextHolder.getContext();
if (context == null) {
return null;
}
final FineractJwtAuthenticationToken authentication = (FineractJwtAuthenticationToken) context.getAuthentication();
if (authentication == null) {
return null;
}
final AppUser principal = (AppUser) authentication.getPrincipal();
if (principal == null) {
return null;
}
final Collection<String> permissions = new ArrayList<>();
AuthenticatedOauthUserData authenticatedUserData;
final Collection<GrantedAuthority> authorities = new ArrayList<>(authentication.getAuthorities());
for (final GrantedAuthority grantedAuthority : authorities) {
permissions.add(grantedAuthority.getAuthority());
}
final Collection<RoleData> roles = new ArrayList<>();
final Set<Role> userRoles = principal.getRoles();
for (final Role role : userRoles) {
roles.add(role.toData());
}
final Long officeId = principal.getOffice().getId();
final String officeName = principal.getOffice().getName();
final Long staffId = principal.getStaffId();
final String staffDisplayName = principal.getStaffDisplayName();
final EnumOptionData organisationalRole = principal.organisationalRoleData();
boolean isTwoFactorRequired = this.twoFactorEnabled
&& !principal.hasSpecificPermissionTo(TwoFactorConstants.BYPASS_TWO_FACTOR_PERMISSION);
if (this.springSecurityPlatformSecurityContext.doesPasswordHasToBeRenewed(principal)) {
authenticatedUserData = new AuthenticatedOauthUserData().setUsername(principal.getUsername()).setUserId(principal.getId())
.setAccessToken(authentication.getToken().getTokenValue()).setAuthenticated(true).setShouldRenewPassword(true)
.setTwoFactorAuthenticationRequired(isTwoFactorRequired);
} else {
authenticatedUserData = new AuthenticatedOauthUserData().setUsername(principal.getUsername()).setOfficeId(officeId)
.setOfficeName(officeName).setStaffId(staffId).setStaffDisplayName(staffDisplayName)
.setOrganisationalRole(organisationalRole).setRoles(roles).setPermissions(permissions).setUserId(principal.getId())
.setAccessToken(authentication.getToken().getTokenValue()).setAuthenticated(true)
.setTwoFactorAuthenticationRequired(isTwoFactorRequired);
}
return this.apiJsonSerializerService.serialize(authenticatedUserData);
}
}
@@ -0,0 +1,63 @@
/**
* 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 io.swagger.v3.oas.annotations.media.Schema;
import java.util.Collection;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.useradministration.data.RoleData;
/**
* Created by sanyam on 13/8/17.
*/
final class UserDetailsApiResourceSwagger {
private UserDetailsApiResourceSwagger() {
}
@Schema(description = "GetUserDetailsResponse")
public static final class GetUserDetailsResponse {
private GetUserDetailsResponse() {
}
@Schema(example = "mifos")
public String username;
@Schema(example = "1")
public Long userId;
@Schema(example = "bWlmb3M6cGFzc3dvcmQ=")
public String accessToken;
@Schema(example = "true")
public boolean authenticated;
@Schema(example = "1")
public Long officeId;
@Schema(example = "Head Office")
public String officeName;
@Schema(example = "1")
public Long staffId;
@Schema(example = "mifosStaffDisplayName")
public String staffDisplayName;
public EnumOptionData organisationalRole;
public Collection<RoleData> roles;
@Schema(example = "ALL_FUNCTIONS")
public Collection<String> permissions;
}
}
@@ -0,0 +1,100 @@
/**
* 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.command;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.security.constants.TwoFactorConstants;
import org.apache.fineract.infrastructure.security.domain.TFAccessToken;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.security.service.TwoFactorService;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "TWOFACTOR_ACCESSTOKEN", action = "INVALIDATE")
@ConditionalOnProperty("fineract.security.2fa.enabled")
public class InvalidateTFAccessTokenCommandHandler implements NewCommandSourceHandler {
private final TwoFactorService twoFactorService;
private final PlatformSecurityContext securityContext;
private final FromJsonHelper fromJsonHelper;
@Autowired
public InvalidateTFAccessTokenCommandHandler(TwoFactorService twoFactorService, PlatformSecurityContext securityContext,
FromJsonHelper fromJsonHelper) {
this.twoFactorService = twoFactorService;
this.securityContext = securityContext;
this.fromJsonHelper = fromJsonHelper;
}
@Transactional
@Override
public CommandProcessingResult processCommand(JsonCommand command) {
validateJson(command.json());
final AppUser user = securityContext.authenticatedUser();
final TFAccessToken accessToken = twoFactorService.invalidateAccessToken(user, command);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withResourceIdAsString(accessToken.getToken())
.build();
}
private void validateJson(String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromJsonHelper.checkForUnsupportedParameters(typeOfMap, json, new HashSet<>(Collections.singletonList("token")));
final JsonElement element = this.fromJsonHelper.parse(json);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(TwoFactorConstants.ACCESSTOKEN_RESOURCE_NAME);
final String token = this.fromJsonHelper.extractStringNamed("token", element);
baseDataValidator.reset().parameter("token").value(token).notNull().notBlank();
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
}
@@ -0,0 +1,56 @@
/**
* 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.command;
import java.util.Map;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.security.data.TwoFactorConfigurationValidator;
import org.apache.fineract.infrastructure.security.service.TwoFactorConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "TWOFACTOR_CONFIGURATION", action = "UPDATE")
@ConditionalOnProperty("fineract.security.2fa.enabled")
public class UpdateTwoFactorConfigCommandHandler implements NewCommandSourceHandler {
private final TwoFactorConfigurationService configurationService;
private final TwoFactorConfigurationValidator dataValidator;
@Autowired
public UpdateTwoFactorConfigCommandHandler(TwoFactorConfigurationService configurationService,
TwoFactorConfigurationValidator dataValidator) {
this.configurationService = configurationService;
this.dataValidator = dataValidator;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
this.dataValidator.validateForUpdate(command.json());
final Map<String, Object> changes = configurationService.update(command);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).with(changes).build();
}
}
@@ -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.constants;
public final class TenantConstants {
private TenantConstants() {
}
public static final String PROPERTY_RO_SCHEMA_SERVER_NAME = "FINERACT_RO_SCHEMA_SERVER_NAME";
public static final String PROPERTY_RO_SCHEMA_SERVER_PORT = "FINERACT_RO_SCHEMA_SERVER_PORT";
public static final String PROPERTY_RO_SCHEMA_SCHEMA_NAME = "FINERACT_RO_SCHEMA_SCHEMA_NAME";
public static final String PROPERTY_RO_SCHEMA_USERNAME = "FINERACT_RO_SCHEMA_USERNAME";
public static final String PROPERTY_RO_SCHEMA_PASSWORD = "FINERACT_RO_SCHEMA_PASSWORD";
public static final String PROPERTY_RO_SCHEMA_CONNECTION_PARAMETERS = "FINERACT_RO_SCHEMA_CONNECTION_PARAMETERS";
}
@@ -0,0 +1,62 @@
/**
* 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.constants;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public final class TwoFactorConfigurationConstants {
private TwoFactorConfigurationConstants() {
}
public static final String RESOURCE_NAME = "TWOFACTOR_CONFIGURATION";
public static final String ENABLE_EMAIL_DELIVERY = "otp-delivery-email-enable";
public static final String EMAIL_SUBJECT = "otp-delivery-email-subject";
public static final String EMAIL_BODY = "otp-delivery-email-body";
public static final String ENABLE_SMS_DELIVERY = "otp-delivery-sms-enable";
public static final String SMS_PROVIDER_ID = "otp-delivery-sms-provider";
public static final String SMS_MESSAGE_TEXT = "otp-delivery-sms-text";
public static final String OTP_TOKEN_LIVE_TIME = "otp-token-live-time";
public static final String OTP_TOKEN_LENGTH = "otp-token-length";
public static final String ACCESS_TOKEN_LIVE_TIME = "access-token-live-time";
public static final String ACCESS_TOKEN_LIVE_TIME_EXTENDED = "access-token-live-time-extended";
public static final Set<String> REQUEST_DATA_PARAMETERS = Collections.unmodifiableSet(
new HashSet<>(Arrays.asList(ENABLE_EMAIL_DELIVERY, EMAIL_SUBJECT, EMAIL_BODY, ENABLE_SMS_DELIVERY, SMS_PROVIDER_ID,
SMS_MESSAGE_TEXT, OTP_TOKEN_LIVE_TIME, OTP_TOKEN_LENGTH, ACCESS_TOKEN_LIVE_TIME, ACCESS_TOKEN_LIVE_TIME_EXTENDED)));
public static final List<String> STRING_PARAMETERS = Collections
.unmodifiableList(Arrays.asList(EMAIL_SUBJECT, EMAIL_BODY, SMS_MESSAGE_TEXT));
public static final List<String> BOOLEAN_PARAMETERS = Collections
.unmodifiableList(Arrays.asList(ENABLE_EMAIL_DELIVERY, ENABLE_SMS_DELIVERY));
public static final List<String> NUMBER_PARAMETERS = Collections.unmodifiableList(
Arrays.asList(SMS_PROVIDER_ID, OTP_TOKEN_LIVE_TIME, OTP_TOKEN_LENGTH, ACCESS_TOKEN_LIVE_TIME, ACCESS_TOKEN_LIVE_TIME_EXTENDED));
}
@@ -0,0 +1,34 @@
/**
* 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.constants;
public final class TwoFactorConstants {
private TwoFactorConstants() {
}
public static final String ACCESSTOKEN_RESOURCE_NAME = "TWOFACTOR_ACCESSTOKEN";
public static final String SMS_DELIVERY_METHOD_NAME = "sms";
public static final String EMAIL_DELIVERY_METHOD_NAME = "email";
public static final String BYPASS_TWO_FACTOR_PERMISSION = "BYPASS_TWOFACTOR";
}
@@ -0,0 +1,52 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.security.converter;
import java.util.Collection;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.security.data.FineractJwtAuthenticationToken;
import org.apache.fineract.infrastructure.security.service.TenantAwareJpaPlatformUserDetailsService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.NonNull;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
@RequiredArgsConstructor
public class FineractJwtAuthenticationTokenConverter implements Converter<Jwt, FineractJwtAuthenticationToken> {
private final TenantAwareJpaPlatformUserDetailsService userDetailsService;
@Override
@NonNull
public FineractJwtAuthenticationToken convert(@NonNull Jwt jwt) {
try {
UserDetails user = userDetailsService.loadUserByUsername(jwt.getSubject());
Collection<GrantedAuthority> authorities = new JwtGrantedAuthoritiesConverter().convert(jwt);
return new FineractJwtAuthenticationToken(jwt, authorities, user);
} catch (UsernameNotFoundException ex) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN), ex);
}
}
}
@@ -0,0 +1,35 @@
/**
* 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 java.time.ZonedDateTime;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@Accessors(chain = true)
public class AccessTokenData {
private String token;
private ZonedDateTime validFrom;
private ZonedDateTime validTo;
}
@@ -0,0 +1,65 @@
/**
* 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 java.util.Collection;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.useradministration.data.RoleData;
/**
* Immutable data object for authentication. Used in case of Oauth2.
*/
@Data
@NoArgsConstructor
@Accessors(chain = true)
public class AuthenticatedOauthUserData {
@SuppressWarnings("unused")
private String username;
@SuppressWarnings("unused")
private Long userId;
@SuppressWarnings("unused")
private String accessToken;
@SuppressWarnings("unused")
private boolean authenticated;
@SuppressWarnings("unused")
private Long officeId;
@SuppressWarnings("unused")
private String officeName;
@SuppressWarnings("unused")
private Long staffId;
@SuppressWarnings("unused")
private String staffDisplayName;
@SuppressWarnings("unused")
private EnumOptionData organisationalRole;
@SuppressWarnings("unused")
private Collection<RoleData> roles;
@SuppressWarnings("unused")
private Collection<String> permissions;
@SuppressWarnings("unused")
private boolean shouldRenewPassword;
@SuppressWarnings("unused")
private boolean isTwoFactorAuthenticationRequired;
}
@@ -0,0 +1,66 @@
/**
* 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 java.util.Collection;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.useradministration.data.RoleData;
/**
* Immutable data object for authentication.
*/
@Data
@NoArgsConstructor
@Accessors(chain = true)
public class AuthenticatedUserData {
@SuppressWarnings("unused")
private String username;
@SuppressWarnings("unused")
private Long userId;
@SuppressWarnings("unused")
private String base64EncodedAuthenticationKey;
@SuppressWarnings("unused")
private boolean authenticated;
@SuppressWarnings("unused")
private Long officeId;
@SuppressWarnings("unused")
private String officeName;
@SuppressWarnings("unused")
private Long staffId;
@SuppressWarnings("unused")
private String staffDisplayName;
@SuppressWarnings("unused")
private EnumOptionData organisationalRole;
@SuppressWarnings("unused")
private Collection<RoleData> roles;
@SuppressWarnings("unused")
private Collection<String> permissions;
private Collection<Long> clients;
@SuppressWarnings("unused")
private boolean shouldRenewPassword;
@SuppressWarnings("unused")
private boolean isTwoFactorAuthenticationRequired;
}
@@ -0,0 +1,42 @@
/**
* 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 java.util.Collection;
import java.util.Objects;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
public class FineractJwtAuthenticationToken extends JwtAuthenticationToken {
private final UserDetails user;
public FineractJwtAuthenticationToken(Jwt jwt, Collection<GrantedAuthority> authorities, UserDetails user) {
super(jwt, authorities, user.getUsername());
this.user = Objects.requireNonNull(user, "user");
}
@Override
public UserDetails getPrincipal() {
return user;
}
}
@@ -0,0 +1,32 @@
/**
* 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.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@Accessors(chain = true)
public class OTPDeliveryMethod {
private String name;
private String target;
}
@@ -0,0 +1,35 @@
/**
* 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 java.time.ZonedDateTime;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@Accessors(chain = true)
public class OTPMetadata {
private ZonedDateTime requestTime;
private int tokenLiveTimeInSec;
private boolean extendedAccessToken;
private OTPDeliveryMethod deliveryMethod;
}
@@ -0,0 +1,47 @@
/**
* 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 java.time.ZonedDateTime;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Data
@NoArgsConstructor
@Accessors(chain = true)
public class OTPRequest {
private String token;
private OTPMetadata metadata;
public static OTPRequest create(String token, int tokenLiveTimeInSec, boolean extendedAccessToken, OTPDeliveryMethod deliveryMethod) {
final OTPMetadata metadata = new OTPMetadata()
.setRequestTime(DateUtils.getLocalDateTimeOfTenant().atZone(DateUtils.getDateTimeZoneOfTenant()))
.setTokenLiveTimeInSec(tokenLiveTimeInSec).setExtendedAccessToken(extendedAccessToken).setDeliveryMethod(deliveryMethod);
return new OTPRequest().setToken(token).setMetadata(metadata);
}
public boolean isValid() {
ZonedDateTime expireTime = metadata.getRequestTime().plusSeconds(metadata.getTokenLiveTimeInSec());
return ZonedDateTime.now(DateUtils.getDateTimeZoneOfTenant()).isBefore(expireTime);
}
}
@@ -0,0 +1,59 @@
/**
* 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 jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.apache.commons.lang3.time.StopWatch;
/**
* Immutable data object representing platform API request used for logging/debugging.
*/
@Data
@NoArgsConstructor
@Accessors(chain = true)
public final class PlatformRequestLog {
@SuppressWarnings("unused")
private long startTime;
@SuppressWarnings("unused")
private long totalTime;
@SuppressWarnings("unused")
private String method;
@SuppressWarnings("unused")
private String url;
@SuppressWarnings("unused")
private Map<String, String[]> parameters;
public static PlatformRequestLog from(final StopWatch task, final HttpServletRequest request) throws IOException {
final String requestUrl = request.getRequestURL().toString();
final Map<String, String[]> parameters = new HashMap<>(request.getParameterMap());
parameters.remove("password");
parameters.remove("_");
return new PlatformRequestLog().setStartTime(task.getStartTime()).setTotalTime(task.getTime()).setMethod(request.getMethod())
.setUrl(requestUrl).setParameters(parameters);
}
}
@@ -0,0 +1,33 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.security.data;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@AllArgsConstructor
@Accessors(chain = true)
public class TenantAuthenticationDetails {
private String userName;
private String tenantId;
private String password;
}
@@ -0,0 +1,112 @@
/**
* 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 com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.security.constants.TwoFactorConfigurationConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
@Component
@ConditionalOnProperty("fineract.security.2fa.enabled")
public class TwoFactorConfigurationValidator {
private final FromJsonHelper fromJsonHelper;
@Autowired
public TwoFactorConfigurationValidator(FromJsonHelper fromJsonHelper) {
this.fromJsonHelper = fromJsonHelper;
}
public void validateForUpdate(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
boolean atLeastOneParameterPassedForUpdate = false;
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromJsonHelper.checkForUnsupportedParameters(typeOfMap, json, TwoFactorConfigurationConstants.REQUEST_DATA_PARAMETERS);
final JsonElement element = this.fromJsonHelper.parse(json);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(TwoFactorConfigurationConstants.RESOURCE_NAME);
for (String parameterName : TwoFactorConfigurationConstants.BOOLEAN_PARAMETERS) {
if (this.fromJsonHelper.parameterExists(parameterName, element)) {
atLeastOneParameterPassedForUpdate = true;
validateBooleanParameter(parameterName, element, baseDataValidator);
}
}
for (String parameterName : TwoFactorConfigurationConstants.STRING_PARAMETERS) {
if (this.fromJsonHelper.parameterExists(parameterName, element)) {
atLeastOneParameterPassedForUpdate = true;
validateStringParameter(parameterName, element, baseDataValidator);
}
}
for (String parameterName : TwoFactorConfigurationConstants.NUMBER_PARAMETERS) {
if (this.fromJsonHelper.parameterExists(parameterName, element)) {
atLeastOneParameterPassedForUpdate = true;
validateNumberParameter(parameterName, element, baseDataValidator);
}
}
if (!atLeastOneParameterPassedForUpdate) {
final Object forceError = null;
baseDataValidator.reset().anyOfNotNull(forceError);
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
private void validateBooleanParameter(final String name, final JsonElement element, final DataValidatorBuilder baseDataValidator) {
final String value = this.fromJsonHelper.extractStringNamed(name, element);
baseDataValidator.reset().parameter(name).value(value).notNull().trueOrFalseRequired(value);
}
private void validateStringParameter(final String name, final JsonElement element, final DataValidatorBuilder baseDataValidator) {
final String value = this.fromJsonHelper.extractStringNamed(name, element);
baseDataValidator.reset().parameter(name).value(value).notBlank().notExceedingLengthOf(1000);
}
private void validateNumberParameter(final String name, final JsonElement element, final DataValidatorBuilder baseDataValidator) {
final Integer value = this.fromJsonHelper.extractIntegerSansLocaleNamed(name, element);
baseDataValidator.reset().parameter(name).value(value).notNull().integerGreaterThanZero();
}
}
@@ -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.domain;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.fineract.infrastructure.security.data.OTPRequest;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Repository;
import org.springframework.util.Assert;
@Repository
@ConditionalOnProperty("fineract.security.2fa.enabled")
@SuppressWarnings({ "MemberName" })
public class OTPRequestRepository {
private static final ConcurrentHashMap<Long, OTPRequest> OTT_REQUESTS = new ConcurrentHashMap<>();
public OTPRequest getOTPRequestForUser(AppUser user) {
Assert.notNull(user, "User must not be null");
return OTT_REQUESTS.get(user.getId());
}
public void addOTPRequest(AppUser user, OTPRequest request) {
Assert.notNull(user, "User must not be null");
Assert.notNull(request, "Request must not be null");
OTT_REQUESTS.put(user.getId(), request);
}
public void deleteOTPRequestForUser(AppUser user) {
OTT_REQUESTS.remove(user.getId());
}
}
@@ -0,0 +1,25 @@
/**
* 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.domain;
public interface PlatformUserRepository {
PlatformUser findByUsernameAndDeletedAndEnabled(String username, boolean deleted, boolean enabled);
}
@@ -0,0 +1,78 @@
/**
* 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.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.security.data.AccessTokenData;
import org.apache.fineract.useradministration.domain.AppUser;
@Entity
@Table(name = "twofactor_access_token", uniqueConstraints = {
@UniqueConstraint(columnNames = { "token", "appuser_id" }, name = "token_appuser_UNIQUE") })
@Getter
@Setter
@NoArgsConstructor
@Accessors(chain = true)
public class TFAccessToken extends AbstractPersistableCustom<Long> {
@Column(name = "token", nullable = false, length = 32)
private String token;
@ManyToOne
@JoinColumn(name = "appuser_id", nullable = false)
private AppUser user;
@Column(name = "valid_from", nullable = false)
private LocalDateTime validFrom;
@Column(name = "valid_to", nullable = false)
private LocalDateTime validTo;
@Column(name = "enabled", nullable = false)
private boolean enabled;
public static TFAccessToken create(String token, AppUser user, int tokenLiveTimeInSec) {
LocalDateTime validFrom = DateUtils.getLocalDateTimeOfTenant();
LocalDateTime validTo = validFrom.plusSeconds(tokenLiveTimeInSec);
return new TFAccessToken().setToken(token).setUser(user).setValidFrom(validFrom).setValidTo(validTo).setEnabled(true);
}
public boolean isValid() {
// valid_from is in the past inclusive, valid_to is in the future exclusive
return this.enabled && !DateUtils.isAfterTenantDateTime(getValidFrom()) && DateUtils.isAfterTenantDateTime(getValidTo());
}
public AccessTokenData toTokenData() {
return new AccessTokenData().setToken(this.token).setValidFrom(getValidFrom().atZone(DateUtils.getDateTimeZoneOfTenant()))
.setValidTo(getValidTo().atZone(DateUtils.getDateTimeZoneOfTenant()));
}
}
@@ -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.domain;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@ConditionalOnProperty("fineract.security.2fa.enabled")
public interface TFAccessTokenRepository extends JpaRepository<TFAccessToken, Long>, JpaSpecificationExecutor<TFAccessToken> {
TFAccessToken findByUserAndToken(AppUser user, String token);
}
@@ -0,0 +1,58 @@
/**
* 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.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.security.constants.TwoFactorConfigurationConstants;
@Entity
@Table(name = "twofactor_configuration", uniqueConstraints = { @UniqueConstraint(columnNames = { "name" }, name = "name_UNIQUE") })
@Getter
@Setter
@NoArgsConstructor
@Accessors(chain = true)
public class TwoFactorConfiguration extends AbstractPersistableCustom<Long> {
@Column(name = "name", nullable = false, length = 32)
private String name;
@Column(name = "value", nullable = true, length = 1024)
private String value;
public Object getObjectValue() {
if (TwoFactorConfigurationConstants.NUMBER_PARAMETERS.contains(name)) {
return NumberUtils.createInteger(value);
}
if (TwoFactorConfigurationConstants.BOOLEAN_PARAMETERS.contains(name)) {
return BooleanUtils.toBooleanObject(value);
}
return getValue();
}
}
@@ -0,0 +1,34 @@
/**
* 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.domain;
import java.util.List;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@ConditionalOnProperty("fineract.security.2fa.enabled")
public interface TwoFactorConfigurationRepository
extends JpaRepository<TwoFactorConfiguration, Long>, JpaSpecificationExecutor<TwoFactorConfiguration> {
TwoFactorConfiguration findByName(String name);
@Override
List<TwoFactorConfiguration> findAll();
}
@@ -0,0 +1,28 @@
/**
* 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.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class AccessTokenInvalidIException extends AbstractPlatformDomainRuleException {
public AccessTokenInvalidIException() {
super("error.msg.twofactor.access.token.invalid", "The provided access token is invalid");
}
}
@@ -0,0 +1,28 @@
/**
* 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.exception;
import java.sql.SQLException;
public class EscapeSqlLiteralException extends RuntimeException {
public EscapeSqlLiteralException(String message, SQLException e) {
super(message, e);
}
}
@@ -0,0 +1,30 @@
/**
* 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.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class ForcePasswordResetException extends AbstractPlatformDomainRuleException {
public ForcePasswordResetException() {
super("error.msg.password.reset.days.value.must.be.greater.than.zero",
"For enabling 'Force Password Reset Days' configuration , the value (number of days after which a user is forced to reset his password) must be set to a number greater than 0.");
}
}
@@ -0,0 +1,29 @@
/**
* 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.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class OTPDeliveryMethodInvalidException extends AbstractPlatformDomainRuleException {
public OTPDeliveryMethodInvalidException() {
super("error.msg.twofactor.otp.delivery.invalid",
"The requested OTP delivery method " + "is not supported or not currently unavailable.");
}
}
@@ -0,0 +1,28 @@
/**
* 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.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class OTPTokenInvalidException extends AbstractPlatformDomainRuleException {
public OTPTokenInvalidException() {
super("error.msg.twofactor.otp.token.invalid", "The provided one time token is invalid");
}
}
@@ -0,0 +1,42 @@
/**
* 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.exception;
import java.util.ArrayList;
import java.util.List;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
/**
* A {@link RuntimeException} that is thrown in the case where a user does not have sufficient authorization to execute
* operation on platform.
*/
public class ResetPasswordException extends PlatformApiDataValidationException {
public ResetPasswordException(final Long userId) {
super("error.msg.password.outdated", "The password of the user with id " + userId + " has expired, please reset it",
new ArrayList<ApiParameterError>(List.of(ApiParameterError.parameterError("error.msg.password.outdated",
"The password of the user with id " + userId + " has expired, please reset it", "userId", userId)))
);
}
}
@@ -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);
}
}
@@ -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();
}
}
}
@@ -0,0 +1,191 @@
/**
* 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.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDate;
import java.util.HashMap;
import lombok.Setter;
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.AuthTenantDetailsService;
import org.apache.fineract.notification.service.UserNotificationService;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* A customised version of spring security's {@link BasicAuthenticationFilter}.
*
* This filter is responsible for extracting multi-tenant and basic auth credentials from the request and checking that
* the details provided are valid.
*
* If multi-tenant and basic auth credentials 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 and basic auth credentials are invalid, a http error response is returned.
*/
@Slf4j
public class TenantAwareBasicAuthenticationFilter extends BasicAuthenticationFilter {
private static boolean FIRST_REQUEST_PROCESSED = false;
private static final String TENANT_ID_REQUEST_HEADER = "Fineract-Platform-TenantId";
private static final boolean EXCEPTION_IF_HEADER_MISSING = true;
private final ToApiJsonSerializer<PlatformRequestLog> toApiJsonSerializer;
private final ConfigurationDomainService configurationDomainService;
private final CacheWritePlatformService cacheWritePlatformService;
private final UserNotificationService userNotificationService;
private final AuthTenantDetailsService basicAuthTenantDetailsService;
private final BusinessDateReadPlatformService businessDateReadPlatformService;
@Setter
private RequestMatcher requestMatcher = AnyRequestMatcher.INSTANCE;
public TenantAwareBasicAuthenticationFilter(final AuthenticationManager authenticationManager,
final AuthenticationEntryPoint authenticationEntryPoint, ToApiJsonSerializer<PlatformRequestLog> toApiJsonSerializer,
ConfigurationDomainService configurationDomainService, CacheWritePlatformService cacheWritePlatformService,
UserNotificationService userNotificationService, AuthTenantDetailsService basicAuthTenantDetailsService,
BusinessDateReadPlatformService businessDateReadPlatformService) {
super(authenticationManager, authenticationEntryPoint);
this.toApiJsonSerializer = toApiJsonSerializer;
this.configurationDomainService = configurationDomainService;
this.cacheWritePlatformService = cacheWritePlatformService;
this.userNotificationService = userNotificationService;
this.basicAuthTenantDetailsService = basicAuthTenantDetailsService;
this.businessDateReadPlatformService = businessDateReadPlatformService;
}
@Override
@SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT")
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
final StopWatch task = new StopWatch();
task.start();
try {
ThreadLocalContextUtil.reset();
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
// ignore to allow 'preflight' requests from AJAX applications
// in different origin (domain name)
filterChain.doFilter(request, response);
} else {
if (requestMatcher.matches(request)) {
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("Basic ")) {
ThreadLocalContextUtil.setAuthToken(authToken.replaceFirst("Basic ", ""));
}
if (!FIRST_REQUEST_PROCESSED) {
final String baseUrl = request.getRequestURL().toString().replace(request.getPathInfo(), "/");
System.setProperty("baseUrl", baseUrl);
final boolean ehcacheEnabled = configurationDomainService.isEhcacheEnabled();
if (ehcacheEnabled) {
cacheWritePlatformService.switchToCache(CacheType.SINGLE_NODE);
} else {
cacheWritePlatformService.switchToCache(CacheType.NO_CACHE);
}
TenantAwareBasicAuthenticationFilter.FIRST_REQUEST_PROCESSED = true;
}
}
super.doFilterInternal(request, response, filterChain);
}
} 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 msg = PlatformRequestLog.from(task, request);
log.debug("{}", toApiJsonSerializer.serialize(msg));
}
}
@Override
protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult)
throws IOException {
super.onSuccessfulAuthentication(request, response, authResult);
AppUser user = (AppUser) authResult.getPrincipal();
if (userNotificationService.hasUnreadUserNotifications(user.getId())) {
response.addHeader("X-Notification-Refresh", "true");
} else {
response.addHeader("X-Notification-Refresh", "false");
}
String pathURL = request.getRequestURI();
boolean isSelfServiceRequest = pathURL != null && pathURL.contains("/self/");
boolean notAllowed = (isSelfServiceRequest && !user.isSelfServiceUser()) || (!isSelfServiceRequest && user.isSelfServiceUser());
if (notAllowed) {
throw new BadCredentialsException("User not authorised to use the requested resource.");
}
}
}
@@ -0,0 +1,125 @@
/**
* 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 jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.security.constants.TwoFactorConstants;
import org.apache.fineract.infrastructure.security.data.FineractJwtAuthenticationToken;
import org.apache.fineract.infrastructure.security.domain.TFAccessToken;
import org.apache.fineract.infrastructure.security.service.TwoFactorService;
import org.apache.fineract.useradministration.domain.AppUser;
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;
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.
*
* 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
* invalid) token is provided, 403 response is returned.
*
* An authenticated platform user with permission 'BYPASS_TWOFACTOR' will always be granted 'TWOFACTOR_AUTHENTICATED'
* authority regardless of the value of the 'Fineract-Platform-TFA-Token' header.
*/
@RequiredArgsConstructor
public class TwoFactorAuthenticationFilter extends GenericFilterBean {
private final TwoFactorService twoFactorService;
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = null;
if (context != null) {
authentication = context.getAuthentication();
}
// Process two-factor only when user is authenticated
if (authentication != null && authentication.isAuthenticated() && authentication.getPrincipal() instanceof AppUser) {
AppUser user = (AppUser) authentication.getPrincipal();
if (user == null) {
return;
}
if (!user.hasSpecificPermissionTo(TwoFactorConstants.BYPASS_TWO_FACTOR_PERMISSION)) {
// User can't bypass two-factor auth, check two-factor access
// token
String token = request.getHeader("Fineract-Platform-TFA-Token");
if (token != null) {
TFAccessToken accessToken = twoFactorService.fetchAccessTokenForUser(user, token);
// Token is non-existent or invalid
if (accessToken == null || !accessToken.isValid()) {
response.addHeader("WWW-Authenticate", "Basic realm=\"Fineract Platform API Two Factor\"");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid two-factor access token provided");
return;
}
} else {
// No token provided
chain.doFilter(req, res);
return;
}
}
List<GrantedAuthority> updatedAuthorities = new ArrayList<>(authentication.getAuthorities());
updatedAuthorities.add(new SimpleGrantedAuthority("TWOFACTOR_AUTHENTICATED"));
context.setAuthentication(createUpdatedAuthentication(authentication, updatedAuthorities));
}
chain.doFilter(req, res);
}
private Authentication createUpdatedAuthentication(final Authentication currentAuthentication,
final List<GrantedAuthority> updatedAuthorities) throws ServletException {
if (currentAuthentication instanceof UsernamePasswordAuthenticationToken) {
UsernamePasswordAuthenticationToken updatedAuthentication = new UsernamePasswordAuthenticationToken(
currentAuthentication.getPrincipal(), currentAuthentication.getCredentials(), updatedAuthorities);
return updatedAuthentication;
} else if (currentAuthentication instanceof FineractJwtAuthenticationToken) {
FineractJwtAuthenticationToken fineractJwtAuthenticationToken = (FineractJwtAuthenticationToken) currentAuthentication;
FineractJwtAuthenticationToken updatedAuthentication = new FineractJwtAuthenticationToken(
fineractJwtAuthenticationToken.getToken(), updatedAuthorities, (UserDetails) currentAuthentication.getPrincipal());
return updatedAuthentication;
} else {
throw new ServletException("Unknown authentication type: " + currentAuthentication.getClass().getName());
}
}
}
@@ -0,0 +1,36 @@
/**
* 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.
*/
/**
* Fineract Security Module.
*
* This module contains all security-related components for Apache Fineract including:
* <ul>
* <li>Authentication APIs (HTTP Basic, OAuth2, OIDC Federation)</li>
* <li>Two-Factor Authentication (2FA/TFA)</li>
* <li>Security filters and converters</li>
* <li>User authentication services</li>
* <li>Security configuration</li>
* </ul>
*
* @see org.apache.fineract.infrastructure.security.api
* @see org.apache.fineract.infrastructure.security.config
* @see org.apache.fineract.infrastructure.security.service
*/
package org.apache.fineract.infrastructure.security;
@@ -0,0 +1,24 @@
/**
* 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.service;
public interface AccessTokenGenerationService {
String generateRandomToken();
}
@@ -0,0 +1,26 @@
/**
* 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.service;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
public interface AuthTenantDetailsService {
FineractPlatformTenant loadTenantById(String tenantId, boolean isReport);
}
@@ -0,0 +1,59 @@
/**
* 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.service;
import javax.sql.DataSource;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.service.tenant.TenantMapper;
import org.apache.fineract.infrastructure.security.exception.InvalidTenantIdentifierException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
/**
* A JDBC implementation of {@link AuthTenantDetailsService} for loading a tenants details by a
* <code>tenantIdentifier</code>.
*/
@Service
public class AuthTenantDetailsServiceJdbc implements AuthTenantDetailsService {
private final JdbcTemplate jdbcTemplate;
@Autowired
public AuthTenantDetailsServiceJdbc(@Qualifier("hikariTenantDataSource") final DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
@Cacheable(value = "tenantsById")
public FineractPlatformTenant loadTenantById(final String tenantIdentifier, final boolean isReport) {
try {
final TenantMapper rm = new TenantMapper(isReport);
final String sql = "select " + rm.schema() + " where t.identifier = ?";
return this.jdbcTemplate.queryForObject(sql, rm, new Object[] { tenantIdentifier }); // NOSONAR
} catch (final EmptyResultDataAccessException e) {
throw new InvalidTenantIdentifierException("The tenant identifier: " + tenantIdentifier + " is not valid.", e);
}
}
}
@@ -0,0 +1,137 @@
/**
* 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.service;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
@Slf4j
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
private String defaultFailureUrl;
private boolean forwardToDestination = false;
private boolean allowSessionCreation = true;
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
public CustomAuthenticationFailureHandler() {}
/**
* Performs the redirect or forward to the {@code defaultFailureUrl} if set, otherwise returns a 401 error code.
* <p>
* If redirecting or forwarding, {@code saveException} will be called to cache the exception for use in the target
* view.
*/
@Override
public void onAuthenticationFailure(final HttpServletRequest request, final HttpServletResponse response,
final AuthenticationException exception) throws IOException, ServletException {
if (this.defaultFailureUrl == null) {
log.debug("No failure URL set, sending 401 Unauthorized error");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication Failed: " + exception.getMessage());
} else {
saveException(request, exception);
if (this.forwardToDestination) {
log.debug("Forwarding to {}", this.defaultFailureUrl);
request.getRequestDispatcher(this.defaultFailureUrl).forward(request, response);
} else {
log.debug("Redirecting to {}", this.defaultFailureUrl);
final String oauthToken = request.getParameter("oauth_token");
request.setAttribute("oauth_token", oauthToken);
final String url = this.defaultFailureUrl + "?oauth_token=" + oauthToken;
this.redirectStrategy.sendRedirect(request, response, url);
}
}
}
/**
* Caches the {@code AuthenticationException} for use in view rendering.
* <p>
* If {@code forwardToDestination} is set to true, request scope will be used, otherwise it will attempt to store
* the exception in the session. If there is no session and {@code allowSessionCreation} is {@code true} a session
* will be created. Otherwise the exception will not be stored.
*/
protected final void saveException(final HttpServletRequest request, final AuthenticationException exception) {
if (this.forwardToDestination) {
request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
} else {
final HttpSession session = request.getSession(false);
if (session != null || this.allowSessionCreation) {
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
}
}
}
/**
* The URL which will be used as the failure destination.
*
* @param defaultFailureUrl
* the failure URL, for example "/loginFailed.jsp".
*/
public void setDefaultFailureUrl(final String defaultFailureUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(defaultFailureUrl), "'" + defaultFailureUrl + "' is not a valid redirect URL");
this.defaultFailureUrl = defaultFailureUrl;
}
protected boolean isUseForward() {
return this.forwardToDestination;
}
/**
* If set to <tt>true</tt>, performs a forward to the failure destination URL instead of a redirect. Defaults to
* <tt>false</tt>.
*/
public void setUseForward(final boolean forwardToDestination) {
this.forwardToDestination = forwardToDestination;
}
/**
* Allows overriding of the behaviour when redirecting to a target URL.
*/
public void setRedirectStrategy(final RedirectStrategy redirectStrategy) {
this.redirectStrategy = redirectStrategy;
}
protected RedirectStrategy getRedirectStrategy() {
return this.redirectStrategy;
}
protected boolean isAllowSessionCreation() {
return this.allowSessionCreation;
}
public void setAllowSessionCreation(final boolean allowSessionCreation) {
this.allowSessionCreation = allowSessionCreation;
}
}
@@ -0,0 +1,41 @@
/**
* 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.service;
import java.security.SecureRandom;
public class RandomOTPGenerator {
private static final String allowedCharacters = "0123456789ABCDEFGHIJKLMNOPQRSTUVQXYZ";
private final int tokenLength;
private final SecureRandom secureRandom = new SecureRandom();
public RandomOTPGenerator(int tokenLength) {
this.tokenLength = tokenLength;
}
public String generate() {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < tokenLength; i++) {
builder.append(allowedCharacters.charAt((int) (secureRandom.nextDouble() * allowedCharacters.length())));
}
return builder.toString();
}
}
@@ -0,0 +1,177 @@
/**
* 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.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;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException;
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.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
/**
* Wrapper around spring security's {@link SecurityContext} for extracted the current authenticated {@link AppUser}.
*/
@Service
@RequiredArgsConstructor
public class SpringSecurityPlatformSecurityContext implements PlatformSecurityContext {
// private static final Logger LOG =
// LoggerFactory.getLogger(SpringSecurityPlatformSecurityContext.class);
private final ConfigurationDomainService configurationDomainService;
protected static final List<CommandWrapper> EXEMPT_FROM_PASSWORD_RESET_CHECK = new ArrayList<CommandWrapper>(
List.of(new CommandWrapperBuilder().updateUser(null).build()));
@Override
public AppUser authenticatedUser() {
AppUser currentUser = null;
final SecurityContext context = SecurityContextHolder.getContext();
if (context != null) {
final Authentication auth = context.getAuthentication();
if (auth != null) {
Object principal = auth.getPrincipal();
if (principal instanceof AppUser appUser) {
currentUser = appUser;
}
}
}
if (currentUser == null) {
throw new UnAuthenticatedUserException();
}
if (this.doesPasswordHasToBeRenewed(currentUser)) {
throw new ResetPasswordException(currentUser.getId());
}
return currentUser;
}
@Override
public void isAuthenticated() {
authenticatedUser();
}
@Override
public AppUser getAuthenticatedUserIfPresent() {
AppUser currentUser = null;
final SecurityContext context = SecurityContextHolder.getContext();
if (context != null) {
final Authentication auth = context.getAuthentication();
if (auth != null) {
currentUser = (AppUser) auth.getPrincipal();
}
}
if (currentUser == null) {
return null;
}
if (this.doesPasswordHasToBeRenewed(currentUser)) {
throw new ResetPasswordException(currentUser.getId());
}
return currentUser;
}
@Override
public AppUser authenticatedUser(CommandWrapper commandWrapper) {
AppUser currentUser = null;
final SecurityContext context = SecurityContextHolder.getContext();
if (context != null) {
final Authentication auth = context.getAuthentication();
if (auth != null) {
currentUser = (AppUser) auth.getPrincipal();
}
}
if (currentUser == null) {
throw new UnAuthenticatedUserException();
}
if (this.shouldCheckForPasswordForceReset(commandWrapper) && this.doesPasswordHasToBeRenewed(currentUser)) {
throw new ResetPasswordException(currentUser.getId());
}
return currentUser;
}
@Override
public void validateAccessRights(final String resourceOfficeHierarchy) {
final AppUser user = authenticatedUser();
final String userOfficeHierarchy = user.getOffice().getHierarchy();
if (!resourceOfficeHierarchy.startsWith(userOfficeHierarchy)) {
throw new NoAuthorizationException("The user doesn't have enough permissions to access the resource.");
}
}
@Override
public String officeHierarchy() {
return authenticatedUser().getOffice().getHierarchy();
}
@Override
public boolean doesPasswordHasToBeRenewed(AppUser currentUser) {
if (this.configurationDomainService.isPasswordForcedResetEnable() && !currentUser.getPasswordNeverExpires()) {
Long passwordDurationDays = this.configurationDomainService.retrievePasswordLiveTime();
final LocalDate passWordLastUpdateDate = currentUser.getLastTimePasswordUpdated();
final LocalDate passwordExpirationDate = passWordLastUpdateDate.plusDays(passwordDurationDays);
if (DateUtils.isBeforeTenantDate(passwordExpirationDate)) {
return true;
}
}
return false;
}
private boolean shouldCheckForPasswordForceReset(CommandWrapper commandWrapper) {
for (CommandWrapper commandItem : EXEMPT_FROM_PASSWORD_RESET_CHECK) {
if (commandItem.actionName().equals(commandWrapper.actionName())
&& commandItem.getEntityName().equals(commandWrapper.getEntityName())) {
return false;
}
}
return true;
}
}
@@ -0,0 +1,149 @@
/**
* 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.service;
import java.sql.SQLException;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver;
import org.apache.fineract.infrastructure.security.exception.EscapeSqlLiteralException;
import org.postgresql.core.Utils;
import org.springframework.stereotype.Service;
@Service
public class SqlInjectionPreventerServiceImpl implements SqlInjectionPreventerService {
private final DatabaseTypeResolver databaseTypeResolver;
public SqlInjectionPreventerServiceImpl(DatabaseTypeResolver databaseTypeResolver) {
this.databaseTypeResolver = databaseTypeResolver;
}
@Override
public String encodeSql(String literal) {
if (literal == null) {
return "NULL";
}
if (databaseTypeResolver.isMySQL()) {
return escapeMySQLLiteral(literal);
} else if (databaseTypeResolver.isPostgreSQL()) {
try {
return Utils.escapeLiteral(null, literal, true).toString();
} catch (SQLException e) {
throw new EscapeSqlLiteralException("Failed to escape SQL literal", e);
}
} else {
// For unknown database types, return the input unchanged
// This maintains backward compatibility while still providing basic protection for known types
return literal;
}
}
/**
* Escapes a string literal for MySQL/MariaDB using native MySQL standard escaping rules. This method replaces the
* vulnerable ESAPI SQL encoding to address CVE-2025-5878.
*
* According to MySQL documentation, the following characters need to be escaped: - Single quote (') -> \' - Double
* quote (") -> \" - Backslash (\) -> \\ - Null byte (\0) -> \\0 - Newline (\n) -> \\n - Carriage return (\r) -> \\r
* - Tab (\t) -> \\t - ASCII 26 (Ctrl+Z) -> \\Z
*
* @param literal
* the string literal to escape
* @return the escaped string literal safe for MySQL/MariaDB SQL queries
*/
private String escapeMySQLLiteral(String literal) {
StringBuilder escaped = new StringBuilder(literal.length() * 2);
for (int i = 0; i < literal.length(); i++) {
char c = literal.charAt(i);
switch (c) {
case '\0': // Null byte
escaped.append("\\0");
break;
case '\'':
escaped.append("\\'");
break;
case '\"':
escaped.append("\\\"");
break;
case '\\':
escaped.append("\\\\");
break;
case '\n':
escaped.append("\\n");
break;
case '\r':
escaped.append("\\r");
break;
case '\t':
escaped.append("\\t");
break;
case '\032':
escaped.append("\\Z");
break;
default:
escaped.append(c);
break;
}
}
return escaped.toString();
}
@Override
public String quoteIdentifier(String identifier, Set<String> allowedValues) {
if (StringUtils.isBlank(identifier)) {
throw new IllegalArgumentException("Identifier cannot be null or empty");
}
// Whitelist validation if provided
if (allowedValues != null && !allowedValues.contains(identifier.toLowerCase())) {
throw new IllegalArgumentException("Identifier not in whitelist: " + identifier);
}
return quoteIdentifier(identifier);
}
@Override
public String quoteIdentifier(String identifier) {
if (StringUtils.isBlank(identifier)) {
throw new IllegalArgumentException("Identifier cannot be null or empty");
}
// Validate identifier contains only safe characters (alphanumeric and underscore)
if (!identifier.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) {
throw new IllegalArgumentException("Invalid identifier format: " + identifier);
}
if (databaseTypeResolver.isMySQL()) {
// MySQL/MariaDB uses backticks for identifier quoting
return "`" + identifier.replace("`", "``") + "`";
} else if (databaseTypeResolver.isPostgreSQL()) {
// PostgreSQL uses double quotes for identifier quoting
return "\"" + identifier.replace("\"", "\"\"") + "\"";
} else {
// For unknown database types, return identifier as-is if it passes validation
// This maintains backward compatibility while still providing basic protection
return identifier;
}
}
}
@@ -0,0 +1,56 @@
/**
* 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.service;
import org.apache.fineract.infrastructure.security.domain.PlatformUser;
import org.apache.fineract.infrastructure.security.domain.PlatformUserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.dao.DataAccessException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
* Used in securityContext.xml as implementation of spring security's {@link UserDetailsService}.
*/
@Service("userDetailsService")
public class TenantAwareJpaPlatformUserDetailsService implements PlatformUserDetailsService {
@Autowired
private PlatformUserRepository platformUserRepository;
@Override
@Cacheable(value = "usersByUsername", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat(#username+'ubu')")
public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException, DataAccessException {
// Retrieve active users only
final boolean deleted = false;
final boolean enabled = true;
final PlatformUser appUser = this.platformUserRepository.findByUsernameAndDeletedAndEnabled(username, deleted, enabled);
if (appUser == null) {
throw new UsernameNotFoundException(username + ": not found");
}
return appUser;
}
}
@@ -0,0 +1,57 @@
/**
* 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.service;
import java.util.Map;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.security.data.OTPRequest;
import org.apache.fineract.useradministration.domain.AppUser;
public interface TwoFactorConfigurationService {
Map<String, Object> retrieveAll();
boolean isSMSEnabled();
Integer getSMSProviderId();
String getSmsText();
boolean isEmailEnabled();
String getEmailSubject();
String getEmailBody();
String getFormattedEmailSubjectFor(AppUser user, OTPRequest request);
String getFormattedEmailBodyFor(AppUser user, OTPRequest request);
String getFormattedSmsTextFor(AppUser user, OTPRequest request);
Integer getOTPTokenLength();
Integer getOTPTokenLiveTime();
Integer getAccessTokenLiveTime();
Integer getAccessTokenExtendedLiveTime();
Map<String, Object> update(JsonCommand command);
}
@@ -0,0 +1,42 @@
/**
* 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.service;
import java.util.List;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.security.data.OTPDeliveryMethod;
import org.apache.fineract.infrastructure.security.data.OTPRequest;
import org.apache.fineract.infrastructure.security.domain.TFAccessToken;
import org.apache.fineract.useradministration.domain.AppUser;
public interface TwoFactorService {
List<OTPDeliveryMethod> getDeliveryMethodsForUser(AppUser user);
OTPRequest createNewOTPToken(AppUser user, String deliveryMethodName, boolean extendedAccessToken);
TFAccessToken createAccessTokenFromOTP(AppUser user, String otpToken);
void validateTwoFactorAccessToken(AppUser user, String token);
TFAccessToken fetchAccessTokenForUser(AppUser user, String token);
TFAccessToken invalidateAccessToken(AppUser user, JsonCommand command);
}
@@ -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.service;
import java.util.UUID;
import org.springframework.stereotype.Service;
@Service
public class UUIDAccessTokenGenerationService implements AccessTokenGenerationService {
@Override
public String generateRandomToken() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
}
@@ -0,0 +1,51 @@
/**
* 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.vote;
import java.util.function.Supplier;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
public class SelfServiceUserAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext fi) {
if (!"OPTIONS".equalsIgnoreCase(fi.getRequest().getMethod())) {
AppUser user = (AppUser) authentication.get().getPrincipal();
String pathURL = fi.getRequest().getRequestURL().toString();
boolean isSelfServiceRequest = pathURL.contains("/self/");
boolean notAllowed = ((isSelfServiceRequest && !user.isSelfServiceUser())
|| (!isSelfServiceRequest && user.isSelfServiceUser()));
if (notAllowed) {
return new AuthorizationDecision(false);
}
}
return new AuthorizationDecision(true);
}
public static SelfServiceUserAuthorizationManager selfServiceUserAuthManager() {
return new SelfServiceUserAuthorizationManager();
}
}