This is an automated email from the ASF dual-hosted git repository.

massx1 pushed a commit to branch 4_1_X
in repository https://gitbox.apache.org/repos/asf/syncope.git

commit 537c607d4e88726d7fe338e6ab3408238ca4d568
Author: Massimiliano Perrone <[email protected]>
AuthorDate: Thu Jun 4 06:34:15 2026 +0200

    Make authentication error detail disclosure configurable
---
 .../core/spring/security/SecurityProperties.java   | 31 ++++++++++++++++
 .../SyncopeBasicAuthenticationEntryPoint.java      | 15 ++++++--
 .../core/spring/security/WebSecurityContext.java   |  5 +--
 .../SyncopeBasicAuthenticationEntryPointTest.java  | 41 +++++++++++++++++++---
 core/starter/src/main/resources/core.properties    |  3 ++
 .../syncope/fit/core/AuthenticationITCase.java     | 22 ++++++++++++
 .../org/apache/syncope/fit/core/JWTITCase.java     |  3 +-
 .../syncope/fit/core/MultitenancyITCase.java       |  3 +-
 8 files changed, 112 insertions(+), 11 deletions(-)

diff --git 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityProperties.java
 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityProperties.java
index fce444315f..296d870ebc 100644
--- 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityProperties.java
+++ 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityProperties.java
@@ -68,6 +68,31 @@ public class SecurityProperties {
         }
     }
 
