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");
+        }
+    }
 }

Reply via email to