From a4217577d7d929fe4cb0e254d9baf0dde9c8af46 Mon Sep 17 00:00:00 2001 From: Joe Mahady Date: Wed, 29 Jul 2026 11:01:54 +0100 Subject: [PATCH] Implement fix for TNZGOV-14861 reject ineffective jwt+rotate+!revocable combination ai-assisted=yes Co-authored-by: Cursor --- .../beans/OauthEndpointBeanConfiguration.java | 5 ++++ ...ralIdentityZoneConfigurationValidator.java | 5 ++++ ...entityZoneConfigurationValidatorTests.java | 30 +++++++++++++++++++ .../uaa/oauth/UaaTokenServicesTests.java | 2 +- 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/OauthEndpointBeanConfiguration.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/OauthEndpointBeanConfiguration.java index 614d70d30e3..ce81246415e 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/OauthEndpointBeanConfiguration.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/OauthEndpointBeanConfiguration.java @@ -697,6 +697,11 @@ TokenPolicy uaaTokenPolicy( bean.setMaxSessionLimit(TokenPolicy.parseRefreshTokenUnique(refreshTokenUniqueStr)); bean.setRefreshTokenRotate(refreshTokenRotate); + + if (refreshTokenRotate && "jwt".equalsIgnoreCase(refreshTokenFormat) && !jwtRevocable) { + throw new IllegalArgumentException("A token policy cannot have JWT-format refresh tokens with rotation enabled unless they are also revocable."); + } + return bean; } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/zone/GeneralIdentityZoneConfigurationValidator.java b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/GeneralIdentityZoneConfigurationValidator.java index 4c3e374b5a0..9f9793e0978 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/zone/GeneralIdentityZoneConfigurationValidator.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/GeneralIdentityZoneConfigurationValidator.java @@ -49,6 +49,11 @@ public IdentityZoneConfiguration validate(IdentityZone zone, IdentityZoneValidat TokenPolicy tokenPolicy = config.getTokenPolicy(); if (tokenPolicy != null) { + if (tokenPolicy.isRefreshTokenRotate() && + "jwt".equalsIgnoreCase(tokenPolicy.getRefreshTokenFormat()) && + !tokenPolicy.isJwtRevocable()) { + throw new InvalidIdentityZoneConfigurationException("A token policy cannot have JWT-format refresh tokens with rotation enabled unless they are also revocable."); + } String activeKeyId = tokenPolicy.getActiveKeyId(); if (StringUtils.hasText(activeKeyId)) { Map jwtKeys = tokenPolicy.getKeys(); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/zone/GeneralIdentityZoneConfigurationValidatorTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/zone/GeneralIdentityZoneConfigurationValidatorTests.java index f6123b92b2d..01e8cd0138a 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/zone/GeneralIdentityZoneConfigurationValidatorTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/zone/GeneralIdentityZoneConfigurationValidatorTests.java @@ -297,6 +297,36 @@ void validate_without_legacy_key_and_null_active_key(IdentityZoneValidator.Mode .hasMessageContaining("Invalid SAML active key ID: 'null'. Couldn't find any matching keys."); } + @ParameterizedTest + @MethodSource("parameters") + void validate_refreshTokenRotate_with_jwt_and_not_revocable(IdentityZoneValidator.Mode mode) { + IdentityZone testZone = new IdentityZone(); + testZone.setId(IdentityZone.getUaaZoneId()); + TokenPolicy tokenPolicy = new TokenPolicy(); + tokenPolicy.setRefreshTokenRotate(true); + tokenPolicy.setRefreshTokenFormat("jwt"); + tokenPolicy.setJwtRevocable(false); + testZone.getConfig().setTokenPolicy(tokenPolicy); + + assertThatThrownBy(() -> validator.validate(testZone, mode)) + .isInstanceOf(InvalidIdentityZoneConfigurationException.class) + .hasMessage("A token policy cannot have JWT-format refresh tokens with rotation enabled unless they are also revocable."); + } + + @ParameterizedTest + @MethodSource("parameters") + void validate_refreshTokenRotate_with_jwt_and_revocable(IdentityZoneValidator.Mode mode) throws Exception { + IdentityZone testZone = new IdentityZone(); + testZone.setId(IdentityZone.getUaaZoneId()); + TokenPolicy tokenPolicy = new TokenPolicy(); + tokenPolicy.setRefreshTokenRotate(true); + tokenPolicy.setRefreshTokenFormat("jwt"); + tokenPolicy.setJwtRevocable(true); + testZone.getConfig().setTokenPolicy(tokenPolicy); + + validator.validate(testZone, mode); + } + @MethodSource("parameters") @ParameterizedTest void validate_no_keys(IdentityZoneValidator.Mode mode) throws Exception { diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServicesTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServicesTests.java index 0aba9b20527..8ed1fa40375 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServicesTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServicesTests.java @@ -449,7 +449,7 @@ void ensureJKUHeaderIsSetWhenBuildingARefreshToken() { @Nested @DisplayName("when performing the refresh grant type") @DefaultTestContext - @TestPropertySource(properties = {"uaa.url=https://uaa.some.test.domain.com:555/uaa", "jwt.token.refresh.rotate=true"}) + @TestPropertySource(properties = {"uaa.url=https://uaa.some.test.domain.com:555/uaa", "jwt.token.refresh.rotate=true", "jwt.token.revocable=true"}) @DirtiesContext class WhenRefreshGrant { @Autowired