+    public static class AuthenticationErrorProperties {
+
+        public static final String DEFAULT_GENERIC_MESSAGE = "Authentication 
failed";
+
+        private boolean exposeDetails = false;
+
+        private String genericMessage = DEFAULT_GENERIC_MESSAGE;
+
+        public boolean isExposeDetails() {
+            return exposeDetails;
+        }
+
+        public void setExposeDetails(final boolean exposeDetails) {
+            this.exposeDetails = exposeDetails;
+        }
+
+        public String getGenericMessage() {
+            return genericMessage;
+        }
+
+        public void setGenericMessage(final String genericMessage) {
+            this.genericMessage = genericMessage;
+        }
+    }
+
     public static class DigesterProperties {
 
         private int saltIterations = 1;
@@ -149,6 +174,8 @@ public class SecurityProperties {
 
     private final AuthenticationThrottleProperties authenticationThrottle = 
new AuthenticationThrottleProperties();
 
+    private final AuthenticationErrorProperties authenticationError = new 
AuthenticationErrorProperties();
+
     private final DigesterProperties digester = new DigesterProperties();
 
     public String getAdminUser() {
@@ -243,6 +270,10 @@ public class SecurityProperties {
         return authenticationThrottle;
     }
 
+    public AuthenticationErrorProperties getAuthenticationError() {
+        return authenticationError;
+    }
+
     public DigesterProperties getDigester() {
         return digester;
     }
diff --git 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/SyncopeBasicAuthenticationEntryPoint.java
 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/SyncopeBasicAuthenticationEntryPoint.java
index 1f03bf4479..9693780dbd 100644
--- 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/SyncopeBasicAuthenticationEntryPoint.java
+++ 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/SyncopeBasicAuthenticationEntryPoint.java
@@ -21,6 +21,7 @@ package org.apache.syncope.core.spring.security;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import java.io.IOException;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.rest.api.RESTHeaders;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpStatus;
@@ -32,13 +33,23 @@ import 
org.springframework.security.web.authentication.www.BasicAuthenticationEn
  */
 public class SyncopeBasicAuthenticationEntryPoint extends 
BasicAuthenticationEntryPoint {
 
-    public static final String AUTHENTICATION_FAILED = "Authentication failed";
+    private final SecurityProperties securityProperties;
+
+    public SyncopeBasicAuthenticationEntryPoint(final SecurityProperties 
securityProperties) {
+        this.securityProperties = securityProperties;
+    }
 
     @Override
     public void commence(final HttpServletRequest request, final 
HttpServletResponse response,
             final AuthenticationException authException) throws IOException {
 
-        response.addHeader(RESTHeaders.ERROR_INFO, AUTHENTICATION_FAILED);
+        response.addHeader(
+                RESTHeaders.ERROR_INFO,
+                securityProperties.getAuthenticationError().isExposeDetails()
+                ? authException.getMessage()
+                : StringUtils.defaultIfBlank(
+                        
securityProperties.getAuthenticationError().getGenericMessage(),
+                        
SecurityProperties.AuthenticationErrorProperties.DEFAULT_GENERIC_MESSAGE));
         if (authException instanceof RateLimitAuthenticationException 
rateLimit) {
             response.addHeader(HttpHeaders.RETRY_AFTER, 
Long.toString(rateLimit.getRetryAfterSeconds()));
             response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(), 
authException.getMessage());
diff --git 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/WebSecurityContext.java
 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/WebSecurityContext.java
index bb948d0961..c99fd6cc83 100644
--- 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/WebSecurityContext.java
+++ 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/WebSecurityContext.java
@@ -84,7 +84,8 @@ public class WebSecurityContext {
             final UsernamePasswordAuthenticationProvider 
usernamePasswordAuthenticationProvider,
             final AccessDeniedHandler accessDeniedHandler,
             final AuthDataAccessor dataAccessor,
-            final DefaultCredentialChecker defaultCredentialChecker) throws 
Exception {
+            final DefaultCredentialChecker defaultCredentialChecker,
+            final SecurityProperties securityProperties) throws Exception {
 
         AuthenticationManager authenticationManager = 
http.getSharedObject(AuthenticationManagerBuilder.class).
                 parentAuthenticationManager(null).
@@ -96,7 +97,7 @@ public class WebSecurityContext {
                 new SyncopeAuthenticationDetailsSource();
 
         SyncopeBasicAuthenticationEntryPoint basicAuthenticationEntryPoint =
-                new SyncopeBasicAuthenticationEntryPoint();
+                new SyncopeBasicAuthenticationEntryPoint(securityProperties);
         basicAuthenticationEntryPoint.setRealmName("Apache Syncope 
authentication");
         http.httpBasic(customizer -> customizer.
                 authenticationEntryPoint(basicAuthenticationEntryPoint).
diff --git 
a/core/spring/src/test/java/org/apache/syncope/core/spring/security/SyncopeBasicAuthenticationEntryPointTest.java
 
b/core/spring/src/test/java/org/apache/syncope/core/spring/security/SyncopeBasicAuthenticationEntryPointTest.java
index 86ce543ecb..191b6b0028 100644
--- 
a/core/spring/src/test/java/org/apache/syncope/core/spring/security/SyncopeBasicAuthenticationEntryPointTest.java
+++ 
b/core/spring/src/test/java/org/apache/syncope/core/spring/security/SyncopeBasicAuthenticationEntryPointTest.java
@@ -35,9 +35,12 @@ class SyncopeBasicAuthenticationEntryPointTest {
 
     private SyncopeBasicAuthenticationEntryPoint entryPoint;
 
+    private SecurityProperties securityProperties;
+
     @BeforeEach
     public void setUp() throws Exception {
-        entryPoint = new SyncopeBasicAuthenticationEntryPoint();
+        securityProperties = new SecurityProperties();
+        entryPoint = new 
SyncopeBasicAuthenticationEntryPoint(securityProperties);
         entryPoint.setRealmName("Apache Syncope authentication");
         entryPoint.afterPropertiesSet();
     }
@@ -53,7 +56,7 @@ class SyncopeBasicAuthenticationEntryPointTest {
 
         assertEquals(HttpStatus.TOO_MANY_REQUESTS.value(), 
response.getStatus());
         assertEquals("30", response.getHeader(HttpHeaders.RETRY_AFTER));
-        
assertEquals(SyncopeBasicAuthenticationEntryPoint.AUTHENTICATION_FAILED,
+        
assertEquals(SecurityProperties.AuthenticationErrorProperties.DEFAULT_GENERIC_MESSAGE,
                 response.getHeader(RESTHeaders.ERROR_INFO));
     }
 
@@ -67,7 +70,7 @@ class SyncopeBasicAuthenticationEntryPointTest {
                 new BadCredentialsException("rossini: invalid password 
provided"));
 
         assertEquals(HttpStatus.UNAUTHORIZED.value(), response.getStatus());
-        
assertEquals(SyncopeBasicAuthenticationEntryPoint.AUTHENTICATION_FAILED,
+        
assertEquals(SecurityProperties.AuthenticationErrorProperties.DEFAULT_GENERIC_MESSAGE,
                 response.getHeader(RESTHeaders.ERROR_INFO));
     }
 
@@ -81,7 +84,7 @@ class SyncopeBasicAuthenticationEntryPointTest {
                 new UsernameNotFoundException("not-a-user"));
 
         assertEquals(HttpStatus.UNAUTHORIZED.value(), response.getStatus());
-        
assertEquals(SyncopeBasicAuthenticationEntryPoint.AUTHENTICATION_FAILED,
+        
assertEquals(SecurityProperties.AuthenticationErrorProperties.DEFAULT_GENERIC_MESSAGE,
                 response.getHeader(RESTHeaders.ERROR_INFO));
     }
 
@@ -95,7 +98,35 @@ class SyncopeBasicAuthenticationEntryPointTest {
                 new DisabledException("User rossini is suspended"));
 
         assertEquals(HttpStatus.UNAUTHORIZED.value(), response.getStatus());
-        
assertEquals(SyncopeBasicAuthenticationEntryPoint.AUTHENTICATION_FAILED,
+        
assertEquals(SecurityProperties.AuthenticationErrorProperties.DEFAULT_GENERIC_MESSAGE,
                 response.getHeader(RESTHeaders.ERROR_INFO));
     }
+
+    @Test
+    public void genericErrorInfoCanBeConfigured() throws Exception {
+        securityProperties.getAuthenticationError().setGenericMessage("Login 
failed");
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        entryPoint.commence(
+                new MockHttpServletRequest(),
+                response,
+                new BadCredentialsException("rossini: invalid password 
provided"));
+
+        assertEquals(HttpStatus.UNAUTHORIZED.value(), response.getStatus());
+        assertEquals("Login failed", 
response.getHeader(RESTHeaders.ERROR_INFO));
+    }
+
+    @Test
+    public void detailsCanBeExposedWhenConfigured() throws Exception {
+        securityProperties.getAuthenticationError().setExposeDetails(true);
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        entryPoint.commence(
+                new MockHttpServletRequest(),
+                response,
+                new BadCredentialsException("rossini: invalid password 
provided"));
+
+        assertEquals(HttpStatus.UNAUTHORIZED.value(), response.getStatus());
+        assertEquals("rossini: invalid password provided", 
response.getHeader(RESTHeaders.ERROR_INFO));
+    }
 }
diff --git a/core/starter/src/main/resources/core.properties 
b/core/starter/src/main/resources/core.properties
index 54a5bef4cf..05d6cc30ed 100644
--- a/core/starter/src/main/resources/core.properties
+++ b/core/starter/src/main/resources/core.properties
@@ -107,6 +107,9 @@ security.authenticationThrottle.maxAttempts=5
 security.authenticationThrottle.windowSeconds=60
 security.authenticationThrottle.lockSeconds=60
 
+security.authenticationError.exposeDetails=false
+security.authenticationError.genericMessage=Authentication failed
+
 # default for LDAP / RFC2307 SSHA
 security.digester.saltIterations=1
 security.digester.saltSizeBytes=8
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuthenticationITCase.java
 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuthenticationITCase.java
index 90f0bff11a..8b5bf35676 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuthenticationITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuthenticationITCase.java
@@ -71,6 +71,7 @@ import 
org.apache.syncope.common.rest.api.beans.UserRequestQuery;
 import org.apache.syncope.common.rest.api.service.AnyObjectService;
 import org.apache.syncope.common.rest.api.service.SchemaService;
 import org.apache.syncope.common.rest.api.service.UserService;
+import org.apache.syncope.core.spring.security.SecurityProperties;
 import org.apache.syncope.fit.AbstractITCase;
 import org.junit.jupiter.api.Test;
 import org.springframework.jdbc.core.JdbcTemplate;
@@ -330,6 +331,27 @@ public class AuthenticationITCase extends AbstractITCase {
         assertEquals(0, userService4.read(userKey).getFailedLogins());
     }
 
+    @Test
+    public void authenticationFailureErrorInfoIsGeneric() {
+        NotAuthorizedException e = assertThrows(
+                NotAuthorizedException.class,
+                () -> CLIENT_FACTORY.create(getUUIDString(), 
"anypassword").self());
+        
assertEquals(SecurityProperties.AuthenticationErrorProperties.DEFAULT_GENERIC_MESSAGE,
+                e.getResponse().getHeaderString(RESTHeaders.ERROR_INFO));
+
+        UserCR userCR = 
UserITCase.getUniqueSample("[email protected]");
+        userCR.getRoles().add("User manager");
+
+        UserTO userTO = createUser(userCR).getEntity();
+        assertNotNull(userTO);
+
+        e = assertThrows(
+                NotAuthorizedException.class,
+                () -> CLIENT_FACTORY.create(userTO.getUsername(), "wrongpwd"));
+        
assertEquals(SecurityProperties.AuthenticationErrorProperties.DEFAULT_GENERIC_MESSAGE,
+                e.getResponse().getHeaderString(RESTHeaders.ERROR_INFO));
+    }
+
     @Test
     public void checkUserSuspension() {
         UserCR userCR = 
UserITCase.getUniqueSample("[email protected]");
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/JWTITCase.java 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/JWTITCase.java
index c1f626b9b9..b77027f636 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/JWTITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/JWTITCase.java
@@ -54,6 +54,7 @@ import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.rest.api.RESTHeaders;
 import org.apache.syncope.common.rest.api.service.AccessTokenService;
 import org.apache.syncope.common.rest.api.service.UserSelfService;
+import org.apache.syncope.core.spring.security.SecurityProperties;
 import org.apache.syncope.core.spring.security.jws.AccessTokenJWSSigner;
 import org.apache.syncope.core.spring.security.jws.AccessTokenJWSVerifier;
 import org.apache.syncope.fit.AbstractITCase;
@@ -142,7 +143,7 @@ public class JWTITCase extends AbstractITCase {
             jwtUserSelfService.read();
             fail("Failure expected on a modified token");
         } catch (NotAuthorizedException e) {
-            assertEquals("Invalid signature found in JWT", e.getMessage());
+            
assertEquals(SecurityProperties.AuthenticationErrorProperties.DEFAULT_GENERIC_MESSAGE,
 e.getMessage());
         }
     }
 
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MultitenancyITCase.java
 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MultitenancyITCase.java
index 6f596a1f3f..d65e40846a 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MultitenancyITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MultitenancyITCase.java
@@ -66,6 +66,7 @@ import 
org.apache.syncope.common.rest.api.service.SchemaService;
 import org.apache.syncope.common.rest.api.service.TaskService;
 import org.apache.syncope.common.rest.api.service.UserSelfService;
 import org.apache.syncope.common.rest.api.service.UserService;
+import org.apache.syncope.core.spring.security.SecurityProperties;
 import org.apache.syncope.fit.AbstractITCase;
 import org.identityconnectors.framework.common.objects.ObjectClass;
 import org.junit.jupiter.api.BeforeEach;
@@ -243,7 +244,7 @@ public class MultitenancyITCase extends AbstractITCase {
                     
create(UserITCase.getUniqueSample("[email protected]"));
             fail("This should not happen");
         } catch (NotAuthorizedException e) {
-            assertTrue(e.getMessage().contains("Could not find domain 
NotExisting"));
+            
assertEquals(SecurityProperties.AuthenticationErrorProperties.DEFAULT_GENERIC_MESSAGE,
 e.getMessage());
         }
     }
 }

Reply via email to