FINERACT-2003: Enforce password reset on first login
:wq
This commit is contained in:
+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>
|
||||
Reference in New Issue
Block a user