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

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


The following commit(s) were added to refs/heads/4_1_X by this push:
     new ea702f5b46 Checking that AccessToken owner matches JWT subject for 
admin
ea702f5b46 is described below

commit ea702f5b461c912ac49db6c741f526c25da100e3
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Mon Aug 24 11:36:10 2026 +0200

    Checking that AccessToken owner matches JWT subject for admin
---
 .../core/spring/security/AuthDataAccessor.java     |  40 +++----
 .../spring/security/SyncopeJWTSSOProvider.java     |   2 +-
 .../core/spring/security/AuthDataAccessorTest.java | 118 +++++++++++++++++++++
 3 files changed, 141 insertions(+), 19 deletions(-)

diff --git 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/AuthDataAccessor.java
 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/AuthDataAccessor.java
index 45ced8e0f8..b87d9e9d7e 100644
--- 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/AuthDataAccessor.java
+++ 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/AuthDataAccessor.java
@@ -60,6 +60,7 @@ import org.apache.syncope.core.persistence.api.dao.RoleDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.AccessToken;
 import org.apache.syncope.core.persistence.api.entity.Delegation;
 import org.apache.syncope.core.persistence.api.entity.ExternalResource;
 import org.apache.syncope.core.persistence.api.entity.Realm;
@@ -185,7 +186,7 @@ public class AuthDataAccessor {
 
         delegation.getRoles().forEach(role -> role.getEntitlements().
                 forEach(entitlement -> populateEntForRealms(
-                entForRealms, entitlement, 
role.getRealms().stream().map(Realm::getFullPath).toList())));
+                        entForRealms, entitlement, 
role.getRealms().stream().map(Realm::getFullPath).toList())));
 
         return buildAuthorities(entForRealms);
     }
