FINERACT-2003: Enforce password reset on first login
:wq
This commit is contained in:
+1
@@ -81,6 +81,7 @@ public final class GlobalConfigurationConstants {
|
||||
public static final String ALLOWED_LOAN_STATUSES_OF_DELAYED_SETTLEMENT_FOR_EXTERNAL_ASSET_TRANSFER = "allowed-loan-statuses-of-delayed-settlement-for-external-asset-transfer";
|
||||
public static final String ENABLE_ORIGINATOR_CREATION_DURING_LOAN_APPLICATION = "enable-originator-creation-during-loan-application";
|
||||
public static final String PASSWORD_REUSE_CHECK_HISTORY_COUNT = "password-reuse-check-history-count";
|
||||
public static final String FORCE_PASSWORD_RESET_ON_FIRST_LOGIN = "force-password-reset-on-first-login";
|
||||
|
||||
private GlobalConfigurationConstants() {}
|
||||
}
|
||||
|
||||
+2
@@ -153,4 +153,6 @@ public interface ConfigurationDomainService {
|
||||
String getAssetOwnerTransferOustandingInterestStrategy();
|
||||
|
||||
Integer getPasswordReuseRestrictionCount();
|
||||
|
||||
boolean isForcePasswordResetOnFirstLoginEnabled();
|
||||
}
|
||||
|
||||
+11
@@ -131,6 +131,17 @@ public class AppUser extends AbstractPersistableCustom<Long> implements Platform
|
||||
@Column(name = "cannot_change_password", nullable = true)
|
||||
private Boolean cannotChangePassword;
|
||||
|
||||
@Column(name = "password_reset_required", nullable = false)
|
||||
private boolean passwordResetRequired;
|
||||
|
||||
public boolean isPasswordResetRequired() {
|
||||
return this.passwordResetRequired;
|
||||
}
|
||||
|
||||
public void updatePasswordResetRequired(final boolean required) {
|
||||
this.passwordResetRequired = required;
|
||||
}
|
||||
|
||||
public static AppUser fromJson(final Office userOffice, final Staff linkedStaff, final Set<Role> allRoles,
|
||||
final Collection<Client> clients, final JsonCommand command) {
|
||||
|
||||
|
||||
+5
@@ -559,4 +559,9 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService
|
||||
Long value = property.getValue();
|
||||
return value != null && value > 0 ? value.intValue() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isForcePasswordResetOnFirstLoginEnabled() {
|
||||
return getGlobalConfigurationPropertyData(GlobalConfigurationConstants.FORCE_PASSWORD_RESET_ON_FIRST_LOGIN).isEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -45,6 +45,7 @@ import org.apache.fineract.infrastructure.security.data.PlatformRequestLog;
|
||||
import org.apache.fineract.infrastructure.security.filter.TenantAwareBasicAuthenticationFilter;
|
||||
import org.apache.fineract.infrastructure.security.filter.TwoFactorAuthenticationFilter;
|
||||
import org.apache.fineract.infrastructure.security.service.AuthTenantDetailsService;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformUserDetailsChecker;
|
||||
import org.apache.fineract.infrastructure.security.service.TenantAwareJpaPlatformUserDetailsService;
|
||||
import org.apache.fineract.infrastructure.security.service.TwoFactorService;
|
||||
import org.apache.fineract.notification.service.UserNotificationService;
|
||||
@@ -115,7 +116,9 @@ public class SecurityConfig {
|
||||
@Autowired
|
||||
private IdempotencyStoreHelper idempotencyStoreHelper;
|
||||
@Autowired
|
||||
ProgressiveLoanModelCheckerFilter progressiveLoanModelCheckerFilter;
|
||||
private ProgressiveLoanModelCheckerFilter progressiveLoanModelCheckerFilter;
|
||||
@Autowired
|
||||
private PlatformUserDetailsChecker platformUserDetailsChecker;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
@@ -418,6 +421,7 @@ public class SecurityConfig {
|
||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
||||
authProvider.setUserDetailsService(userDetailsService);
|
||||
authProvider.setPasswordEncoder(passwordEncoder());
|
||||
authProvider.setPostAuthenticationChecks(platformUserDetailsChecker);
|
||||
return authProvider;
|
||||
}
|
||||
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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.springframework.security.authentication.CredentialsExpiredException;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsChecker;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Checks user details during Spring Security authentication. Password reset enforcement is handled by
|
||||
* SpringSecurityPlatformSecurityContext and AuthenticationApiResource after authentication succeeds.
|
||||
*/
|
||||
@Component
|
||||
public class PlatformUserDetailsChecker implements UserDetailsChecker {
|
||||
|
||||
@Override
|
||||
public void check(UserDetails userDetails) {
|
||||
if (!userDetails.isCredentialsNonExpired()) {
|
||||
throw new CredentialsExpiredException("User credentials have expired");
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -23,6 +23,7 @@ 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.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.POST;
|
||||
@@ -53,7 +54,9 @@ public class SelfAuthenticationApiResource {
|
||||
@Operation(summary = "Verify authentication", description = "Authenticates the credentials provided and returns the set roles and permissions allowed.\n\n"
|
||||
+ "Please visit this link for more info - https://fineract.apache.org/docs/legacy/#selfbasicauth")
|
||||
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = AuthenticationApiResourceSwagger.PostAuthenticationRequest.class)))
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SelfAuthenticationApiResourceSwagger.PostSelfAuthenticationResponse.class)))
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SelfAuthenticationApiResourceSwagger.PostSelfAuthenticationResponse.class))),
|
||||
@ApiResponse(responseCode = "403", description = "Password reset required") })
|
||||
public String authenticate(final String apiRequestBodyAsJson) {
|
||||
return this.authenticationApiResource.authenticate(apiRequestBodyAsJson, true);
|
||||
}
|
||||
|
||||
+13
-4
@@ -127,6 +127,9 @@ public class AppUserWritePlatformServiceJpaRepositoryImpl implements AppUserWrit
|
||||
}
|
||||
|
||||
AppUser appUser = AppUser.fromJson(userOffice, linkedStaff, allRoles, clients, command);
|
||||
if (this.configurationDomainService.isForcePasswordResetOnFirstLoginEnabled()) {
|
||||
appUser.updatePasswordResetRequired(true);
|
||||
}
|
||||
|
||||
final Boolean sendPasswordToEmail = command.booleanObjectValueOfParameterNamed("sendPasswordToEmail");
|
||||
this.userDomainService.create(appUser, sendPasswordToEmail);
|
||||
@@ -160,12 +163,14 @@ public class AppUserWritePlatformServiceJpaRepositoryImpl implements AppUserWrit
|
||||
@Caching(evict = { @CacheEvict(value = "users", allEntries = true), @CacheEvict(value = "usersByUsername", allEntries = true) })
|
||||
public CommandProcessingResult changeUserPassword(final Long userId, final JsonCommand command) {
|
||||
try {
|
||||
this.context.authenticatedUser(new CommandWrapperBuilder().updateUser(null).build());
|
||||
this.fromApiJsonDeserializer.validateForChangePassword(command.json(), this.context.authenticatedUser());
|
||||
this.context.authenticatedUser(new CommandWrapperBuilder().changeUserPassword(userId).build());
|
||||
this.fromApiJsonDeserializer.validateForChangePassword(command.json(),
|
||||
this.context.authenticatedUser(new CommandWrapperBuilder().changeUserPassword(userId).build()));
|
||||
final AppUser userToUpdate = this.appUserRepository.findById(userId).orElseThrow(() -> new UserNotFoundException(userId));
|
||||
final AppUserPreviousPassword currentPasswordToSaveAsPreview = getCurrentPasswordToSaveAsPreview(userToUpdate, command);
|
||||
final Map<String, Object> changes = userToUpdate.changePassword(command, this.platformPasswordEncoder);
|
||||
if (!changes.isEmpty()) {
|
||||
userToUpdate.updatePasswordResetRequired(false);
|
||||
this.appUserRepository.saveAndFlush(userToUpdate);
|
||||
if (currentPasswordToSaveAsPreview != null) {
|
||||
this.appUserPreviewPasswordRepository.save(currentPasswordToSaveAsPreview);
|
||||
@@ -190,9 +195,9 @@ public class AppUserWritePlatformServiceJpaRepositoryImpl implements AppUserWrit
|
||||
@Caching(evict = { @CacheEvict(value = "users", allEntries = true), @CacheEvict(value = "usersByUsername", allEntries = true) })
|
||||
public CommandProcessingResult updateUser(final Long userId, final JsonCommand command) {
|
||||
try {
|
||||
this.context.authenticatedUser(new CommandWrapperBuilder().updateUser(null).build());
|
||||
final AppUser currentUser = this.context.authenticatedUser(new CommandWrapperBuilder().updateUser(null).build());
|
||||
|
||||
this.fromApiJsonDeserializer.validateForUpdate(command.json(), this.context.authenticatedUser());
|
||||
this.fromApiJsonDeserializer.validateForUpdate(command.json(), currentUser);
|
||||
|
||||
final AppUser userToUpdate = this.appUserRepository.findById(userId).orElseThrow(() -> new UserNotFoundException(userId));
|
||||
|
||||
@@ -238,6 +243,10 @@ public class AppUserWritePlatformServiceJpaRepositoryImpl implements AppUserWrit
|
||||
}
|
||||
|
||||
if (!changes.isEmpty()) {
|
||||
if ((changes.containsKey("password") || changes.containsKey("passwordEncoded")) && !currentUser.getId().equals(userId)
|
||||
&& this.configurationDomainService.isForcePasswordResetOnFirstLoginEnabled()) {
|
||||
userToUpdate.updatePasswordResetRequired(true);
|
||||
}
|
||||
this.appUserRepository.saveAndFlush(userToUpdate);
|
||||
|
||||
if (currentPasswordToSaveAsPreview != null) {
|
||||
|
||||
@@ -230,4 +230,5 @@
|
||||
<include file="parts/0209_transaction_summary_with_asset_owner_and_from_asset_owner_id_for_asset_sales.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0210_add_configuration_password_reuse_check_history_count.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0211_trial_balance_summary_fix_external_owners_and_aggregation.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0212_add_force_password_reset_config.xml" relativeToChangelogFile="true" />
|
||||
</databaseChangeLog>
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
|
||||
<changeSet author="fineract" id="1">
|
||||
<addColumn tableName="m_appuser">
|
||||
<column name="password_reset_required" type="boolean" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="3">
|
||||
<insert tableName="c_configuration">
|
||||
<column name="name" value="force-password-reset-on-first-login"/>
|
||||
<column name="value" valueNumeric="0"/>
|
||||
<column name="enabled" valueBoolean="false"/>
|
||||
<column name="is_trap_door" valueBoolean="false"/>
|
||||
<column name="description" value="If enabled, users must reset their password upon first login or after an admin reset. Value is unused."/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+3
@@ -43,6 +43,7 @@ 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.exception.PasswordResetRequiredException;
|
||||
import org.apache.fineract.infrastructure.security.service.SpringSecurityPlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.client.service.ClientReadPlatformService;
|
||||
import org.apache.fineract.useradministration.data.RoleData;
|
||||
@@ -86,6 +87,7 @@ public class AuthenticationApiResource {
|
||||
@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")
|
||||
@ApiResponse(responseCode = "403", description = "Password reset required")
|
||||
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
|
||||
@@ -137,6 +139,7 @@ public class AuthenticationApiResource {
|
||||
authenticatedUserData = new AuthenticatedUserData().setUsername(request.username).setUserId(userId)
|
||||
.setBase64EncodedAuthenticationKey(new String(base64EncodedAuthenticationKey, StandardCharsets.UTF_8))
|
||||
.setAuthenticated(true).setShouldRenewPassword(true).setTwoFactorAuthenticationRequired(isTwoFactorRequired);
|
||||
throw new PasswordResetRequiredException(authenticatedUserData);
|
||||
} else {
|
||||
|
||||
authenticatedUserData = new AuthenticatedUserData().setUsername(request.username).setOfficeId(officeId)
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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.security.data.AuthenticatedUserData;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
/**
|
||||
* Exception thrown when a user must reset their password before proceeding.
|
||||
*
|
||||
* This exception is thrown during authentication when the user's credentials are valid but they are required to change
|
||||
* their password (e.g., on first login or after an admin reset). It carries the authenticated user data so the client
|
||||
* receives enough information to proceed with the password reset flow.
|
||||
*/
|
||||
public class PasswordResetRequiredException extends AuthenticationException {
|
||||
|
||||
private final AuthenticatedUserData authenticatedUserData;
|
||||
|
||||
public PasswordResetRequiredException(AuthenticatedUserData authenticatedUserData) {
|
||||
super("Password reset required");
|
||||
this.authenticatedUserData = authenticatedUserData;
|
||||
}
|
||||
|
||||
public AuthenticatedUserData getAuthenticatedUserData() {
|
||||
return authenticatedUserData;
|
||||
}
|
||||
}
|
||||
+57
@@ -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.exception;
|
||||
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import jakarta.ws.rs.ext.ExceptionMapper;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
|
||||
import org.apache.fineract.infrastructure.security.data.AuthenticatedUserData;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* An {@link ExceptionMapper} to map {@link PasswordResetRequiredException} thrown during authentication into a HTTP API
|
||||
* friendly format.
|
||||
*
|
||||
* The exception is thrown when a user's credentials are valid but they must reset their password before proceeding.
|
||||
* This mapper returns a 403 FORBIDDEN response with the authenticated user data, including the
|
||||
* {@code shouldRenewPassword} flag set to true.
|
||||
*/
|
||||
@Provider
|
||||
@Component
|
||||
@Scope("singleton")
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class PasswordResetRequiredExceptionMapper implements ExceptionMapper<PasswordResetRequiredException> {
|
||||
|
||||
private final ToApiJsonSerializer<AuthenticatedUserData> apiJsonSerializer;
|
||||
|
||||
@Override
|
||||
public Response toResponse(final PasswordResetRequiredException exception) {
|
||||
log.warn("Exception occurred", ErrorHandler.findMostSpecificException(exception));
|
||||
return Response.status(Status.FORBIDDEN).entity(apiJsonSerializer.serialize(exception.getAuthenticatedUserData()))
|
||||
.type(MediaType.APPLICATION_JSON).build();
|
||||
}
|
||||
}
|
||||
+8
-4
@@ -49,7 +49,7 @@ public class SpringSecurityPlatformSecurityContext implements PlatformSecurityCo
|
||||
private final ConfigurationDomainService configurationDomainService;
|
||||
|
||||
protected static final List<CommandWrapper> EXEMPT_FROM_PASSWORD_RESET_CHECK = new ArrayList<CommandWrapper>(
|
||||
List.of(new CommandWrapperBuilder().updateUser(null).build()));
|
||||
List.of(new CommandWrapperBuilder().changeUserPassword(null).build()));
|
||||
|
||||
@Override
|
||||
public AppUser authenticatedUser() {
|
||||
@@ -121,7 +121,7 @@ public class SpringSecurityPlatformSecurityContext implements PlatformSecurityCo
|
||||
throw new UnAuthenticatedUserException();
|
||||
}
|
||||
|
||||
if (this.shouldCheckForPasswordForceReset(commandWrapper) && this.doesPasswordHasToBeRenewed(currentUser)) {
|
||||
if (this.shouldCheckForPasswordForceReset(commandWrapper, currentUser) && this.doesPasswordHasToBeRenewed(currentUser)) {
|
||||
throw new ResetPasswordException(currentUser.getId());
|
||||
}
|
||||
|
||||
@@ -149,6 +149,10 @@ public class SpringSecurityPlatformSecurityContext implements PlatformSecurityCo
|
||||
@Override
|
||||
public boolean doesPasswordHasToBeRenewed(AppUser currentUser) {
|
||||
|
||||
if (currentUser.isPasswordResetRequired()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.configurationDomainService.isPasswordForcedResetEnable() && !currentUser.getPasswordNeverExpires()) {
|
||||
|
||||
Long passwordDurationDays = this.configurationDomainService.retrievePasswordLiveTime();
|
||||
@@ -164,11 +168,11 @@ public class SpringSecurityPlatformSecurityContext implements PlatformSecurityCo
|
||||
|
||||
}
|
||||
|
||||
private boolean shouldCheckForPasswordForceReset(CommandWrapper commandWrapper) {
|
||||
private boolean shouldCheckForPasswordForceReset(CommandWrapper commandWrapper, AppUser currentUser) {
|
||||
for (CommandWrapper commandItem : EXEMPT_FROM_PASSWORD_RESET_CHECK) {
|
||||
if (commandItem.actionName().equals(commandWrapper.actionName())
|
||||
&& commandItem.getEntityName().equals(commandWrapper.getEntityName())) {
|
||||
return false;
|
||||
return commandWrapper.getEntityId() == null || !commandWrapper.getEntityId().equals(currentUser.getId());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 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.integrationtests;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.builder.RequestSpecBuilder;
|
||||
import io.restassured.builder.ResponseSpecBuilder;
|
||||
import io.restassured.http.ContentType;
|
||||
import io.restassured.response.Response;
|
||||
import io.restassured.specification.RequestSpecification;
|
||||
import io.restassured.specification.ResponseSpecification;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.client.models.PostUsersRequest;
|
||||
import org.apache.fineract.client.models.PostUsersResponse;
|
||||
import org.apache.fineract.client.models.PutGlobalConfigurationsRequest;
|
||||
import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
|
||||
import org.apache.fineract.integrationtests.client.IntegrationTest;
|
||||
import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
|
||||
import org.apache.fineract.integrationtests.common.Utils;
|
||||
import org.apache.fineract.integrationtests.useradministration.users.UserHelper;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class PasswordResetIntegrationTest extends IntegrationTest {
|
||||
|
||||
private RequestSpecification requestSpec;
|
||||
private ResponseSpecification responseSpec;
|
||||
private GlobalConfigurationHelper globalConfigurationHelper;
|
||||
private List<Integer> transientUsers = new ArrayList<>();
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
Utils.initializeRESTAssured();
|
||||
this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
|
||||
this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
|
||||
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
|
||||
this.globalConfigurationHelper = new GlobalConfigurationHelper();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.FORCE_PASSWORD_RESET_ON_FIRST_LOGIN,
|
||||
new PutGlobalConfigurationsRequest().value(0L).enabled(false));
|
||||
|
||||
for (Integer userId : this.transientUsers) {
|
||||
UserHelper.deleteUser(this.requestSpec, this.responseSpec, userId);
|
||||
}
|
||||
this.transientUsers.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasswordResetEnforcement() {
|
||||
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.FORCE_PASSWORD_RESET_ON_FIRST_LOGIN,
|
||||
new PutGlobalConfigurationsRequest().value(0L).enabled(true));
|
||||
|
||||
String password = "Abcdef1#2$3%XYZ";
|
||||
PostUsersRequest userRequest = UserHelper.buildUserRequest(responseSpec, requestSpec, password);
|
||||
PostUsersResponse userResponse = UserHelper.createUser(requestSpec, responseSpec, userRequest);
|
||||
Long userId = userResponse.getResourceId();
|
||||
assertNotNull(userId, "User creation failed to return an ID!");
|
||||
this.transientUsers.add(userId.intValue());
|
||||
String username = userRequest.getUsername();
|
||||
|
||||
Response loginResponse = attemptLogin(username, password);
|
||||
assertEquals(403, loginResponse.getStatusCode(), "User should be forced to change password");
|
||||
|
||||
String newPassword = "Abcdef1#2$3%XYZ_NEW";
|
||||
Response changePasswordResponse = changePassword(username, password, userId, newPassword);
|
||||
assertEquals(200, changePasswordResponse.getStatusCode(), "Password change should succeed");
|
||||
|
||||
loginResponse = attemptLogin(username, newPassword);
|
||||
assertEquals(200, loginResponse.getStatusCode(), "User should be able to login after reset");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFeatureDisabledByDefault() {
|
||||
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.FORCE_PASSWORD_RESET_ON_FIRST_LOGIN,
|
||||
new PutGlobalConfigurationsRequest().value(0L).enabled(false));
|
||||
|
||||
String password = "Abcdef1#2$3%XYZ";
|
||||
PostUsersRequest userRequest = UserHelper.buildUserRequest(responseSpec, requestSpec, password);
|
||||
PostUsersResponse userResponse = UserHelper.createUser(requestSpec, responseSpec, userRequest);
|
||||
assertNotNull(userResponse.getResourceId(), "User creation failed!");
|
||||
this.transientUsers.add(userResponse.getResourceId().intValue());
|
||||
String username = userRequest.getUsername();
|
||||
|
||||
Response loginResponse = attemptLogin(username, password);
|
||||
assertEquals(200, loginResponse.getStatusCode(), "User should login normally when feature is disabled");
|
||||
}
|
||||
|
||||
private Response attemptLogin(String username, String password) {
|
||||
return RestAssured.given().contentType(ContentType.JSON)
|
||||
.body("{\"username\":\"" + username + "\", \"password\":\"" + password + "\"}")
|
||||
.post("/fineract-provider/api/v1/authentication?" + Utils.TENANT_IDENTIFIER);
|
||||
}
|
||||
|
||||
private Response changePassword(String username, String password, Long userId, String newPassword) {
|
||||
String authKey = java.util.Base64.getEncoder().encodeToString((username + ":" + password).getBytes(UTF_8));
|
||||
return RestAssured.given().contentType(ContentType.JSON).header("Authorization", "Basic " + authKey)
|
||||
.header("Fineract-Platform-TenantId", "default")
|
||||
.body("{\"password\":\"" + newPassword + "\", \"repeatPassword\":\"" + newPassword + "\"}")
|
||||
.post("/fineract-provider/api/v1/users/" + userId + "/pwd?" + Utils.TENANT_IDENTIFIER);
|
||||
}
|
||||
}
|
||||
+7
@@ -231,6 +231,13 @@ public class GlobalConfigurationHelper {
|
||||
graceOnPenaltyPostingDefault.put("trapDoor", false);
|
||||
defaults.add(graceOnPenaltyPostingDefault);
|
||||
|
||||
HashMap<String, Object> forcePasswordResetOnFirstLoginDefault = new HashMap<>();
|
||||
forcePasswordResetOnFirstLoginDefault.put("name", GlobalConfigurationConstants.FORCE_PASSWORD_RESET_ON_FIRST_LOGIN);
|
||||
forcePasswordResetOnFirstLoginDefault.put("value", 0L);
|
||||
forcePasswordResetOnFirstLoginDefault.put("enabled", false);
|
||||
forcePasswordResetOnFirstLoginDefault.put("trapDoor", false);
|
||||
defaults.add(forcePasswordResetOnFirstLoginDefault);
|
||||
|
||||
HashMap<String, Object> savingsInterestPostingCurrentPeriodEndDefault = new HashMap<>();
|
||||
savingsInterestPostingCurrentPeriodEndDefault.put("name", GlobalConfigurationConstants.SAVINGS_INTEREST_POSTING_CURRENT_PERIOD_END);
|
||||
savingsInterestPostingCurrentPeriodEndDefault.put("value", 0L);
|
||||
|
||||
Reference in New Issue
Block a user