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