This is an automated email from the ASF dual-hosted git repository. ilgrosso pushed a commit to branch 4_0_X in repository https://gitbox.apache.org/repos/asf/syncope.git
commit 58f0e7dd6fee8be4551dea2f55b01e7664a4e74c Author: alberto bogi <[email protected]> AuthorDate: Tue Aug 5 17:13:10 2025 +0200 [SYNCOPE-1903] Add endpoint to verify the security answer (#1152) --- .../common/rest/api/service/UserService.java | 16 +++++++++++++ .../org/apache/syncope/core/logic/UserLogic.java | 14 +++++++++++ .../core/rest/cxf/IdRepoRESTCXFContext.java | 3 ++- .../core/rest/cxf/service/UserServiceImpl.java | 20 +++++++++++++++- .../org/apache/syncope/fit/core/UserITCase.java | 28 ++++++++++++++++++++++ 5 files changed, 79 insertions(+), 2 deletions(-) diff --git a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/UserService.java b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/UserService.java index a8cd07e556..913b17acd9 100644 --- a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/UserService.java +++ b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/UserService.java @@ -34,6 +34,7 @@ import jakarta.ws.rs.PATCH; 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.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; @@ -194,4 +195,19 @@ public interface UserService extends AnyService<UserTO> { @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML }) @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML }) Response status(@NotNull StatusR updateReq); + + /** + * Provides answer for the security question configured for user matching the given username, if any. + * If provided answer matches the one stored for that user, check completes successfully, + * otherwise an error is returned. + * + * @param username username for which the security answer is provided + * @param securityAnswer actual answer text + */ + @ApiResponses( + @ApiResponse(responseCode = "204", description = "Operation was successful")) + @POST + @Path("verifySecurityAnswer") + @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML }) + void verifySecurityAnswer(@NotNull @QueryParam("username") String username, String securityAnswer); } diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java index 720bde173c..ffcbaa991d 100644 --- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java +++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java @@ -638,6 +638,20 @@ public class UserLogic extends AbstractAnyLogic<UserTO, UserCR, UserUR> { return result; } + @PreAuthorize("hasRole('" + IdRepoEntitlement.USER_SEARCH + "')") + @Transactional(readOnly = true) + public void verifySecurityAnswer(final String username, final String securityAnswer) { + User user = userDAO.findByUsername(username). + orElseThrow(() -> new NotFoundException("User " + username)); + + if (syncopeLogic.isPwdResetRequiringSecurityQuestions() + && (securityAnswer == null || !encryptorManager.getInstance(). + verify(securityAnswer, user.getCipherAlgorithm(), user.getSecurityAnswer()))) { + + throw SyncopeClientException.build(ClientExceptionType.InvalidSecurityAnswer); + } + } + @Override protected UserTO resolveReference(final Method method, final Object... args) throws UnresolvedReferenceException { String key = null; diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdRepoRESTCXFContext.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdRepoRESTCXFContext.java index 231a64f2a4..ebe7ed979b 100644 --- a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdRepoRESTCXFContext.java +++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdRepoRESTCXFContext.java @@ -488,8 +488,9 @@ public class IdRepoRESTCXFContext { public UserService userService( final UserDAO userDAO, final UserLogic userLogic, + final SyncopeLogic syncopeLogic, final SearchCondVisitor searchCondVisitor) { - return new UserServiceImpl(searchCondVisitor, userDAO, userLogic); + return new UserServiceImpl(searchCondVisitor, userDAO, userLogic, syncopeLogic); } } diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserServiceImpl.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserServiceImpl.java index 85d3161b8d..1d34a8ab15 100644 --- a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserServiceImpl.java +++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserServiceImpl.java @@ -20,13 +20,16 @@ package org.apache.syncope.core.rest.cxf.service; import jakarta.ws.rs.core.Response; import java.time.OffsetDateTime; +import org.apache.syncope.common.lib.SyncopeClientException; import org.apache.syncope.common.lib.request.StatusR; import org.apache.syncope.common.lib.request.UserCR; import org.apache.syncope.common.lib.request.UserUR; import org.apache.syncope.common.lib.to.ProvisioningResult; import org.apache.syncope.common.lib.to.UserTO; +import org.apache.syncope.common.lib.types.ClientExceptionType; import org.apache.syncope.common.rest.api.service.UserService; import org.apache.syncope.core.logic.AbstractAnyLogic; +import org.apache.syncope.core.logic.SyncopeLogic; import org.apache.syncope.core.logic.UserLogic; import org.apache.syncope.core.persistence.api.dao.AnyDAO; import org.apache.syncope.core.persistence.api.dao.UserDAO; @@ -38,14 +41,18 @@ public class UserServiceImpl extends AbstractAnyService<UserTO, UserCR, UserUR> protected final UserLogic logic; + protected final SyncopeLogic syncopeLogic; + public UserServiceImpl( final SearchCondVisitor searchCondVisitor, final UserDAO userDAO, - final UserLogic logic) { + final UserLogic logic, + final SyncopeLogic syncopeLogic) { super(searchCondVisitor); this.userDAO = userDAO; this.logic = logic; + this.syncopeLogic = syncopeLogic; } @Override @@ -82,4 +89,15 @@ public class UserServiceImpl extends AbstractAnyService<UserTO, UserCR, UserUR> ProvisioningResult<UserTO> updated = logic.status(statusR, isNullPriorityAsync()); return modificationResponse(updated); } + + @Override + public void verifySecurityAnswer(final String username, final String securityAnswer) { + if (!syncopeLogic.isPwdResetAllowed()) { + SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.DelegatedAdministration); + sce.getElements().add("Password reset forbidden by configuration"); + throw sce; + } + + logic.verifySecurityAnswer(username, securityAnswer); + } } diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java index 71f763101f..b5755f32da 100644 --- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java +++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java @@ -1391,4 +1391,32 @@ public class UserITCase extends AbstractITCase { UserTO userTO = createUser(userCR).getEntity(); assertNotNull(userTO.getKey()); } + + @Test + public void verifySecurityAnswer() throws Exception { + // 0. ensure that password request DOES require security question + confParamOps.set(SyncopeConstants.MASTER_DOMAIN, "passwordReset.securityQuestion", true); + + // 1. create an user with security question and answer + UserCR user = UserITCase.getUniqueSample("[email protected]"); + user.setSecurityQuestion("887028ea-66fc-41e7-b397-620d7ea6dfbb"); + user.setSecurityAnswer("Rossi"); + createUser(user); + + // 2. verify wrong security answer + try { + ADMIN_CLIENT.getService(UserService.class).verifySecurityAnswer(user.getUsername(), "WRONG"); + fail("This should not happen"); + } catch (SyncopeClientException e) { + assertEquals(ClientExceptionType.InvalidSecurityAnswer, e.getType()); + } + + // 3. verify the expected security answer + try { + ADMIN_CLIENT.getService(UserService.class).verifySecurityAnswer(user.getUsername(), "Rossi"); + } catch (SyncopeClientException e) { + assertEquals(ClientExceptionType.InvalidSecurityAnswer, e.getType()); + fail("This should not happen"); + } + } }
