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()); } } }
