diff --git a/sdk/spring/CHANGELOG.md b/sdk/spring/CHANGELOG.md index 13c2b3c2b539..43115e10ec39 100644 --- a/sdk/spring/CHANGELOG.md +++ b/sdk/spring/CHANGELOG.md @@ -5,6 +5,11 @@ 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)) + #### 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)) diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadResourceServerConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadResourceServerConfiguration.java index 94b43b0a0a45..d03d7215f997 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadResourceServerConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadResourceServerConfiguration.java @@ -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; @@ -50,8 +51,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)) @@ -64,6 +66,8 @@ JwtDecoder jwtDecoder(AadAuthenticationProperties aadAuthenticationProperties) { List> createDefaultValidator(AadAuthenticationProperties aadAuthenticationProperties) { List> validators = new ArrayList<>(); List validAudiences = new ArrayList<>(); + String tenantId = getTrimmedTenantId(aadAuthenticationProperties); + validateTenantId(tenantId); if (StringUtils.hasText(aadAuthenticationProperties.getAppIdUri())) { validAudiences.add(aadAuthenticationProperties.getAppIdUri()); } @@ -71,13 +75,34 @@ List> createDefaultValidator(AadAuthenticationProperti validAudiences.add(aadAuthenticationProperties.getCredential().getClientId()); } if (!validAudiences.isEmpty()) { - validators.add(new JwtClaimValidator>(AadJwtClaimNames.AUD, validAudiences::containsAll)); + validators.add(new JwtClaimValidator>(AadJwtClaimNames.AUD, + audiences -> audiences != null + && !audiences.isEmpty() + && audiences.stream().anyMatch(validAudiences::contains))); } - validators.add(new AadJwtIssuerValidator()); + validators.add(new JwtClaimValidator(AadJwtClaimNames.TID, tenantId::equals)); + validators.add(new AadJwtIssuerValidator(new AadTrustedIssuerRepository(tenantId))); validators.add(new JwtTimestampValidator()); return validators; } + private static String getTrimmedTenantId(AadAuthenticationProperties aadAuthenticationProperties) { + String tenantId = aadAuthenticationProperties.getProfile().getTenantId(); + return tenantId != null ? tenantId.trim() : 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."); + } + } + @EnableWebSecurity @EnableMethodSecurity @ConditionalOnDefaultWebSecurity diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/filter/AadAuthenticationFilter.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/filter/AadAuthenticationFilter.java index d1d55f186b2f..27de595c9908 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/filter/AadAuthenticationFilter.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/filter/AadAuthenticationFilter.java @@ -69,7 +69,7 @@ public AadAuthenticationFilter(AadAuthenticationProperties aadAuthenticationProp endpoints, aadAuthenticationProperties, resourceRetriever, - false + true ), restTemplateBuilder ); @@ -97,7 +97,7 @@ public AadAuthenticationFilter(AadAuthenticationProperties aadAuthenticationProp endpoints, aadAuthenticationProperties, resourceRetriever, - false, + true, jwkSetCache ), restTemplateBuilder diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/filter/UserPrincipalManager.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/filter/UserPrincipalManager.java index 2517661c84fe..16da143c58eb 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/filter/UserPrincipalManager.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/filter/UserPrincipalManager.java @@ -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."); } } } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/security/jwt/AadJwtIssuerValidator.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/security/jwt/AadJwtIssuerValidator.java index 6aaea487f2e9..c6d7f7602cad 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/security/jwt/AadJwtIssuerValidator.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/security/jwt/AadJwtIssuerValidator.java @@ -18,45 +18,23 @@ */ public class AadJwtIssuerValidator implements OAuth2TokenValidator { - 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 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()); } private Predicate 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); } /** diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadAutoConfigurationServletConditionTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadAutoConfigurationServletConditionTests.java index ff817f148995..525871593932 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadAutoConfigurationServletConditionTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadAutoConfigurationServletConditionTests.java @@ -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)); } @@ -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)); diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadOAuth2ClientConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadOAuth2ClientConfigurationTests.java index a8daec063143..7813ed21249c 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadOAuth2ClientConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadOAuth2ClientConfigurationTests.java @@ -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); diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadResourceServerConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadResourceServerConfigurationTests.java index 53902ba4260e..f02ab9edc315 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadResourceServerConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadResourceServerConfigurationTests.java @@ -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; @@ -68,14 +69,16 @@ void testCreateJwtDecoderByJwkKeySetUri() { @Test void testNotAudienceDefaultValidator() { - resourceServerContextRunner() - .withPropertyValues("spring.cloud.azure.active-directory.enabled=true") + resourceServerRunner() + .withPropertyValues("spring.cloud.azure.active-directory.enabled=true", + "spring.cloud.azure.active-directory.profile.tenant-id=fake-tenant-id") .run(context -> { AadAuthenticationProperties properties = context.getBean(AadAuthenticationProperties.class); AadResourceServerConfiguration bean = context .getBean(AadResourceServerConfiguration.class); List> defaultValidator = bean.createDefaultValidator(properties); assertThat(defaultValidator).isNotNull(); + // No AUD validator (no app-id-uri or client-id configured) + TID + ISS + Timestamp validators assertThat(defaultValidator).hasSize(3); }); } @@ -90,7 +93,134 @@ void testExistAudienceDefaultValidator() { .getBean(AadResourceServerConfiguration.class); List> defaultValidator = bean.createDefaultValidator(properties); assertThat(defaultValidator).isNotNull(); - assertThat(defaultValidator).hasSize(3); + // AUD (from app-id-uri) + TID + ISS + Timestamp validators + assertThat(defaultValidator).hasSize(4); + }); + } + + @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> 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 testValidateTenantIdRejectsCommon() { + 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 -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()) + .hasRootCauseInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("cannot be null, empty, or set to"); + }); + } + + @Test + void testValidateTenantIdRejectsOrganizations() { + resourceServerRunner() + .withPropertyValues("spring.cloud.azure.active-directory.enabled=true", + "spring.cloud.azure.active-directory.profile.tenant-id=organizations", + "spring.cloud.azure.active-directory.app-id-uri=fake-app-id-uri") + .run(context -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()) + .hasRootCauseInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("cannot be null, empty, or set to"); + }); + } + + @Test + void testValidateTenantIdRejectsConsumers() { + resourceServerRunner() + .withPropertyValues("spring.cloud.azure.active-directory.enabled=true", + "spring.cloud.azure.active-directory.profile.tenant-id=consumers", + "spring.cloud.azure.active-directory.app-id-uri=fake-app-id-uri") + .run(context -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()) + .hasRootCauseInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("cannot be null, empty, or set to"); + }); + } + + @Test + void testValidateTenantIdRejectsNull() { + resourceServerRunner() + .withPropertyValues("spring.cloud.azure.active-directory.enabled=true", + "spring.cloud.azure.active-directory.app-id-uri=fake-app-id-uri") + .run(context -> { + // When tenant-id is null, AadAuthenticationProperties.afterPropertiesSet() sets it to "common" + // Then our validation should reject "common" + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()) + .hasRootCauseInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("cannot be null, empty, or set to"); + }); + } + + @Test + void testValidateTenantIdRejectsEmptyString() { + resourceServerRunner() + .withPropertyValues("spring.cloud.azure.active-directory.enabled=true", + "spring.cloud.azure.active-directory.profile.tenant-id=", + "spring.cloud.azure.active-directory.app-id-uri=fake-app-id-uri") + .run(context -> { + // When tenant-id is empty string, AadAuthenticationProperties.afterPropertiesSet() sets it to "common" + // Then our validation should reject "common" + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()) + .hasRootCauseInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("cannot be null, empty, or set to"); + }); + } + + @Test + void testValidateTenantIdRejectsWhitespacePaddedReservedValue() { + 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 -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()) + .hasRootCauseInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("cannot be null, empty, or set to"); + }); + } + + @Test + void testValidateTenantIdAcceptsValidGuid() { + resourceServerRunner() + .withPropertyValues("spring.cloud.azure.active-directory.enabled=true", + "spring.cloud.azure.active-directory.profile.tenant-id=12345678-1234-1234-1234-123456789012", + "spring.cloud.azure.active-directory.app-id-uri=fake-app-id-uri") + .run(context -> { + assertThat(context).hasNotFailed(); + AadAuthenticationProperties properties = context.getBean(AadAuthenticationProperties.class); + AadResourceServerConfiguration bean = context.getBean(AadResourceServerConfiguration.class); + List> 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(); }); } @@ -153,6 +283,7 @@ void useCustomJwtGrantedAuthoritiesConverter() { void useDefaultWebSecurityConfigurerAdapter() { resourceServerRunner() .withPropertyValues("spring.cloud.azure.active-directory.enabled=true", + "spring.cloud.azure.active-directory.profile.tenant-id=fake-tenant-id", "spring.cloud.azure.active-directory.credential.client-id=fake-client-id" ) .run(context -> { @@ -173,6 +304,7 @@ void useCustomSecurityFilterChain() { .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) .withClassLoader(new FilteredClassLoader(AzureAuthenticationTemplate.class, ClientRegistration.class)) .withPropertyValues("spring.cloud.azure.active-directory.enabled=true", + "spring.cloud.azure.active-directory.profile.tenant-id=fake-tenant-id", "spring.cloud.azure.active-directory.credential.client-id=fake-client-id" ) .run(context -> { diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadWebApplicationConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadWebApplicationConfigurationTests.java index 59d66b9ee79d..a28f8a94569d 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadWebApplicationConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadWebApplicationConfigurationTests.java @@ -26,6 +26,7 @@ class AadWebApplicationConfigurationTests { void useDefaultSecurityFilterChain() { webApplicationContextRunner() .withPropertyValues("spring.cloud.azure.active-directory.enabled=true", + "spring.cloud.azure.active-directory.application-type=web_application", "spring.cloud.azure.active-directory.credential.client-id=fake-client-id" ) .run(context -> { @@ -45,6 +46,7 @@ void useCustomSecurityFilterChain() { AadAutoConfiguration.class) .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) .withPropertyValues("spring.cloud.azure.active-directory.enabled=true", + "spring.cloud.azure.active-directory.application-type=web_application", "spring.cloud.azure.active-directory.credential.client-id=fake-client-id" ) .run(context -> { diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/filter/AadAuthenticationFilterTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/filter/AadAuthenticationFilterTests.java index 6a2e629fcf7c..6b2c9cb5ea68 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/filter/AadAuthenticationFilterTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/filter/AadAuthenticationFilterTests.java @@ -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; @@ -26,6 +28,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.util.ReflectionTestUtils; import java.io.IOException; import java.text.ParseException; @@ -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); + } + } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/security/jwt/AadJwtIssuerValidatorTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/security/jwt/AadJwtIssuerValidatorTests.java index 668958075e3e..71062a5f32ae 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/security/jwt/AadJwtIssuerValidatorTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/security/jwt/AadJwtIssuerValidatorTests.java @@ -20,32 +20,6 @@ class AadJwtIssuerValidatorTests { private final AadTrustedIssuerRepository aadTrustedIssuerRepository = new AadTrustedIssuerRepository("fake-tenant" + "-id"); - @Test - void testNoStructureIssuerSuccessVerify() { - AadProfileProperties profile = new AadProfileProperties(); - profile.setTenantId("fake-tenant-id"); - when(aadAuthenticationProperties.getProfile()).thenReturn(profile); - when(jwt.getClaim(AadJwtClaimNames.ISS)).thenReturn("https://sts.windows.net/fake-tenant-id/v2.0"); - - AadJwtIssuerValidator validator = new AadJwtIssuerValidator(); - OAuth2TokenValidatorResult result = validator.validate(jwt); - assertThat(result).isNotNull(); - assertThat(result.getErrors()).isEmpty(); - } - - @Test - void testNoStructureIssuerFailureVerify() { - AadProfileProperties profile = new AadProfileProperties(); - profile.setTenantId("common"); - when(aadAuthenticationProperties.getProfile()).thenReturn(profile); - when(jwt.getClaim(AadJwtClaimNames.ISS)).thenReturn("https://sts.failure.net/fake-tenant-id/v2.0"); - - AadJwtIssuerValidator validator = new AadJwtIssuerValidator(); - OAuth2TokenValidatorResult result = validator.validate(jwt); - assertThat(result).isNotNull(); - assertThat(result.getErrors()).isNotEmpty(); - } - @Test void testIssuerSuccessVerify() { AadProfileProperties profile = new AadProfileProperties();