You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by il...@apache.org on 2026/08/24 09:46:44 UTC
(syncope) branch master updated: Checking that AccessToken owner matches JWT subject for admin
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 504cdb9e69 Checking that AccessToken owner matches JWT subject for admin
504cdb9e69 is described below
commit 504cdb9e696bb2b3c6ea2ddd8bb3ad6fbc7638df
Author: Francesco Chicchiriccò <il...@apache.org>
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 caf8e7bf75..346db0a30d 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());
+ }
+}