Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a8bf37e
Fix AAD issuer and audience validation defaults
rujche May 4, 2026
a88077e
Update change log
rujche May 4, 2026
3d2b4b4
Address review comments: fix import order, centralize isMultiTenantsA…
Copilot May 4, 2026
fc1af23
Revert isMultiTenantsApplication to private: not intended as public API
Copilot May 6, 2026
5261f67
Revert static modifier from isMultiTenantsApplication in AadAuthentic…
Copilot May 6, 2026
50ff6de
Fix grammar and spacing in UserPrincipalManager audience-mismatch err…
Copilot May 6, 2026
8dbba20
Fix critical security vulnerability in AAD Resource Server tenant val…
rujche May 6, 2026
2a273b5
Add explicit tenant ID (tid) claim validation
rujche May 6, 2026
73c3ca4
Simplify tenant ID validation logic and unify error messages
rujche May 6, 2026
01e3e23
Update CHANGELOG for AAD resource server security hardening
rujche May 6, 2026
22ad1b9
Fix test failures by adding tenant-id configuration
rujche May 6, 2026
7c3c599
Fix CHANGELOG wording and redundant tests per review feedback
Copilot May 6, 2026
a338c24
Update changelog
rujche May 6, 2026
940ab78
Potential fix for pull request finding
rujche May 6, 2026
aae84ed
Potential fix for pull request finding
rujche May 6, 2026
5070bfe
Update CHANGELOG to document AadAuthenticationFilter audience validation
rujche May 6, 2026
8e15962
Update comment
rujche May 6, 2026
81eb2f4
Delete comment
rujche May 6, 2026
0d4a4fa
Delete useless empty lines
rujche May 6, 2026
93f7bbb
Potential fix for pull request finding
rujche May 6, 2026
cbaa20b
Potential fix for pull request finding
rujche May 6, 2026
583fe2c
Potential fix for pull request finding
rujche May 6, 2026
72e7f88
Fix test assertions to match actual validateTenantId error message
Copilot May 6, 2026
ec7587c
Trim tenant-id before validation and use in validators to prevent whi…
Copilot May 6, 2026
1174bab
Add test for whitespace-padded reserved tenant-id rejection after tri…
Copilot May 6, 2026
93f18a6
Trim tenant-id in jwtDecoder() before building AadAuthorizationServer…
Copilot May 6, 2026
3c3b72f
Extract getTrimmedTenantId helper to eliminate duplicate trim logic
Copilot May 6, 2026
975e867
Remove unused aadAuthenticationProperties field and stubs from AadJwt…
Copilot May 7, 2026
50a21f8
Potential fix for pull request finding
rujche May 7, 2026
b007f7c
Merge branch 'main' into rujche/main/fix-issue-in-AadJwtIssuerValidat…
rujche May 7, 2026
6822bf7
Normalize tenant ID to lowercase for case-insensitive tid/iss validat…
Copilot May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdk/spring/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This section includes changes in `spring-cloud-azure-autoconfigure` module.
#### Bugs Fixed

