This is an automated email from the ASF dual-hosted git repository.
ilgrosso pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/syncope.git
The following commit(s) were added to refs/heads/master by this push:
new eb4ee94ee0 [SYNCOPE-1903] Add endpoint to verify the security answer
(#1152)
eb4ee94ee0 is described below
commit eb4ee94ee08c2a2230b14bdd6481ac6c488a2f3d
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..7c88cdcd5e 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
@@ -669,4 +669,18 @@ public class UserLogic extends AbstractAnyLogic<UserTO,
UserCR, UserUR> {
throw new UnresolvedReferenceException();
}
+
+ @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);
+ }
+ }
}
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..d5331e56a4 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 passwordReset() 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");
+ }
+ }
}