@@ -269,7 +270,7 @@ public class AuthDataAccessor {
 
         return jwtSSOProviders.stream().filter(provider -> 
issuer.equals(provider.getIssuer())).findFirst().
                 orElseThrow(() -> new 
AuthenticationCredentialsNotFoundException(
-                "Could not find any registered JWTSSOProvider for issuer " + 
issuer));
+                        "Could not find any registered JWTSSOProvider for 
issuer " + issuer));
     }
 
     protected String getDelegationKey(final SyncopeAuthenticationDetails 
details, final String delegatedKey) {
@@ -289,7 +290,7 @@ public class AuthDataAccessor {
 
         return delegationDAO.findValidFor(delegatingKey, delegatedKey, 
OffsetDateTime.now()).
                 orElseThrow(() -> new SessionAuthenticationException(
-                "Delegation by " + delegatingKey + " was requested but none 
found"));
+                        "Delegation by " + delegatingKey + " was requested but 
none found"));
     }
 
     protected UsernamePasswordAuthResult authenticateAnonymous(final 
Authentication authentication) {
@@ -437,10 +438,10 @@ public class AuthDataAccessor {
             try {
                 Provision provision = 
resource.getProvisionByAnyType(AnyTypeKind.USER.name()).
                         orElseThrow(() -> new AccountNotFoundException(
-                        "Unable to locate provision for user type " + 
AnyTypeKind.USER.name()));
+                                "Unable to locate provision for user type " + 
AnyTypeKind.USER.name()));
                 connObjectKey = mappingManager.getConnObjectKeyValue(user, 
resource, provision).
                         orElseThrow(() -> new AccountNotFoundException(
-                        "Unable to locate conn object key value for " + 
AnyTypeKind.USER.name()));
+                                "Unable to locate conn object key value for " 
+ AnyTypeKind.USER.name()));
                 Uid uid = 
connectorManager.getConnector(resource).authenticate(connObjectKey, password, 
null);
                 if (uid != null) {
                     passwordVerified = true;
@@ -570,7 +571,7 @@ public class AuthDataAccessor {
         // Give role entitlements
         userDAO.findAllRoles(user).forEach(role -> role.getEntitlements().
                 forEach(e -> populateEntForRealms(entForRealms, e, 
role.getRealms().stream().map(Realm::getFullPath).
-                toList())));
+                        toList())));
 
         // Give manager entitlements
         if (userDAO.isManager(user.getKey())) {
@@ -580,15 +581,15 @@ public class AuthDataAccessor {
 
         userDAO.findManagedUsers(user.getKey()).forEach(managedUser -> 
USER_MANAGER_ENTITLEMENTS.
                 forEach(e -> populateEntForRealms(entForRealms, e, Set.of(new 
RealmUtils.ManagerRealm(
-                managedUser.getRealm().getFullPath(),
-                AnyTypeKind.USER,
-                managedUser.getKey()).output()))));
+                        managedUser.getRealm().getFullPath(),
+                        AnyTypeKind.USER,
+                        managedUser.getKey()).output()))));
 
         userDAO.findManagedGroups(user.getKey()).forEach(group -> 
GROUP_MANAGER_ENTITLEMENTS.
                 forEach(e -> populateEntForRealms(entForRealms, e, Set.of(new 
RealmUtils.ManagerRealm(
-                group.getRealm().getFullPath(),
-                AnyTypeKind.GROUP,
-                group.getKey()).output()))));
+                        group.getRealm().getFullPath(),
+                        AnyTypeKind.GROUP,
+                        group.getKey()).output()))));
 
         userDAO.findManagedAnyObjects(user.getKey()).forEach(anyObject -> 
ANYOBJECT_MANAGER_ENTITLEMENTS.
                 apply(anyObject.getType().getKey()).forEach(e -> 
populateEntForRealms(entForRealms, e, Set.of(
@@ -611,7 +612,7 @@ public class AuthDataAccessor {
         } else if (delegationKey != null) {
             Delegation delegation = delegationDAO.findById(delegationKey).
                     orElseThrow(() -> new UsernameNotFoundException(
-                    "Could not find delegation " + delegationKey));
+                            "Could not find delegation " + delegationKey));
 
             authorities = delegation.getRoles().isEmpty()
                     ? getUserAuthorities(delegation.getDelegating())
@@ -619,7 +620,7 @@ public class AuthDataAccessor {
         } else {
             User user = userDAO.findByUsername(username).
                     orElseThrow(() -> new UsernameNotFoundException(
-                    "Could not find any user with username " + username));
+                            "Could not find any user with username " + 
username));
 
             authorities = getUserAuthorities(user);
         }
@@ -633,9 +634,12 @@ public class AuthDataAccessor {
         Set<SyncopeGrantedAuthority> authorities;
 
         if 
(securityProperties.getAdminUser().equals(authentication.getClaims().getSubject()))
 {
-            accessTokenDAO.findById(authentication.getClaims().getJWTID()).
+            AccessToken accessToken = 
accessTokenDAO.findById(authentication.getClaims().getJWTID()).
                     orElseThrow(() -> new 
AuthenticationCredentialsNotFoundException(
-                    "Could not find an Access Token for JWT " + 
authentication.getClaims().getJWTID()));
+                            "Could not find an Access Token for JWT " + 
authentication.getClaims().getJWTID()));
+            if 
(!securityProperties.getAdminUser().equals(accessToken.getOwner())) {
+                throw new AuthenticationCredentialsNotFoundException("Access 
Token owner does not match JWT subject");
+            }
 
             username = securityProperties.getAdminUser();
             authorities = getAdminAuthorities();
@@ -643,8 +647,8 @@ public class AuthDataAccessor {
             JWTSSOProvider jwtSSOProvider = 
getJWTSSOProvider(authentication.getClaims().getIssuer());
             JWTSSOProvider.ResolvedClaims resolved = 
jwtSSOProvider.resolve(authentication.getClaims()).
                     orElseThrow(() -> new 
AuthenticationCredentialsNotFoundException(
-                    "Could not find User " + 
authentication.getClaims().getSubject()
-                    + " for JWT " + authentication.getClaims().getJWTID()));
+                            "Could not find User " + 
authentication.getClaims().getSubject()
+                            + " for JWT " + 
authentication.getClaims().getJWTID()));
 
             User user = resolved.user();
             String delegationKey = 
getDelegationKey(authentication.getDetails(), user.getKey());
diff --git 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/SyncopeJWTSSOProvider.java
 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/SyncopeJWTSSOProvider.java
index 3c882ccb0a..8436a5b25a 100644
--- 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/SyncopeJWTSSOProvider.java
+++ 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/SyncopeJWTSSOProvider.java
@@ -100,7 +100,7 @@ public class SyncopeJWTSSOProvider implements 
JWTSSOProvider {
     public Optional<ResolvedClaims> resolve(final JWTClaimsSet jwtClaims) {
         AccessToken accessToken = 
accessTokenDAO.findById(jwtClaims.getJWTID()).
                 orElseThrow(() -> new 
AuthenticationCredentialsNotFoundException(
-                "Could not find an Access Token for JWT " + 
jwtClaims.getJWTID()));
+                        "Could not find an Access Token for JWT " + 
jwtClaims.getJWTID()));
 
         Set<SyncopeGrantedAuthority> authorities = new HashSet<>();
         if (accessToken.getAuthorities() != null) {
diff --git 
a/core/spring/src/test/java/org/apache/syncope/core/spring/security/AuthDataAccessorTest.java
 
b/core/spring/src/test/java/org/apache/syncope/core/spring/security/AuthDataAccessorTest.java
new file mode 100644
index 0000000000..fe29f907ac
--- /dev/null
+++ 
b/core/spring/src/test/java/org/apache/syncope/core/spring/security/AuthDataAccessorTest.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.spring.security;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import java.lang.reflect.Constructor;
+import java.time.OffsetDateTime;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.types.EntitlementsHolder;
+import org.apache.syncope.common.lib.types.IdRepoEntitlement;
+import org.apache.syncope.core.persistence.api.dao.AccessTokenDAO;
+import org.apache.syncope.core.persistence.api.entity.AccessToken;
+import org.apache.syncope.core.spring.security.jws.AccessTokenJWSSigner;
+import org.apache.syncope.core.spring.security.jws.AccessTokenJWSVerifier;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import 
org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
+
+class AuthDataAccessorTest {
+
+    private static final String LOW_PRIVILEGE_JTI = 
"low-privilege-users-valid-token-id";
+
+    private static final String LOW_PRIVILEGE_USER = "low-priv-user";
+
+    private SecurityProperties securityProperties;
+
+    private AccessTokenDAO accessTokenDAO;
+
+    private AuthDataAccessor authDataAccessor;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        securityProperties = new SecurityProperties();
+        securityProperties.setJwtIssuer("ApacheSyncope");
+        securityProperties.setJwsAlgorithm("HS512");
+        
securityProperties.setJwsKey(SecureRandomUtils.generateRandomLetters(64));
+        securityProperties.setAdminUser("admin");
+
+        EntitlementsHolder.getInstance().addAll(IdRepoEntitlement.values());
+
+        AccessToken lowPrivilegeToken = mock(AccessToken.class);
+        when(lowPrivilegeToken.getOwner()).thenReturn(LOW_PRIVILEGE_USER);
+        accessTokenDAO = mock(AccessTokenDAO.class);
+        
doReturn(Optional.of(lowPrivilegeToken)).when(accessTokenDAO).findById(LOW_PRIVILEGE_JTI);
+
+        Constructor<?> constructor = 
Arrays.stream(AuthDataAccessor.class.getConstructors()).
+                findFirst().orElseThrow();
+        Object[] arguments = 
Arrays.stream(constructor.getParameterTypes()).map(type -> {
+            if (type == SecurityProperties.class) {
+                return securityProperties;
+            }
+            if (type == AccessTokenDAO.class) {
+                return accessTokenDAO;
+            }
+            if (type == List.class) {
+                return List.of();
+            }
+            return mock(type);
+        }).toArray();
+
+        authDataAccessor = (AuthDataAccessor) 
constructor.newInstance(arguments);
+    }
+
+    @Test
+    void lowPrivilegeTokenIdCanBeReboundToAdminWithDefaultSigningKey() throws 
Exception {
+        JWTClaimsSet claims = new JWTClaimsSet.Builder().
+                issuer(securityProperties.getJwtIssuer()).
+                subject(securityProperties.getAdminUser()).
+                jwtID(LOW_PRIVILEGE_JTI).
+                issueTime(new Date()).
+                notBeforeTime(new Date()).
+                
expirationTime(Date.from(OffsetDateTime.now().plusMinutes(10).toInstant())).
+                build();
+        SignedJWT forged = new SignedJWT(new JWSHeader(JWSAlgorithm.HS512), 
claims);
+        forged.sign(new AccessTokenJWSSigner(JWSAlgorithm.HS512, 
securityProperties.getJwsKey()));
+
+        assertTrue(forged.verify(new 
AccessTokenJWSVerifier(JWSAlgorithm.HS512, securityProperties.getJwsKey())));
+
+        JWTAuthentication authentication = new JWTAuthentication(
+                forged.getJWTClaimsSet(),
+                new 
SyncopeAuthenticationDetails(SyncopeConstants.MASTER_DOMAIN, null));
+
+        AuthenticationCredentialsNotFoundException ex = assertThrows(
+                AuthenticationCredentialsNotFoundException.class,
+                () -> authDataAccessor.authenticate(authentication));
+        assertEquals("Access Token owner does not match JWT subject", 
ex.getMessage());
+    }
+}

Reply via email to