Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions sdk/spring/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ Upgrade Spring Boot dependencies version to 4.0.6 and Spring Cloud dependencies

This section includes changes in `spring-cloud-azure-autoconfigure` module.

#### Breaking Changes

- AAD resource server now requires `spring.cloud.azure.active-directory.profile.tenant-id` to be set to a specific (non-reserved) tenant ID. Empty string, `common`, `organizations`, and `consumers` are no longer accepted and will cause application startup to fail with an `IllegalArgumentException`. ([#49033](https://github.com/Azure/azure-sdk-for-java/pull/49033))
- `AadAuthenticationFilter` now enables explicit audience validation by default. The filter will verify that the JWT's `aud` (audience) claim matches either `spring.cloud.azure.active-directory.credential.client-id` or `spring.cloud.azure.active-directory.app-id-uri`. Tokens issued for other applications will be rejected with `BadJWTException`. This prevents cross-application token reuse and aligns with OAuth2/OIDC security best practices. ([#49033](https://github.com/Azure/azure-sdk-for-java/pull/49033))
Comment thread
rujche marked this conversation as resolved.

#### 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))
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 All @@ -33,6 +34,7 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import static com.azure.spring.cloud.autoconfigure.implementation.aad.security.AadResourceServerHttpSecurityConfigurer.aadResourceServer;
import static com.azure.spring.cloud.autoconfigure.implementation.aad.utils.AadRestTemplateCreator.createRestTemplate;
Expand All @@ -50,8 +52,9 @@ class AadResourceServerConfiguration {
@Bean
@ConditionalOnMissingBean(JwtDecoder.class)
JwtDecoder jwtDecoder(AadAuthenticationProperties aadAuthenticationProperties) {
String tenantId = getTrimmedTenantId(aadAuthenticationProperties);
AadAuthorizationServerEndpoints identityEndpoints = new AadAuthorizationServerEndpoints(
aadAuthenticationProperties.getProfile().getEnvironment().getActiveDirectoryEndpoint(), aadAuthenticationProperties.getProfile().getTenantId());
aadAuthenticationProperties.getProfile().getEnvironment().getActiveDirectoryEndpoint(), tenantId);
NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder
.withJwkSetUri(identityEndpoints.getJwkSetEndpoint())
.restOperations(createRestTemplate(restTemplateBuilder))
Expand All @@ -64,20 +67,50 @@ JwtDecoder jwtDecoder(AadAuthenticationProperties aadAuthenticationProperties) {
List<OAuth2TokenValidator<Jwt>> createDefaultValidator(AadAuthenticationProperties aadAuthenticationProperties) {
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
List<String> validAudiences = new ArrayList<>();
String tenantId = getTrimmedTenantId(aadAuthenticationProperties);
validateTenantId(tenantId);
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());
}
if (StringUtils.hasText(aadAuthenticationProperties.getCredential().getClientId())) {
validAudiences.add(aadAuthenticationProperties.getCredential().getClientId());
}
if (!validAudiences.isEmpty()) {
validators.add(new JwtClaimValidator<List<String>>(AadJwtClaimNames.AUD, validAudiences::containsAll));
validators.add(new JwtClaimValidator<List<String>>(AadJwtClaimNames.AUD,
audiences -> audiences != null
&& !audiences.isEmpty()
&& audiences.stream().anyMatch(validAudiences::contains)));
}
validators.add(new AadJwtIssuerValidator());
validators.add(new JwtClaimValidator<String>(AadJwtClaimNames.TID, tenantId::equals));
validators.add(new AadJwtIssuerValidator(new AadTrustedIssuerRepository(tenantId)));
validators.add(new JwtTimestampValidator());
Comment thread
rujche marked this conversation as resolved.
return validators;
}

private static String getTrimmedTenantId(AadAuthenticationProperties aadAuthenticationProperties) {
String tenantId = aadAuthenticationProperties.getProfile().getTenantId();
return tenantId != null ? tenantId.trim().toLowerCase(Locale.ROOT) : null;
}

private static void validateTenantId(String tenantId) {
if (!StringUtils.hasText(tenantId)
|| "common".equalsIgnoreCase(tenantId)
|| "organizations".equalsIgnoreCase(tenantId)
|| "consumers".equalsIgnoreCase(tenantId)) {
throw new IllegalArgumentException(
"For resource server, "
+ "'spring.cloud.azure.active-directory.profile.tenant-id' "
+ "cannot be null, empty, or set to 'common', "
+ "'organizations', or 'consumers'. "
+ "These values are not supported for resource server token "
+ "validation because a specific tenant ID is required to "
+ "validate the token 'tid' claim and issuer against a "
+ "single Azure AD tenant. "
+ "Please configure an explicit tenant ID for your "
+ "organization's tenant.");
}
Comment thread
rujche marked this conversation as resolved.
}

@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
),
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 @@ -220,7 +220,7 @@ public void verify(JWTClaimsSet claimsSet, SecurityContext ctx) throws BadJWTExc
LOGGER.debug("Matched audience: [{}]", matchedAudience.get());
} else {
throw new BadJWTException("Invalid token audience. Provided value " + claimsSet.getAudience()
+ "does not match neither client-id nor AppIdUri.");
+ " does not match either the client-id or AppIdUri.");
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,45 +18,23 @@
*/
public class AadJwtIssuerValidator implements OAuth2TokenValidator<Jwt> {

private static final String LOGIN_MICROSOFT_ONLINE_ISSUER = "https://login.microsoftonline.com/";

private static final String STS_WINDOWS_ISSUER = "https://sts.windows.net/";

private static final String STS_CHINA_CLOUD_API_ISSUER = "https://sts.chinacloudapi.cn/";

private final JwtClaimValidator<String> validator;

private final AadTrustedIssuerRepository trustedIssuerRepo;

/**
* Constructs a {@link AadJwtIssuerValidator} using the provided parameters
*/
public AadJwtIssuerValidator() {
this(null);
}

/**
* Constructs a {@link AadJwtIssuerValidator} using the provided parameters
*
* @param aadTrustedIssuerRepository trusted issuer repository.
*/
public AadJwtIssuerValidator(AadTrustedIssuerRepository aadTrustedIssuerRepository) {
Assert.notNull(aadTrustedIssuerRepository, "aadTrustedIssuerRepository cannot be null");
this.trustedIssuerRepo = aadTrustedIssuerRepository;
this.validator = new JwtClaimValidator<>(AadJwtClaimNames.ISS, trustedIssuerRepoValidIssuer());
}
Comment thread
rujche marked this conversation as resolved.

private Predicate<String> trustedIssuerRepoValidIssuer() {
return iss -> {
if (iss == null) {
return false;
}
if (trustedIssuerRepo == null) {
return iss.startsWith(LOGIN_MICROSOFT_ONLINE_ISSUER)
|| iss.startsWith(STS_WINDOWS_ISSUER)
|| iss.startsWith(STS_CHINA_CLOUD_API_ISSUER);
}
return trustedIssuerRepo.getTrustedIssuers().contains(iss);
};
return iss -> iss != null && trustedIssuerRepo.getTrustedIssuers().contains(iss);
Comment thread
rujche marked this conversation as resolved.
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ void servletApplication() {
oauthClientAndResourceServerRunner()
.withPropertyValues(
"spring.cloud.azure.active-directory.enabled=true",
"spring.cloud.azure.active-directory.credential.client-id=fake-client-id"
"spring.cloud.azure.active-directory.credential.client-id=fake-client-id",
"spring.cloud.azure.active-directory.profile.tenant-id=fake-tenant-id"
)
.run(context -> assertThat(context).hasSingleBean(AadAuthenticationProperties.class));
}
Expand All @@ -29,7 +30,8 @@ void nonServletApplication() {
oauthClientAndResourceServerRunner()
.withPropertyValues(
"spring.cloud.azure.active-directory.enabled=true",
"spring.cloud.azure.active-directory.credential.client-id=fake-client-id"
"spring.cloud.azure.active-directory.credential.client-id=fake-client-id",
"spring.cloud.azure.active-directory.profile.tenant-id=fake-tenant-id"
)
.withClassLoader(new FilteredClassLoader(SERVLET_WEB_APPLICATION_CLASS))
.run(context -> assertThat(context).doesNotHaveBean(AadAuthenticationProperties.class));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ void testWithRequiredPropertiesSet() {
oauthClientAndResourceServerRunner()
.withPropertyValues(
"spring.cloud.azure.active-directory.enabled=true",
"spring.cloud.azure.active-directory.credential.client-id=fake-client-id"
"spring.cloud.azure.active-directory.credential.client-id=fake-client-id",
"spring.cloud.azure.active-directory.profile.tenant-id=fake-tenant-id"
)
.run(context -> {
assertThat(context).hasSingleBean(AadAuthenticationProperties.class);
Expand Down
Loading
Loading