- Fixed JDBC/Azure Database and Redis passwordless connection scope defaulting using the wrong `azure.scopes` value for Azure China and Azure US Government when `spring.cloud.azure.profile.cloud-type` is set to `azure_china` or `azure_us_government`. The scopes are now correctly derived from the merged cloud type. ([#47096](https://github.com/Azure/azure-sdk-for-java/issues/47096))
- Hardened AAD token validation defaults in `spring-cloud-azure-autoconfigure`: resource server issuer validation now enforces tenant-aware trusted issuers for single-tenant configurations, and `AadAuthenticationFilter` now enables explicit audience validation by default. [49032](https://github.com/Azure/azure-sdk-for-java/pull/49033)
Comment thread
rujche marked this conversation as resolved.
Outdated

### Spring Cloud Azure Stream Binder Service Bus
This section includes changes in `spring-cloud-azure-stream-binder-servicebus` module.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.azure.spring.cloud.autoconfigure.implementation.aad.configuration.properties.AadResourceServerProperties;
import com.azure.spring.cloud.autoconfigure.implementation.aad.security.constants.AadJwtClaimNames;
import com.azure.spring.cloud.autoconfigure.implementation.aad.security.jwt.AadJwtIssuerValidator;
import com.azure.spring.cloud.autoconfigure.implementation.aad.security.jwt.AadTrustedIssuerRepository;
import com.azure.spring.cloud.autoconfigure.implementation.aad.security.properties.AadAuthorizationServerEndpoints;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
Expand Down Expand Up @@ -64,6 +65,7 @@ JwtDecoder jwtDecoder(AadAuthenticationProperties aadAuthenticationProperties) {
List<OAuth2TokenValidator<Jwt>> createDefaultValidator(AadAuthenticationProperties aadAuthenticationProperties) {
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
List<String> validAudiences = new ArrayList<>();
String tenantId = aadAuthenticationProperties.getProfile().getTenantId();
if (StringUtils.hasText(aadAuthenticationProperties.getAppIdUri())) {
Comment thread
rujche marked this conversation as resolved.
Comment thread
rujche marked this conversation as resolved.
validAudiences.add(aadAuthenticationProperties.getAppIdUri());
}
Expand All @@ -73,11 +75,21 @@ List<OAuth2TokenValidator<Jwt>> createDefaultValidator(AadAuthenticationProperti
if (!validAudiences.isEmpty()) {
validators.add(new JwtClaimValidator<List<String>>(AadJwtClaimNames.AUD, validAudiences::containsAll));
Comment thread
rujche marked this conversation as resolved.
Outdated
}
validators.add(new AadJwtIssuerValidator());
if (isMultiTenantsApplication(tenantId)) {
validators.add(new AadJwtIssuerValidator());
} else {
validators.add(new AadJwtIssuerValidator(new AadTrustedIssuerRepository(tenantId)));
}
validators.add(new JwtTimestampValidator());
Comment thread
rujche marked this conversation as resolved.
return validators;
}

private boolean isMultiTenantsApplication(String tenantId) {
return "common".equalsIgnoreCase(tenantId)
|| "organizations".equalsIgnoreCase(tenantId)
|| "consumers".equalsIgnoreCase(tenantId);
}
Comment thread
rujche marked this conversation as resolved.
Outdated

@EnableWebSecurity
@EnableMethodSecurity
@ConditionalOnDefaultWebSecurity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public AadAuthenticationFilter(AadAuthenticationProperties aadAuthenticationProp
endpoints,
aadAuthenticationProperties,
resourceRetriever,
false
true
),
Comment thread
rujche marked this conversation as resolved.
restTemplateBuilder
);
Expand Down Expand Up @@ -97,7 +97,7 @@ public AadAuthenticationFilter(AadAuthenticationProperties aadAuthenticationProp
endpoints,
aadAuthenticationProperties,
resourceRetriever,
false,
true,
jwkSetCache
),
restTemplateBuilder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import com.azure.identity.extensions.implementation.template.AzureAuthenticationTemplate;
import com.azure.spring.cloud.autoconfigure.implementation.aad.configuration.properties.AadAuthenticationProperties;
import com.azure.spring.cloud.autoconfigure.implementation.aad.security.jwt.AadJwtIssuerValidator;
import com.azure.spring.cloud.autoconfigure.implementation.aad.security.AadResourceServerHttpSecurityConfigurer;
import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration;
import com.nimbusds.jwt.proc.JWTClaimsSetAwareJWSKeySelector;
Expand Down Expand Up @@ -94,6 +95,44 @@ void testExistAudienceDefaultValidator() {
});
Comment thread
rujche marked this conversation as resolved.
}

@Test
void testSingleTenantUsesTrustedIssuerRepository() {
resourceServerContextRunner()
.withPropertyValues("spring.cloud.azure.active-directory.enabled=true")
.run(context -> {
AadAuthenticationProperties properties = context.getBean(AadAuthenticationProperties.class);
AadResourceServerConfiguration bean = context.getBean(AadResourceServerConfiguration.class);
List<OAuth2TokenValidator<Jwt>> defaultValidator = bean.createDefaultValidator(properties);

AadJwtIssuerValidator issuerValidator = (AadJwtIssuerValidator) defaultValidator.stream()
.filter(AadJwtIssuerValidator.class::isInstance)
.findFirst()
.orElseThrow(() -> new IllegalStateException("AadJwtIssuerValidator not found"));

assertThat(ReflectionTestUtils.getField(issuerValidator, "trustedIssuerRepo")).isNotNull();
});
}

@Test
void testMultiTenantUsesPrefixIssuerValidation() {
resourceServerRunner()
.withPropertyValues("spring.cloud.azure.active-directory.enabled=true",
"spring.cloud.azure.active-directory.profile.tenant-id=common",
"spring.cloud.azure.active-directory.app-id-uri=fake-app-id-uri")
.run(context -> {
AadAuthenticationProperties properties = context.getBean(AadAuthenticationProperties.class);
AadResourceServerConfiguration bean = context.getBean(AadResourceServerConfiguration.class);
List<OAuth2TokenValidator<Jwt>> defaultValidator = bean.createDefaultValidator(properties);

AadJwtIssuerValidator issuerValidator = (AadJwtIssuerValidator) defaultValidator.stream()
.filter(AadJwtIssuerValidator.class::isInstance)
.findFirst()
.orElseThrow(() -> new IllegalStateException("AadJwtIssuerValidator not found"));

assertThat(ReflectionTestUtils.getField(issuerValidator, "trustedIssuerRepo")).isNull();
});
}
Comment thread
rujche marked this conversation as resolved.

@Test
void testResourceServerHttpSecurityConfigured() {
resourceServerContextRunner()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
import com.azure.spring.cloud.autoconfigure.implementation.aad.security.properties.AadAuthorizationServerEndpoints;
import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.source.JWKSetCache;
import com.nimbusds.jose.proc.BadJOSEException;
import com.nimbusds.jose.util.ResourceRetriever;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -38,6 +40,7 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.springframework.test.util.ReflectionTestUtils;
Comment thread
rujche marked this conversation as resolved.
Outdated

class AadAuthenticationFilterTests {
private static final String TOKEN = "dummy-token";
Expand Down Expand Up @@ -146,4 +149,63 @@ void testAlreadyAuthenticated() throws ServletException, IOException, ParseExcep
verify(userPrincipalManager, times(0)).buildUserPrincipal(TOKEN);
}

@Test
void testConstructorEnableExplicitAudienceCheck() {
AadAuthenticationProperties properties = mock(AadAuthenticationProperties.class);
AadCredentialProperties credentialProperties = new AadCredentialProperties();
credentialProperties.setClientId("fake-client-id");
credentialProperties.setClientSecret("fake-client-secret");
when(properties.getCredential()).thenReturn(credentialProperties);
AadProfileProperties profileProperties = new AadProfileProperties();
profileProperties.setTenantId("fake-tenant-id");
when(properties.getProfile()).thenReturn(profileProperties);

AadAuthorizationServerEndpoints endpoints = mock(AadAuthorizationServerEndpoints.class);
when(endpoints.getJwkSetEndpoint()).thenReturn("file://dummy");
ResourceRetriever resourceRetriever = url -> null;

AadAuthenticationFilter testFilter = new AadAuthenticationFilter(
properties,
endpoints,
resourceRetriever,
new RestTemplateBuilder()
);

UserPrincipalManager principalManager = (UserPrincipalManager) ReflectionTestUtils.getField(testFilter,
"userPrincipalManager");
assertThat(principalManager).isNotNull();
assertThat(ReflectionTestUtils.getField(principalManager, "explicitAudienceCheck")).isEqualTo(true);
}

@Test
@SuppressWarnings("deprecation")
void testConstructorWithJwkSetCacheEnableExplicitAudienceCheck() {
AadAuthenticationProperties properties = mock(AadAuthenticationProperties.class);
AadCredentialProperties credentialProperties = new AadCredentialProperties();
credentialProperties.setClientId("fake-client-id");
credentialProperties.setClientSecret("fake-client-secret");
when(properties.getCredential()).thenReturn(credentialProperties);
AadProfileProperties profileProperties = new AadProfileProperties();
profileProperties.setTenantId("fake-tenant-id");
when(properties.getProfile()).thenReturn(profileProperties);

AadAuthorizationServerEndpoints endpoints = mock(AadAuthorizationServerEndpoints.class);
when(endpoints.getJwkSetEndpoint()).thenReturn("file://dummy");
ResourceRetriever resourceRetriever = url -> null;
JWKSetCache jwkSetCache = mock(JWKSetCache.class);

AadAuthenticationFilter testFilter = new AadAuthenticationFilter(
properties,
endpoints,
resourceRetriever,
jwkSetCache,
new RestTemplateBuilder()
);

UserPrincipalManager principalManager = (UserPrincipalManager) ReflectionTestUtils.getField(testFilter,
"userPrincipalManager");
assertThat(principalManager).isNotNull();
assertThat(ReflectionTestUtils.getField(principalManager, "explicitAudienceCheck")).isEqualTo(true);
}

}
Loading