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