diff --git a/eng/code-quality-reports/src/main/resources/revapi/revapi.json b/eng/code-quality-reports/src/main/resources/revapi/revapi.json index 3f88a7dd5aaf4..2103b66568fc7 100644 --- a/eng/code-quality-reports/src/main/resources/revapi/revapi.json +++ b/eng/code-quality-reports/src/main/resources/revapi/revapi.json @@ -254,6 +254,51 @@ "old": "method org.springframework.boot.autoconfigure.kafka.KafkaProperties com.azure.spring.cloud.autoconfigure.eventhubs.kafka.AzureEventHubsKafkaAutoConfiguration::azureKafkaProperties(com.azure.spring.cloud.core.provider.connectionstring.ServiceConnectionStringProvider)", "justification": "To move kafka properties customization to bean post processor." }, + { + "code": "java.annotation.attributeValueChanged", + "new": "class com.azure.spring.cloud.autoconfigure.aad.AadAutoConfiguration", + "justification": "The use of AadOboOAuth2AuthorizedClientProvider is not recommended, and there is no need to retain consideration of the different situations brought by this OBO provider." + }, + { + "code": "java.annotation.added", + "new": "class com.azure.spring.cloud.autoconfigure.aad.configuration.AadOAuth2ClientConfiguration", + "justification": "The use of AadOboOAuth2AuthorizedClientProvider is not recommended, and there is no need to retain consideration of the different situations brought by this OBO provider." + }, + { + "regex": true, + "code": "java.class.removed", + "old": "class com\\.azure\\.spring\\.cloud\\.autoconfigure\\.aad\\.configuration\\.AadOAuth2ClientConfiguration\\..*", + "justification": "The use of AadOboOAuth2AuthorizedClientProvider is not recommended, and there is no need to retain consideration of the different situations brought by this OBO provider." + }, + { + "code": "java.method.returnTypeChanged", + "old:": "method com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType com.azure.spring.cloud.autoconfigure.aad.properties.AuthorizationClientProperties::getAuthorizationGrantType()", + "new": "method org.springframework.security.oauth2.core.AuthorizationGrantType com.azure.spring.cloud.autoconfigure.aad.properties.AuthorizationClientProperties::getAuthorizationGrantType()", + "justification": "To support authorization grant type JWT_BEARER, we should use the default AuthorizationGrantType instead of AadAuthorizationGrantType." + }, + { + "code": "java.method.returnTypeChanged", + "old": "method com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType com.azure.spring.cloud.autoconfigure.aadb2c.properties.AuthorizationClientProperties::getAuthorizationGrantType()", + "new": "method org.springframework.security.oauth2.core.AuthorizationGrantType com.azure.spring.cloud.autoconfigure.aadb2c.properties.AuthorizationClientProperties::getAuthorizationGrantType()", + "justification": "To support authorization grant type JWT_BEARER, we should use the default AuthorizationGrantType instead of AadAuthorizationGrantType." + }, + { + "code": "java.method.parameterTypeChanged", + "old": "parameter void com.azure.spring.cloud.autoconfigure.aad.properties.AuthorizationClientProperties::setAuthorizationGrantType(===com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType===)", + "new": "parameter void com.azure.spring.cloud.autoconfigure.aad.properties.AuthorizationClientProperties::setAuthorizationGrantType(===org.springframework.security.oauth2.core.AuthorizationGrantType===)", + "justification": "To support authorization grant type JWT_BEARER, we should use the default AuthorizationGrantType instead of AadAuthorizationGrantType." + }, + { + "code": "java.method.parameterTypeChanged", + "old": "parameter void com.azure.spring.cloud.autoconfigure.aadb2c.properties.AuthorizationClientProperties::setAuthorizationGrantType(===com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType===)", + "new": "parameter void com.azure.spring.cloud.autoconfigure.aadb2c.properties.AuthorizationClientProperties::setAuthorizationGrantType(===org.springframework.security.oauth2.core.AuthorizationGrantType===)", + "justification": "To support authorization grant type JWT_BEARER, we should use the default AuthorizationGrantType instead of AadAuthorizationGrantType." + }, + { + "code": "java.field.removedWithConstant", + "old": "field com.azure.spring.cloud.autoconfigure.aad.AadAuthenticationFilterAutoConfiguration.PROPERTY_PREFIX", + "justification": "Unused constant." + }, { "code": "java.method.visibilityReduced", "new": "method com.azure.spring.cloud.autoconfigure.context.AzureTokenCredentialAutoConfiguration.AzureServiceClientBuilderFactoryPostProcessor com.azure.spring.cloud.autoconfigure.context.AzureTokenCredentialAutoConfiguration::builderFactoryBeanPostProcessor()", diff --git a/sdk/spring/CHANGELOG.md b/sdk/spring/CHANGELOG.md index 8d92e8006605a..bc99adcd1a8bf 100644 --- a/sdk/spring/CHANGELOG.md +++ b/sdk/spring/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features Added - GA the `spring-cloud-azure-starter-storage`. This starter supports all features of Azure Storage. - GA the `spring-cloud-azure-starter-keyvault`. This starter supports all features of Azure Key Vault. +- Support Jwt Client authentication for Azure AD Starter. ### Breaking Changes @@ -48,6 +49,16 @@ This section includes changes in `spring-cloud-azure-starter-active-directory` m #### Dependency Updates - Upgrade spring-security to 5.6.4 to address [CVE-2022-22978](https://spring.io/blog/2022/05/15/cve-2022-22978-authorization-bypass-in-regexrequestmatcher) [#29304](https://github.com/Azure/azure-sdk-for-java/pull/29304). +#### Features Added ++ Support Jwt Client authentication [#29471](https://github.com/Azure/azure-sdk-for-java/pull/29471). + +#### Breaking Changes ++ Deprecated classes and properties type changed [#29471](https://github.com/Azure/azure-sdk-for-java/pull/29471). + + Deprecated ~~AadAuthorizationGrantType~~, use `AuthorizationGrantType` instead. + + Deprecated ~~AadOAuth2AuthenticatedPrincipal~~, ~~AadJwtBearerTokenAuthenticationConverter~~, use the default converter `JwtAuthenticationConverter` instead in `AadResourceServerWebSecurityConfigurerAdapter`. + + The type of property *authorizationGrantType* is changed to `AuthorizationGrantType` in `AuthorizationClientProperties` class. + + Deprecated ~~AadOboOAuth2AuthorizedClientProvider~~, use `JwtBearerOAuth2AuthorizedClientProvider` instead. + ### Spring Cloud Azure Starter Active Directory B2C This section includes changes in `spring-cloud-azure-starter-active-directory-b2c` module. @@ -55,6 +66,11 @@ This section includes changes in `spring-cloud-azure-starter-active-directory-b2 - Upgrade spring-security to 5.6.4 to address [CVE-2022-22978](https://spring.io/blog/2022/05/15/cve-2022-22978-authorization-bypass-in-regexrequestmatcher) [#29304](https://github.com/Azure/azure-sdk-for-java/pull/29304). +#### Breaking Changes ++ Deprecated classes and properties type changed [#29471](https://github.com/Azure/azure-sdk-for-java/pull/29471). + + Deprecated *~~AadAuthorizationGrantType~~*, use `AuthorizationGrantType` instead. + + The type of property *authorizationGrantType* is changed to `AuthorizationGrantType` in `AuthorizationClientProperties` class. + ## 4.2.0 (2022-05-26) - This release is compatible with Spring Boot 2.5.0-2.5.14, 2.6.0-2.6.8, 2.7.0. (Note: 2.5.x (x>14), 2.6.y (y>8) and 2.7.z (z>0) should be supported, but they aren't tested with this release.) diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml b/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml index fe92664ea937a..aee2fe2813682 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml +++ b/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml @@ -330,7 +330,6 @@ - com.microsoft.azure:msal4j:[1.12.0] com.nimbusds:nimbus-jose-jwt:[9.22] org.messaginghub:pooled-jms:[1.2.4] org.apache.qpid:qpid-jms-client:[0.53.0] diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadAuthenticationFilterAutoConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadAuthenticationFilterAutoConfiguration.java index c318ca2a9ba55..895aa4f15982c 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadAuthenticationFilterAutoConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadAuthenticationFilterAutoConfiguration.java @@ -40,12 +40,8 @@ @ConditionalOnMissingClass({ "org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken" }) @Import(AadPropertiesConfiguration.class) public class AadAuthenticationFilterAutoConfiguration { - /** - * The property prefix - */ - public static final String PROPERTY_PREFIX = "spring.cloud.azure.active-directory"; - private static final Logger LOG = LoggerFactory.getLogger(AadAuthenticationProperties.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AadAuthenticationProperties.class); private final AadAuthenticationProperties properties; private final AadAuthorizationServerEndpoints endpoints; @@ -72,7 +68,7 @@ public AadAuthenticationFilterAutoConfiguration(AadAuthenticationProperties prop @ConditionalOnMissingBean(AadAuthenticationFilter.class) @ConditionalOnExpression("${spring.cloud.azure.active-directory.session-stateless:false} == false") public AadAuthenticationFilter aadAuthenticationFilter(ResourceRetriever resourceRetriever, JWKSetCache jwkSetCache) { - LOG.info("AadAuthenticationFilter Constructor."); + LOGGER.info("AadAuthenticationFilter Constructor."); return new AadAuthenticationFilter( properties, endpoints, @@ -91,7 +87,7 @@ public AadAuthenticationFilter aadAuthenticationFilter(ResourceRetriever resourc @ConditionalOnMissingBean(AadAppRoleStatelessAuthenticationFilter.class) @ConditionalOnExpression("${spring.cloud.azure.active-directory.session-stateless:false} == true") public AadAppRoleStatelessAuthenticationFilter aadStatelessAuthFilter(ResourceRetriever resourceRetriever) { - LOG.info("Creating AadStatelessAuthFilter bean."); + LOGGER.info("Creating AadStatelessAuthFilter bean."); return new AadAppRoleStatelessAuthenticationFilter( new UserPrincipalManager( endpoints, diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadAutoConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadAutoConfiguration.java index 7750a1fb70f1f..3dcc5607d7a6b 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadAutoConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadAutoConfiguration.java @@ -24,10 +24,7 @@ AadPropertiesConfiguration.class, AadWebApplicationConfiguration.class, AadResourceServerConfiguration.class, - AadOAuth2ClientConfiguration.OAuth2ClientRepositoryConfiguration.class, - AadOAuth2ClientConfiguration.WebApplicationWithoutResourceServerOAuth2AuthorizedClientManagerConfiguration.class, - AadOAuth2ClientConfiguration.ResourceServerWithOBOOAuth2AuthorizedClientManagerConfiguration.class, - AadOAuth2ClientConfiguration.WebApplicationAndResourceServiceOAuth2AuthorizedClientManagerConfiguration.class + AadOAuth2ClientConfiguration.class }) public class AadAutoConfiguration { diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadClientRegistrationRepository.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadClientRegistrationRepository.java index 95be68891f714..e558e113022bc 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadClientRegistrationRepository.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadClientRegistrationRepository.java @@ -4,12 +4,14 @@ package com.azure.spring.cloud.autoconfigure.aad; import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthenticationProperties; -import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType; import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationServerEndpoints; import com.azure.spring.cloud.autoconfigure.aad.properties.AuthorizationClientProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.util.Assert; import java.util.Collection; @@ -21,8 +23,10 @@ import java.util.Set; import java.util.stream.Collectors; -import static com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType.AUTHORIZATION_CODE; -import static com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType.AZURE_DELEGATED; +import static com.azure.spring.cloud.autoconfigure.aad.implementation.constants.Constants.AZURE_DELEGATED; +import static com.azure.spring.cloud.autoconfigure.aad.implementation.constants.Constants.ON_BEHALF_OF; +import static org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE; +import static org.springframework.security.oauth2.core.AuthorizationGrantType.JWT_BEARER; /** @@ -34,6 +38,8 @@ */ public class AadClientRegistrationRepository implements ClientRegistrationRepository, Iterable { + private static final Logger LOGGER = LoggerFactory.getLogger(AadClientRegistrationRepository.class); + /** * Azure client registration ID */ @@ -68,13 +74,25 @@ public AadClientRegistrationRepository(AadAuthenticationProperties properties) { .stream() .collect(Collectors.toMap( Map.Entry::getKey, - entry -> toClientRegistration(entry.getKey(), entry.getValue().getAuthorizationGrantType(), - entry.getValue().getScopes(), properties))); + entry -> toClientRegistration(entry.getKey(), + entry.getValue().getAuthorizationGrantType(), + entry.getValue().getScopes(), + entry.getValue().getClientAuthenticationMethod(), + properties))); + ClientAuthenticationMethod azureClientAuthMethod = getAzureDefaultClientAuthenticationMethod(); ClientRegistration azureClient = - toClientRegistration(AZURE_CLIENT_REGISTRATION_ID, AUTHORIZATION_CODE, authorizationCodeScopes, properties); + toClientRegistration(AZURE_CLIENT_REGISTRATION_ID, AUTHORIZATION_CODE, + authorizationCodeScopes, azureClientAuthMethod, properties); allClients.put(AZURE_CLIENT_REGISTRATION_ID, azureClient); } + private ClientAuthenticationMethod getAzureDefaultClientAuthenticationMethod() { + if (this.allClients.containsKey(AZURE_CLIENT_REGISTRATION_ID)) { + return this.allClients.get(AZURE_CLIENT_REGISTRATION_ID).getClientAuthenticationMethod(); + } + return ClientAuthenticationMethod.CLIENT_SECRET_BASIC; + } + /** * Gets the set of Azure client access token scopes. * @@ -94,8 +112,7 @@ public ClientRegistration findByRegistrationId(String registrationId) { public Iterator iterator() { return allClients.values() .stream() - .filter(client -> - client.getAuthorizationGrantType().getValue().equals(AUTHORIZATION_CODE.getValue())) + .filter(client -> AUTHORIZATION_CODE.equals(client.getAuthorizationGrantType())) .iterator(); } @@ -132,19 +149,28 @@ private Set delegatedClientsAccessTokenScopes(AadAuthenticationPropertie } private ClientRegistration toClientRegistration(String registrationId, - AadAuthorizationGrantType aadAuthorizationGrantType, + AuthorizationGrantType authorizationGrantType, Collection scopes, + ClientAuthenticationMethod clientAuthenticationMethod, AadAuthenticationProperties properties) { AadAuthorizationServerEndpoints endpoints = - new AadAuthorizationServerEndpoints(properties.getProfile().getEnvironment().getActiveDirectoryEndpoint(), properties.getProfile().getTenantId()); + new AadAuthorizationServerEndpoints(properties.getProfile().getEnvironment().getActiveDirectoryEndpoint(), + properties.getProfile().getTenantId()); + + if (ON_BEHALF_OF.equals(authorizationGrantType)) { + authorizationGrantType = JWT_BEARER; + LOGGER.warn("The grant type 'on_behalf_of' is an alias, it will be replaced with " + + "'urn:ietf:params:oauth:grant-type:jwt-bearer' for client {}.", registrationId); + } return ClientRegistration.withRegistrationId(registrationId) .clientName(registrationId) - .authorizationGrantType(new AuthorizationGrantType((aadAuthorizationGrantType.getValue()))) + .authorizationGrantType(authorizationGrantType) .scope(scopes) .redirectUri(properties.getRedirectUriTemplate()) .userNameAttributeName(properties.getUserNameAttribute()) .clientId(properties.getCredential().getClientId()) .clientSecret(properties.getCredential().getClientSecret()) + .clientAuthenticationMethod(clientAuthenticationMethod) .authorizationUri(endpoints.getAuthorizationEndpoint()) .tokenUri(endpoints.getTokenEndpoint()) .jwkSetUri(endpoints.getJwkSetEndpoint()) diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadJwtBearerTokenAuthenticationConverter.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadJwtBearerTokenAuthenticationConverter.java index 51200d5d4a25b..d9dd09d67346c 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadJwtBearerTokenAuthenticationConverter.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadJwtBearerTokenAuthenticationConverter.java @@ -14,6 +14,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.util.Assert; import java.util.Collection; @@ -23,7 +24,10 @@ /** * A {@link Converter} that takes a {@link Jwt} and converts it into a {@link BearerTokenAuthentication}. + * + * @deprecated use the default converter {@link JwtAuthenticationConverter} instead in {@link AadResourceServerWebSecurityConfigurerAdapter}. */ +@Deprecated public class AadJwtBearerTokenAuthenticationConverter implements Converter { private final Converter> converter; diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadResourceServerWebSecurityConfigurerAdapter.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadResourceServerWebSecurityConfigurerAdapter.java index 748964c254ca3..c4d7123dbdb2a 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadResourceServerWebSecurityConfigurerAdapter.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadResourceServerWebSecurityConfigurerAdapter.java @@ -3,10 +3,16 @@ package com.azure.spring.cloud.autoconfigure.aad; +import com.azure.spring.cloud.autoconfigure.aad.implementation.jwt.AadJwtGrantedAuthoritiesConverter; import com.azure.spring.cloud.autoconfigure.aad.properties.AadResourceServerProperties; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.util.StringUtils; /** * Abstract configuration class, used to make JwtConfigurer and AADJwtBearerTokenAuthenticationConverter take effect. @@ -30,10 +36,17 @@ protected void configure(HttpSecurity http) throws Exception { // @formatter:off http.oauth2ResourceServer() .jwt() - .jwtAuthenticationConverter( - new AadJwtBearerTokenAuthenticationConverter( - properties.getPrincipalClaimName(), properties.getClaimToAuthorityPrefixMap())); + .jwtAuthenticationConverter(jwtAuthenticationConverter()); // @formatter:off } + private Converter jwtAuthenticationConverter() { + JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); + if (StringUtils.hasText(properties.getPrincipalClaimName())) { + converter.setPrincipalClaimName(properties.getPrincipalClaimName()); + } + converter.setJwtGrantedAuthoritiesConverter( + new AadJwtGrantedAuthoritiesConverter(properties.getClaimToAuthorityPrefixMap())); + return converter; + } } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadWebSecurityConfigurerAdapter.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadWebSecurityConfigurerAdapter.java index 6dbd0c90d170f..cdf27706b7869 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadWebSecurityConfigurerAdapter.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadWebSecurityConfigurerAdapter.java @@ -3,8 +3,11 @@ package com.azure.spring.cloud.autoconfigure.aad; +import com.azure.spring.cloud.autoconfigure.aad.implementation.jwt.AadJwtClientAuthenticationParametersConverter; +import com.azure.spring.cloud.autoconfigure.aad.implementation.oauth2.OAuth2ClientAuthenticationJwkResolver; import com.azure.spring.cloud.autoconfigure.aad.implementation.webapp.AadOAuth2AuthorizationCodeGrantRequestEntityConverter; import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthenticationProperties; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @@ -49,6 +52,12 @@ public abstract class AadWebSecurityConfigurerAdapter extends WebSecurityConfigu @Autowired protected AadAuthenticationProperties properties; + /** + * JWK resolver implementation for client authentication. + */ + @Autowired + protected ObjectProvider jwkResolvers; + /** * configure * @@ -114,9 +123,14 @@ protected LogoutSuccessHandler oidcLogoutSuccessHandler() { protected OAuth2AccessTokenResponseClient accessTokenResponseClient() { DefaultAuthorizationCodeTokenResponseClient result = new DefaultAuthorizationCodeTokenResponseClient(); if (repo instanceof AadClientRegistrationRepository) { - result.setRequestEntityConverter( + AadOAuth2AuthorizationCodeGrantRequestEntityConverter converter = new AadOAuth2AuthorizationCodeGrantRequestEntityConverter( - ((AadClientRegistrationRepository) repo).getAzureClientAccessTokenScopes())); + ((AadClientRegistrationRepository) repo).getAzureClientAccessTokenScopes()); + OAuth2ClientAuthenticationJwkResolver jwkResolver = jwkResolvers.getIfUnique(); + if (jwkResolver != null) { + converter.addParametersConverter(new AadJwtClientAuthenticationParametersConverter<>(jwkResolver::resolve)); + } + result.setRequestEntityConverter(converter); } return result; } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/configuration/AadOAuth2ClientConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/configuration/AadOAuth2ClientConfiguration.java index 952edb6037d4e..62221d9317e0d 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/configuration/AadOAuth2ClientConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/configuration/AadOAuth2ClientConfiguration.java @@ -3,23 +3,34 @@ package com.azure.spring.cloud.autoconfigure.aad.configuration; -import com.azure.spring.cloud.autoconfigure.aad.implementation.conditions.ClientRegistrationCondition; -import com.azure.spring.cloud.autoconfigure.aad.implementation.conditions.ResourceServerWithOBOCondition; -import com.azure.spring.cloud.autoconfigure.aad.implementation.conditions.WebApplicationAndResourceServerCondition; -import com.azure.spring.cloud.autoconfigure.aad.implementation.conditions.WebApplicationWithoutResourceServerCondition; import com.azure.spring.cloud.autoconfigure.aad.AadClientRegistrationRepository; +import com.azure.spring.cloud.autoconfigure.aad.implementation.oauth2.AadOAuth2ClientAuthenticationJwkResolver; +import com.azure.spring.cloud.autoconfigure.aad.implementation.oauth2.OAuth2ClientAuthenticationJwkResolver; +import com.azure.spring.cloud.autoconfigure.aad.implementation.conditions.ClientRegistrationCondition; +import com.azure.spring.cloud.autoconfigure.aad.implementation.conditions.ClientCertificatePropertiesCondition; +import com.azure.spring.cloud.autoconfigure.aad.implementation.jwt.AadJwtClientAuthenticationParametersConverter; import com.azure.spring.cloud.autoconfigure.aad.implementation.oauth2.JacksonHttpSessionOAuth2AuthorizedClientRepository; -import com.azure.spring.cloud.autoconfigure.aad.implementation.webapi.AadOboOAuth2AuthorizedClientProvider; +import com.azure.spring.cloud.autoconfigure.aad.implementation.webapi.AadJwtBearerGrantRequestEntityConverter; import com.azure.spring.cloud.autoconfigure.aad.implementation.webapp.AadAzureDelegatedOAuth2AuthorizedClientProvider; import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthenticationProperties; +import com.nimbusds.jose.jwk.JWK; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.client.JwtBearerOAuth2AuthorizedClientProvider; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder; import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.DefaultJwtBearerTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.DefaultPasswordTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequestEntityConverter; +import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequestEntityConverter; +import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequestEntityConverter; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; @@ -31,146 +42,147 @@ *

*/ @Configuration(proxyBeanMethods = false) +@Conditional(ClientRegistrationCondition.class) public class AadOAuth2ClientConfiguration { /** - * OAuth2 client configuration for AAD. + * Declare ClientRegistrationRepository bean. + * + * @param properties the AAD authentication properties + * @return ClientRegistrationRepository bean */ - @Configuration(proxyBeanMethods = false) - @Conditional(ClientRegistrationCondition.class) - public static class OAuth2ClientRepositoryConfiguration { - - /** - * Declare ClientRegistrationRepository bean. - * - * @param properties the AAD authentication properties - * @return ClientRegistrationRepository bean - */ - @Bean - @ConditionalOnMissingBean - public ClientRegistrationRepository clientRegistrationRepository(AadAuthenticationProperties properties) { - return new AadClientRegistrationRepository(properties); - } - - /** - * Declare OAuth2AuthorizedClientRepository bean. - * - * @return OAuth2AuthorizedClientRepository bean - */ - @Bean - @ConditionalOnMissingBean - public OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository() { - return new JacksonHttpSessionOAuth2AuthorizedClientRepository(); - } + @Bean + @ConditionalOnMissingBean + public ClientRegistrationRepository clientRegistrationRepository(AadAuthenticationProperties properties) { + return new AadClientRegistrationRepository(properties); } /** - * Web application scenario, OAuth2AuthorizedClientManager configuration for AAD. + * Declare OAuth2AuthorizedClientRepository bean. + * + * @return OAuth2AuthorizedClientRepository bean */ - @Configuration(proxyBeanMethods = false) - @Conditional(WebApplicationWithoutResourceServerCondition.class) - public static class WebApplicationWithoutResourceServerOAuth2AuthorizedClientManagerConfiguration { - - /** - * Declare OAuth2AuthorizedClientManager bean for Resource Server with OBO scenario. - * - * @param clientRegistrations the client registration repository - * @param authorizedClients the OAuth2 authorized client repository - * @return OAuth2AuthorizedClientManager bean - */ - @Bean - @ConditionalOnMissingBean - public OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrations, - OAuth2AuthorizedClientRepository authorizedClients) { - DefaultOAuth2AuthorizedClientManager manager = - new DefaultOAuth2AuthorizedClientManager(clientRegistrations, authorizedClients); - AadAzureDelegatedOAuth2AuthorizedClientProvider azureDelegatedProvider = - new AadAzureDelegatedOAuth2AuthorizedClientProvider( - new RefreshTokenOAuth2AuthorizedClientProvider(), - authorizedClients); - OAuth2AuthorizedClientProvider authorizedClientProviders = - OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .provider(azureDelegatedProvider) - .build(); - manager.setAuthorizedClientProvider(authorizedClientProviders); - return manager; - } + @Bean + @ConditionalOnMissingBean + public OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository() { + return new JacksonHttpSessionOAuth2AuthorizedClientRepository(); } /** - * Resource server with OBO scenario, OAuth2AuthorizedClientManager configuration for AAD. + * Return the resolver to resolve a {@link JWK} through the {@link ClientRegistration}. + * + * @param properties the AAD authentication properties + * @return the function that will resolve out the JWK. */ - @Configuration(proxyBeanMethods = false) - @Conditional(ResourceServerWithOBOCondition.class) - public static class ResourceServerWithOBOOAuth2AuthorizedClientManagerConfiguration { - - /** - * Declare OAuth2AuthorizedClientManager bean for Resource Server with OBO scenario. - * - * @param clientRegistrations the client registration repository - * @param authorizedClients the OAuth2 authorized client repository - * @return OAuth2AuthorizedClientManager bean - */ - @Bean - @ConditionalOnMissingBean - public OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrations, - OAuth2AuthorizedClientRepository authorizedClients) { - DefaultOAuth2AuthorizedClientManager manager = - new DefaultOAuth2AuthorizedClientManager(clientRegistrations, authorizedClients); - AadOboOAuth2AuthorizedClientProvider oboProvider = new AadOboOAuth2AuthorizedClientProvider(); - OAuth2AuthorizedClientProvider authorizedClientProviders = - OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .provider(oboProvider) - .build(); - manager.setAuthorizedClientProvider(authorizedClientProviders); - return manager; - } + @Bean + @ConditionalOnMissingBean + @Conditional(ClientCertificatePropertiesCondition.class) + OAuth2ClientAuthenticationJwkResolver oAuth2ClientAuthenticationJwkResolver(AadAuthenticationProperties properties) { + return new AadOAuth2ClientAuthenticationJwkResolver( + properties.getCredential().getClientCertificatePath(), + properties.getCredential().getClientCertificatePassword()); } /** - * Web application and resource server scenario, OAuth2AuthorizedClientManager configuration for AAD. + * Declare OAuth2AuthorizedClientManager bean. + * + * @param clientRegistrations the client registration repository + * @param authorizedClients the OAuth2 authorized client repository + * @param refreshTokenProvider the refresh token grant type provider + * @param jwtBearerProvider the jwt bearer grant type provider + * @param jwkResolvers the {@link JWK} resolver + * @return OAuth2AuthorizedClientManager bean */ - @Configuration(proxyBeanMethods = false) - @Conditional(WebApplicationAndResourceServerCondition.class) - public static class WebApplicationAndResourceServiceOAuth2AuthorizedClientManagerConfiguration { - - /** - * Declare OAuth2AuthorizedClientManager bean. - * - * @param clientRegistrations the client registration repository - * @param authorizedClients the OAuth2 authorized client repository - * @return OAuth2AuthorizedClientManager bean - */ - @Bean - @ConditionalOnMissingBean - public OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrations, - OAuth2AuthorizedClientRepository authorizedClients) { - DefaultOAuth2AuthorizedClientManager manager = - new DefaultOAuth2AuthorizedClientManager(clientRegistrations, authorizedClients); - AadAzureDelegatedOAuth2AuthorizedClientProvider azureDelegatedProvider = - new AadAzureDelegatedOAuth2AuthorizedClientProvider( - new RefreshTokenOAuth2AuthorizedClientProvider(), - authorizedClients); - AadOboOAuth2AuthorizedClientProvider oboProvider = new AadOboOAuth2AuthorizedClientProvider(); - OAuth2AuthorizedClientProvider authorizedClientProviders = - OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .provider(azureDelegatedProvider) - .provider(oboProvider) - .build(); - manager.setAuthorizedClientProvider(authorizedClientProviders); - return manager; + @Bean + @ConditionalOnMissingBean + OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrations, + OAuth2AuthorizedClientRepository authorizedClients, + RefreshTokenOAuth2AuthorizedClientProvider refreshTokenProvider, + JwtBearerOAuth2AuthorizedClientProvider jwtBearerProvider, + ObjectProvider jwkResolvers) { + DefaultOAuth2AuthorizedClientManager manager = + new DefaultOAuth2AuthorizedClientManager(clientRegistrations, authorizedClients); + + OAuth2ClientAuthenticationJwkResolver jwkResolver = jwkResolvers.getIfUnique(); + // @formatter:off + OAuth2AuthorizedClientProvider providers = + OAuth2AuthorizedClientProviderBuilder + .builder() + .authorizationCode() + .clientCredentials(builder -> + clientCredentialsGrantBuilderAccessTokenResponseClientCustomizer(builder, jwkResolver)) + .password(builder -> passwordGrantBuilderAccessTokenResponseClientCustomizer(builder, jwkResolver)) + .provider(refreshTokenProvider) + .provider(jwtBearerProvider) + .provider(azureDelegatedOAuth2AuthorizedClientProvider(refreshTokenProvider, authorizedClients)) + .build(); + // @formatter:on + manager.setAuthorizedClientProvider(providers); + return manager; + } + + @Bean + @ConditionalOnMissingBean + JwtBearerOAuth2AuthorizedClientProvider azureAdJwtBearerProvider(ObjectProvider resolvers) { + JwtBearerOAuth2AuthorizedClientProvider provider = new JwtBearerOAuth2AuthorizedClientProvider(); + OAuth2ClientAuthenticationJwkResolver resolver = resolvers.getIfUnique(); + if (resolver != null) { + AadJwtBearerGrantRequestEntityConverter jwtBearerConverter = new AadJwtBearerGrantRequestEntityConverter(); + jwtBearerConverter.addParametersConverter(new AadJwtClientAuthenticationParametersConverter<>(resolver::resolve)); + + DefaultJwtBearerTokenResponseClient responseClient = new DefaultJwtBearerTokenResponseClient(); + responseClient.setRequestEntityConverter(jwtBearerConverter); + provider.setAccessTokenResponseClient(responseClient); } + return provider; + } + + @Bean + @ConditionalOnMissingBean + RefreshTokenOAuth2AuthorizedClientProvider azureRefreshTokenProvider(ObjectProvider resolvers) { + RefreshTokenOAuth2AuthorizedClientProvider provider = new RefreshTokenOAuth2AuthorizedClientProvider(); + OAuth2ClientAuthenticationJwkResolver resolver = resolvers.getIfUnique(); + if (resolver != null) { + OAuth2RefreshTokenGrantRequestEntityConverter converter = new OAuth2RefreshTokenGrantRequestEntityConverter(); + converter.addParametersConverter(new AadJwtClientAuthenticationParametersConverter<>(resolver::resolve)); + + DefaultRefreshTokenTokenResponseClient responseClient = new DefaultRefreshTokenTokenResponseClient(); + responseClient.setRequestEntityConverter(converter); + provider.setAccessTokenResponseClient(responseClient); + } + return provider; + } + + private void passwordGrantBuilderAccessTokenResponseClientCustomizer(OAuth2AuthorizedClientProviderBuilder.PasswordGrantBuilder builder, + OAuth2ClientAuthenticationJwkResolver resolver) { + if (resolver != null) { + OAuth2PasswordGrantRequestEntityConverter converter = new OAuth2PasswordGrantRequestEntityConverter(); + converter.addParametersConverter(new AadJwtClientAuthenticationParametersConverter<>(resolver::resolve)); + + DefaultPasswordTokenResponseClient client = new DefaultPasswordTokenResponseClient(); + client.setRequestEntityConverter(converter); + + builder.accessTokenResponseClient(client); + } + } + + private void clientCredentialsGrantBuilderAccessTokenResponseClientCustomizer(OAuth2AuthorizedClientProviderBuilder.ClientCredentialsGrantBuilder builder, + OAuth2ClientAuthenticationJwkResolver resolver) { + if (resolver != null) { + OAuth2ClientCredentialsGrantRequestEntityConverter converter = + new OAuth2ClientCredentialsGrantRequestEntityConverter(); + converter.addParametersConverter(new AadJwtClientAuthenticationParametersConverter<>(resolver::resolve)); + + DefaultClientCredentialsTokenResponseClient client = new DefaultClientCredentialsTokenResponseClient(); + client.setRequestEntityConverter(converter); + + builder.accessTokenResponseClient(client); + } + } + + private AadAzureDelegatedOAuth2AuthorizedClientProvider azureDelegatedOAuth2AuthorizedClientProvider( + RefreshTokenOAuth2AuthorizedClientProvider refreshTokenProvider, + OAuth2AuthorizedClientRepository authorizedClients) { + return new AadAzureDelegatedOAuth2AuthorizedClientProvider(refreshTokenProvider, authorizedClients); } } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/configuration/AadPropertiesConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/configuration/AadPropertiesConfiguration.java index b572871e71a1b..b5c8df6f9bc92 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/configuration/AadPropertiesConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/configuration/AadPropertiesConfiguration.java @@ -49,6 +49,8 @@ public AadAuthenticationProperties aadAuthenticationProperties() { global.getProfile().getEnvironment().getMicrosoftGraphEndpoint()); aad.getCredential().setClientId(global.getCredential().getClientId()); aad.getCredential().setClientSecret(global.getCredential().getClientSecret()); + aad.getCredential().setClientCertificatePath(global.getCredential().getClientCertificatePath()); + aad.getCredential().setClientCertificatePassword(global.getCredential().getClientCertificatePassword()); aad.getProfile().setTenantId(global.getProfile().getTenantId()); return aad; } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/AbstractApplicationTypeCondition.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/AbstractApplicationTypeCondition.java index 4759195e70aa9..6872b4a7e2b21 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/AbstractApplicationTypeCondition.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/AbstractApplicationTypeCondition.java @@ -44,7 +44,7 @@ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeM .bind("spring.cloud.azure.active-directory", AadAuthenticationProperties.class) .orElse(null); if (properties == null) { - return ConditionOutcome.noMatch(message.notAvailable("aad authorization properties")); + return ConditionOutcome.noMatch(message.notAvailable("Azure AD authentication properties")); } // Bind properties will not execute AADAuthenticationProperties#afterPropertiesSet() diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/ClientCertificatePropertiesCondition.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/ClientCertificatePropertiesCondition.java new file mode 100644 index 0000000000000..878f8e5db37d6 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/ClientCertificatePropertiesCondition.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.aad.implementation.conditions; + +import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthenticationProperties; +import com.azure.spring.cloud.autoconfigure.context.AzureGlobalProperties; +import org.springframework.boot.autoconfigure.condition.ConditionMessage; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.util.StringUtils; + +/** + * Condition that checks for OAuth2 client JWK resolver. + */ +public class ClientCertificatePropertiesCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + ConditionMessage.Builder message = ConditionMessage + .forCondition("Azure AD OAuth2 client JWK resolver Condition"); + AzureGlobalProperties globalProperties = + Binder.get(context.getEnvironment()) + .bind("spring.cloud.azure", AzureGlobalProperties.class) + .orElse(null); + AadAuthenticationProperties properties = + Binder.get(context.getEnvironment()) + .bind("spring.cloud.azure.active-directory", AadAuthenticationProperties.class) + .orElse(null); + if (globalProperties == null && properties == null) { + return ConditionOutcome.noMatch(message.notAvailable("Azure AD authentication properties")); + } + + if (globalProperties != null + && StringUtils.hasText(globalProperties.getCredential().getClientCertificatePath()) + && StringUtils.hasText(globalProperties.getCredential().getClientCertificatePassword())) { + return ConditionOutcome.match( + message.foundExactly("'client-certificate-path' and 'client-certificate-password' " + + "under the prefix 'spring.cloud.azure.credential'.")); + + } + + if (StringUtils.hasText(properties.getCredential().getClientCertificatePath()) + && StringUtils.hasText(properties.getCredential().getClientCertificatePassword())) { + return ConditionOutcome.match( + message.foundExactly("'client-certificate-path' and 'client-certificate-password' " + + "under the prefix 'spring.cloud.azure.active-directory.credential'.")); + + } + return ConditionOutcome.noMatch( + message.because("No attribute configuration found " + + "for 'client-certificate-path' and 'client-certificate-password'.")); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/ResourceServerWithOBOCondition.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/ResourceServerWithOBOCondition.java deleted file mode 100644 index ee508bce4296b..0000000000000 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/ResourceServerWithOBOCondition.java +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.spring.cloud.autoconfigure.aad.implementation.conditions; - -import com.azure.spring.cloud.autoconfigure.aad.properties.AadApplicationType; -import org.springframework.context.annotation.Condition; - -import static com.azure.spring.cloud.autoconfigure.aad.properties.AadApplicationType.RESOURCE_SERVER_WITH_OBO; - -/** - * {@link Condition} that checks for resource server with OBO scenario. - */ -public final class ResourceServerWithOBOCondition extends AbstractApplicationTypeCondition { - - @Override - boolean isTargetApplicationType(AadApplicationType applicationType) { - return applicationType == RESOURCE_SERVER_WITH_OBO; - } - - @Override - protected String getConditionTitle() { - return "AAD Resource Server with OBO Condition"; - } -} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/WebApplicationAndResourceServerCondition.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/WebApplicationAndResourceServerCondition.java deleted file mode 100644 index 64ba59f2c0ed9..0000000000000 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/WebApplicationAndResourceServerCondition.java +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.spring.cloud.autoconfigure.aad.implementation.conditions; - -import com.azure.spring.cloud.autoconfigure.aad.properties.AadApplicationType; -import org.springframework.context.annotation.Condition; - -import static com.azure.spring.cloud.autoconfigure.aad.properties.AadApplicationType.WEB_APPLICATION_AND_RESOURCE_SERVER; - -/** - * {@link Condition} that checks for Web application and resource server scenario. - */ -public final class WebApplicationAndResourceServerCondition extends AbstractApplicationTypeCondition { - - @Override - boolean isTargetApplicationType(AadApplicationType applicationType) { - return applicationType == WEB_APPLICATION_AND_RESOURCE_SERVER; - } - - @Override - protected String getConditionTitle() { - return "AAD Web Application and Resource Server Condition"; - } -} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/WebApplicationWithoutResourceServerCondition.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/WebApplicationWithoutResourceServerCondition.java deleted file mode 100644 index 1aef1bd4f5641..0000000000000 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/WebApplicationWithoutResourceServerCondition.java +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.spring.cloud.autoconfigure.aad.implementation.conditions; - -import com.azure.spring.cloud.autoconfigure.aad.properties.AadApplicationType; -import org.springframework.context.annotation.Condition; - -import static com.azure.spring.cloud.autoconfigure.aad.properties.AadApplicationType.WEB_APPLICATION; - -/** - * {@link Condition} that checks for Web application without Resource Server scenario. - */ -public final class WebApplicationWithoutResourceServerCondition extends AbstractApplicationTypeCondition { - - @Override - boolean isTargetApplicationType(AadApplicationType applicationType) { - return applicationType == WEB_APPLICATION; - } - - @Override - protected String getConditionTitle() { - return "AAD Web application Condition"; - } -} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/constants/Constants.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/constants/Constants.java index 367682d5645c9..8fc40335512b9 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/constants/Constants.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/constants/Constants.java @@ -4,6 +4,7 @@ package com.azure.spring.cloud.autoconfigure.aad.implementation.constants; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.AuthorizationGrantType; import java.util.Collections; import java.util.HashSet; @@ -37,6 +38,12 @@ private Constants() { */ public static final Set DEFAULT_AUTHORITY_SET; + /** + * The same with {@link AuthorizationGrantType#JWT_BEARER}. + */ + public static final AuthorizationGrantType ON_BEHALF_OF = new AuthorizationGrantType("on_behalf_of"); + public static final AuthorizationGrantType AZURE_DELEGATED = new AuthorizationGrantType("azure_delegated"); + static { Set authoritySet = new HashSet<>(); authoritySet.add(new SimpleGrantedAuthority(AuthorityPrefix.ROLE + "USER")); diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/jackson/StdConverters.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/jackson/StdConverters.java index 79b6f59620c51..c78b6604b25c5 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/jackson/StdConverters.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/jackson/StdConverters.java @@ -8,6 +8,8 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import static com.azure.spring.cloud.autoconfigure.aad.implementation.constants.Constants.ON_BEHALF_OF; + final class StdConverters { private static final String FIELD_NAME_OF_VALUE = "value"; @@ -28,6 +30,9 @@ public ClientAuthenticationMethod convert(JsonNode jsonNode) { || ClientAuthenticationMethod.POST.getValue().equalsIgnoreCase(value)) { return ClientAuthenticationMethod.CLIENT_SECRET_POST; } + if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue().equalsIgnoreCase(value)) { + return ClientAuthenticationMethod.PRIVATE_KEY_JWT; + } if (ClientAuthenticationMethod.NONE.getValue().equalsIgnoreCase(value)) { return ClientAuthenticationMethod.NONE; } @@ -53,6 +58,10 @@ public AuthorizationGrantType convert(JsonNode jsonNode) { if (AuthorizationGrantType.PASSWORD.getValue().equalsIgnoreCase(value)) { return AuthorizationGrantType.PASSWORD; } + if (AuthorizationGrantType.JWT_BEARER.getValue().equalsIgnoreCase(value) + || ON_BEHALF_OF.getValue().equalsIgnoreCase(value)) { + return AuthorizationGrantType.JWT_BEARER; + } return new AuthorizationGrantType(value); } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/jwt/AadJwtClientAuthenticationParametersConverter.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/jwt/AadJwtClientAuthenticationParametersConverter.java new file mode 100644 index 0000000000000..f6d14436cc838 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/jwt/AadJwtClientAuthenticationParametersConverter.java @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.aad.implementation.jwt; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.KeyType; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AuthorizationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; +import org.springframework.security.oauth2.jose.jws.MacAlgorithm; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * A {@link Converter} that customizes the OAuth 2.0 Access Token Request parameters by adding a signed JSON Web Token + * (JWS) to be used for client authentication at the Azure AD Authorization Server's Token Endpoint. + * + * A specially customized version for Azure AD based on 'NimbusJwtClientAuthenticationParametersConverter' implementation. + * + * @param the type of {@link AbstractOAuth2AuthorizationGrantRequest} + * @since 4.3.0 + */ +public final class AadJwtClientAuthenticationParametersConverter + implements Converter> { + + private static final String INVALID_KEY_ERROR_CODE = "invalid_key"; + + private static final String INVALID_ALGORITHM_ERROR_CODE = "invalid_algorithm"; + + public static final String CLIENT_ASSERTION_TYPE_VALUE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; + + private final Function jwkResolver; + + private final Map jwsEncoders = new ConcurrentHashMap<>(); + + /** + * Constructs a {@code AadJwtClientAuthenticationParametersConverter} using the provided parameters. + * + * @param jwkResolver the resolver that provides the {@code com.nimbusds.jose.jwk.JWK} associated to the {@link + * ClientRegistration client} + */ + public AadJwtClientAuthenticationParametersConverter(Function jwkResolver) { + Assert.notNull(jwkResolver, "jwkResolver cannot be null"); + this.jwkResolver = jwkResolver; + } + + @Override + public MultiValueMap convert(T authorizationGrantRequest) { + Assert.notNull(authorizationGrantRequest, "authorizationGrantRequest cannot be null"); + + ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration(); + if (!ClientAuthenticationMethod.PRIVATE_KEY_JWT.equals(clientRegistration.getClientAuthenticationMethod())) { + return null; + } + + JWK jwk = this.jwkResolver.apply(clientRegistration); + if (jwk == null) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_KEY_ERROR_CODE, + "Failed to resolve JWK signing key for client registration '" + + clientRegistration.getRegistrationId() + "'.", + null); + throw new OAuth2AuthorizationException(oauth2Error); + } + + JwsAlgorithm jwsAlgorithm = resolveAlgorithm(jwk); + if (jwsAlgorithm == null) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_ALGORITHM_ERROR_CODE, + "Unable to resolve JWS (signing) algorithm from JWK associated to client registration '" + + clientRegistration.getRegistrationId() + "'.", + null); + throw new OAuth2AuthorizationException(oauth2Error); + } + + Map jwsHeader = new HashMap<>(); + jwsHeader.put("typ", "JWT"); + jwsHeader.put("alg", SignatureAlgorithm.RS256.getName()); + jwsHeader.put("x5t", jwk.getX509CertThumbprint().toString()); + + Map jwtClaimsSet = new HashMap<>(); + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plus(Duration.ofSeconds(60L)); + jwtClaimsSet.put(JwtClaimNames.ISS, clientRegistration.getClientId()); + jwtClaimsSet.put(JwtClaimNames.SUB, clientRegistration.getClientId()); + jwtClaimsSet.put(JwtClaimNames.AUD, + Collections.singletonList(clientRegistration.getProviderDetails().getTokenUri())); + jwtClaimsSet.put(JwtClaimNames.JTI, UUID.randomUUID().toString()); + jwtClaimsSet.put(JwtClaimNames.IAT, issuedAt); + jwtClaimsSet.put(JwtClaimNames.EXP, expiresAt); + + JwsEncoderHolder jwsEncoderHolder = this.jwsEncoders.compute(clientRegistration.getRegistrationId(), + (clientRegistrationId, currentJwsEncoderHolder) -> { + if (currentJwsEncoderHolder != null && currentJwsEncoderHolder.getJwk().equals(jwk)) { + return currentJwsEncoderHolder; + } + JWKSource jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk)); + return new JwsEncoderHolder(new AadJwtEncoder(jwkSource), jwk); + }); + + AadJwtEncoder jwtEncoder = jwsEncoderHolder.getJwtEncoder(); + + Jwt jwt = jwtEncoder.encode(jwsHeader, jwtClaimsSet); + + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.set(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, CLIENT_ASSERTION_TYPE_VALUE); + parameters.set(OAuth2ParameterNames.CLIENT_ASSERTION, jwt.getTokenValue()); + return parameters; + } + + private static JwsAlgorithm resolveAlgorithm(JWK jwk) { + JwsAlgorithm jwsAlgorithm = null; + + if (jwk.getAlgorithm() != null) { + jwsAlgorithm = SignatureAlgorithm.from(jwk.getAlgorithm().getName()); + if (jwsAlgorithm == null) { + jwsAlgorithm = MacAlgorithm.from(jwk.getAlgorithm().getName()); + } + } + if (jwsAlgorithm == null && KeyType.RSA.equals(jwk.getKeyType())) { + jwsAlgorithm = SignatureAlgorithm.RS256; + } + return jwsAlgorithm; + } + + private static final class JwsEncoderHolder { + + private final AadJwtEncoder jwtEncoder; + + private final JWK jwk; + + private JwsEncoderHolder(AadJwtEncoder jwtEncoder, JWK jwk) { + this.jwtEncoder = jwtEncoder; + this.jwk = jwk; + } + + private AadJwtEncoder getJwtEncoder() { + return this.jwtEncoder; + } + + private JWK getJwk() { + return this.jwk; + } + + } + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/jwt/AadJwtEncoder.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/jwt/AadJwtEncoder.java new file mode 100644 index 0000000000000..13bbf2626dd70 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/jwt/AadJwtEncoder.java @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.aad.implementation.jwt; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JOSEObjectType; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKMatcher; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.jwk.KeyType; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jose.produce.JWSSignerFactory; +import com.nimbusds.jose.util.Base64URL; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.time.Instant; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A JWT encoder that encodes a JSON Web Token (JWT) using the JSON Web Signature (JWS) + * Compact Serialization format. The private/secret key used for signing the JWS is + * supplied by the {@code com.nimbusds.jose.jwk.source.JWKSource} provided via the + * constructor. + * + *

+ * NOTE: A specially customized version for Azure AD based on 'NimbusJwsEncoder' or 'JwtEncoder' implementation. + * It is compatible with spring-security 5.5.X and 5.6.X versions, it can be replaced by 'NimbusJwsEncoder' when the minimum supported version is 5.6.X. + * + * @since 4.3.0 + */ +public final class AadJwtEncoder { + + private static final String ENCODING_ERROR_MESSAGE_TEMPLATE = "An error occurred while attempting to encode the Jwt: %s"; + + private static final JWSSignerFactory JWS_SIGNER_FACTORY = new DefaultJWSSignerFactory(); + + private final Map jwsSigners = new ConcurrentHashMap<>(); + + private final JWKSource jwkSource; + + /** + * Constructs a {@code NimbusJwtEncoder} using the provided parameters. + * + * @param jwkSource the {@code com.nimbusds.jose.jwk.source.JWKSource} + */ + public AadJwtEncoder(JWKSource jwkSource) { + Assert.notNull(jwkSource, "jwkSource cannot be null"); + this.jwkSource = jwkSource; + } + + public Jwt encode(Map jwsHeader, Map jwtClaimsSet) throws JwtException { + Assert.notNull(jwsHeader, "jwsHeader cannot be null"); + Assert.notNull(jwtClaimsSet, "jwtClaimsSet cannot be null"); + JWK jwk = selectJwk(jwsHeader); + String jws = serialize(jwsHeader, jwtClaimsSet, jwk); + return new Jwt(jws, + (Instant) jwtClaimsSet.get(JwtClaimNames.IAT), + (Instant) jwtClaimsSet.get(JwtClaimNames.EXP), + jwsHeader, jwtClaimsSet); + } + + private JWK selectJwk(Map jwsHeader) { + List jwks; + try { + JWKSelector jwkSelector = new JWKSelector(createJwkMatcher(jwsHeader)); + jwks = this.jwkSource.get(jwkSelector, null); + } catch (Exception ex) { + throw new JwtException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, + "Failed to select a JWK signing key -> " + ex.getMessage()), ex); + } + + if (jwks.size() > 1) { + throw new JwtException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, + "Found multiple JWK signing keys for algorithm '" + jwsHeader.get("alg") + "'")); + } + + if (jwks.isEmpty()) { + throw new JwtException( + String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, "Failed to select a JWK signing key")); + } + + return jwks.get(0); + } + + private String serialize(Map headers, Map claims, JWK jwk) { + JWSHeader jwsHeader = convertHeader(headers); + JWTClaimsSet jwtClaimsSet = convertClaims(claims); + + JWSSigner jwsSigner = this.jwsSigners.computeIfAbsent(jwk, AadJwtEncoder::createSigner); + + SignedJWT signedJwt = new SignedJWT(jwsHeader, jwtClaimsSet); + try { + signedJwt.sign(jwsSigner); + } catch (JOSEException ex) { + throw new JwtException( + String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, "Failed to sign the JWT -> " + ex.getMessage()), ex); + } + return signedJwt.serialize(); + } + + private static JWKMatcher createJwkMatcher(Map jwsHeader) { + JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse((String) jwsHeader.get("alg")); + + if (JWSAlgorithm.Family.RSA.contains(jwsAlgorithm)) { + // @formatter:off + return new JWKMatcher.Builder() + .keyType(KeyType.forAlgorithm(jwsAlgorithm)) + .keyUses(KeyUse.SIGNATURE, null) + .algorithms(jwsAlgorithm, null) + .x509CertSHA256Thumbprint(Base64URL.from((String) jwsHeader.get("x5t#S256"))) + .build(); + // @formatter:on + } + return null; + } + + private static JWSSigner createSigner(JWK jwk) { + try { + return JWS_SIGNER_FACTORY.createJWSSigner(jwk); + } catch (JOSEException ex) { + throw new JwtException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, + "Failed to create a JWS Signer -> " + ex.getMessage()), ex); + } + } + + @SuppressWarnings("unchecked") + private static JWSHeader convertHeader(Map headers) { + JWSHeader.Builder builder = new JWSHeader.Builder(JWSAlgorithm.parse((String) headers.get("alg"))); + Map jwk = (Map) headers.get("jwk"); + if (!CollectionUtils.isEmpty(jwk)) { + try { + builder.jwk(JWK.parse(jwk)); + } catch (Exception ex) { + throw new JwtException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, + "Unable to convert 'jku' JOSE header"), ex); + } + } + String keyId = (String) headers.get("kid"); + if (StringUtils.hasText(keyId)) { + builder.keyID(keyId); + } + + String x509SHA1Thumbprint = (String) headers.get("x5t"); + if (StringUtils.hasText(x509SHA1Thumbprint)) { + builder.x509CertThumbprint(new Base64URL(x509SHA1Thumbprint)); + } + + String type = (String) headers.get("typ"); + if (StringUtils.hasText(type)) { + builder.type(new JOSEObjectType(type)); + } + + Map customHeaders = new HashMap<>(); + headers.forEach((name, value) -> { + if (!JWSHeader.getRegisteredParameterNames().contains(name)) { + customHeaders.put(name, value); + } + }); + if (!customHeaders.isEmpty()) { + builder.customParams(customHeaders); + } + + return builder.build(); + } + + @SuppressWarnings("unchecked") + private static JWTClaimsSet convertClaims(Map claims) { + JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder(); + Object issuer = claims.get(JwtClaimNames.ISS); + if (issuer != null) { + builder.issuer(issuer.toString()); + } + + String subject = (String) claims.get(JwtClaimNames.SUB); + if (StringUtils.hasText(subject)) { + builder.subject(subject); + } + + List audience = (List) claims.get(JwtClaimNames.AUD); + if (!CollectionUtils.isEmpty(audience)) { + builder.audience(audience); + } + + Instant expiresAt = (Instant) claims.get(JwtClaimNames.EXP); + if (expiresAt != null) { + builder.expirationTime(Date.from(expiresAt)); + } + + Instant notBefore = (Instant) claims.get(JwtClaimNames.NBF); + if (notBefore != null) { + builder.notBeforeTime(Date.from(notBefore)); + } + + Instant issuedAt = (Instant) claims.get(JwtClaimNames.IAT); + if (issuedAt != null) { + builder.issueTime(Date.from(issuedAt)); + } + + String jwtId = (String) claims.get(JwtClaimNames.JTI); + if (StringUtils.hasText(jwtId)) { + builder.jwtID(jwtId); + } + + Map customClaims = new HashMap<>(); + claims.forEach((name, value) -> { + if (!JWTClaimsSet.getRegisteredNames().contains(name)) { + customClaims.put(name, value); + } + }); + if (!customClaims.isEmpty()) { + customClaims.forEach(builder::claim); + } + + return builder.build(); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/oauth2/AadOAuth2AuthenticatedPrincipal.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/oauth2/AadOAuth2AuthenticatedPrincipal.java index c679d63d3aeb6..c17dc3284e295 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/oauth2/AadOAuth2AuthenticatedPrincipal.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/oauth2/AadOAuth2AuthenticatedPrincipal.java @@ -2,10 +2,12 @@ // Licensed under the MIT License. package com.azure.spring.cloud.autoconfigure.aad.implementation.oauth2; +import com.azure.spring.cloud.autoconfigure.aad.AadResourceServerWebSecurityConfigurerAdapter; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.JWTClaimsSet.Builder; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.util.Assert; import java.io.Serializable; @@ -18,7 +20,10 @@ /** * Entity class of AADOAuth2AuthenticatedPrincipal + * + * @deprecated use the default converter {@link JwtAuthenticationConverter} instead in {@link AadResourceServerWebSecurityConfigurerAdapter}. */ +@Deprecated public class AadOAuth2AuthenticatedPrincipal implements OAuth2AuthenticatedPrincipal, Serializable { private static final long serialVersionUID = -3625690847771476854L; diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/oauth2/AadOAuth2ClientAuthenticationJwkResolver.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/oauth2/AadOAuth2ClientAuthenticationJwkResolver.java new file mode 100644 index 0000000000000..e9dda163d104a --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/oauth2/AadOAuth2ClientAuthenticationJwkResolver.java @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.aad.implementation.oauth2; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.util.Base64URL; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.util.Assert; + +import java.io.FileInputStream; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPublicKey; +import java.util.UUID; + +/** + * An {@link RSAKey} resolver implementation parses the certificate locally. + * + * @since 4.3.0 + * @see Certificate credentials + */ +public class AadOAuth2ClientAuthenticationJwkResolver implements OAuth2ClientAuthenticationJwkResolver { + + private static final Logger LOGGER = LoggerFactory.getLogger(AadOAuth2ClientAuthenticationJwkResolver.class); + + private final String clientCertificatePath; + private final String clientCertificatePassword; + + /** + * Creates a new instance of {@link AadOAuth2ClientAuthenticationJwkResolver} + * @param clientCertificatePath the client certificate path + * @param clientCertificatePassword the client certificate password + */ + public AadOAuth2ClientAuthenticationJwkResolver(String clientCertificatePath, + String clientCertificatePassword) { + Assert.notNull(clientCertificatePath, "clientCertificatePath cannot be null"); + Assert.notNull(clientCertificatePassword, "clientCertificatePassword cannot be null"); + + String fileExtension = clientCertificatePath.substring(clientCertificatePath.lastIndexOf(".") + 1); + Assert.isTrue("pfx".equals(fileExtension) || "p12".equals(fileExtension), + "Only files with the '.pfx' or '.p12' extension are supported."); + + this.clientCertificatePath = clientCertificatePath; + this.clientCertificatePassword = clientCertificatePassword; + } + + @Override + public JWK resolve(ClientRegistration clientRegistration) { + if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.equals(clientRegistration.getClientAuthenticationMethod())) { + try (FileInputStream inputStream = new FileInputStream(clientCertificatePath)) { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + char[] password = clientCertificatePassword.toCharArray(); + keyStore.load(inputStream, password); + String alias = keyStore.aliases().nextElement(); + PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, password); + X509Certificate x509Certificate = (X509Certificate) keyStore.getCertificate(alias); + PublicKey publicKey = x509Certificate.getPublicKey(); + return new RSAKey.Builder((RSAPublicKey) publicKey) + .privateKey(privateKey) + .x509CertThumbprint(Base64URL.encode(getX5t(x509Certificate))) + .keyID(UUID.randomUUID().toString()) + .build(); + } catch (KeyStoreException | IOException | NoSuchAlgorithmException + | CertificateException | UnrecoverableKeyException e) { + LOGGER.error("Resolve RSAKey exception.", e); + } + } + return null; + } + + private byte[] getX5t(X509Certificate cert) + throws NoSuchAlgorithmException, CertificateEncodingException { + return getSHA1Byte(cert.getEncoded()); + } + + private byte[] getSHA1Byte(byte[] data) throws NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.update(data); + return digest.digest(); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/oauth2/OAuth2ClientAuthenticationJwkResolver.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/oauth2/OAuth2ClientAuthenticationJwkResolver.java new file mode 100644 index 0000000000000..43011e3ba858e --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/oauth2/OAuth2ClientAuthenticationJwkResolver.java @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.aad.implementation.oauth2; + +import com.nimbusds.jose.jwk.JWK; +import org.springframework.security.oauth2.client.registration.ClientRegistration; + +/** + * Resolver interface to resolve a {@link JWK} implementation through a {@link ClientRegistration}. + * @since 4.3.0 + */ +@FunctionalInterface +public interface OAuth2ClientAuthenticationJwkResolver { + + /** + * @param clientRegistration the client registration. + * @return a a {@link JWK}. + */ + JWK resolve(ClientRegistration clientRegistration); +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapi/AadJwtBearerGrantRequestEntityConverter.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapi/AadJwtBearerGrantRequestEntityConverter.java new file mode 100644 index 0000000000000..fffaf601ceb80 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapi/AadJwtBearerGrantRequestEntityConverter.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.aad.implementation.webapi; + +import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest; +import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequestEntityConverter; +import org.springframework.util.MultiValueMap; + +/** + * This is a special JWT Bearer flow implementation for Microsoft identify platform. + * + * @since 4.3.0 + * @see OAuth 2.0 On-Behalf-Of + */ +public class AadJwtBearerGrantRequestEntityConverter extends JwtBearerGrantRequestEntityConverter { + + @Override + protected MultiValueMap createParameters(JwtBearerGrantRequest jwtBearerGrantRequest) { + MultiValueMap parameters = super.createParameters(jwtBearerGrantRequest); + parameters.add("requested_token_use", "on_behalf_of"); + return parameters; + } +} + diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapi/AadOboOAuth2AuthorizedClientProvider.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapi/AadOboOAuth2AuthorizedClientProvider.java index 3f81fbe5ac2d7..9a28e5cf27acf 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapi/AadOboOAuth2AuthorizedClientProvider.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapi/AadOboOAuth2AuthorizedClientProvider.java @@ -3,7 +3,6 @@ package com.azure.spring.cloud.autoconfigure.aad.implementation.webapi; import com.azure.spring.cloud.autoconfigure.aad.implementation.constants.Constants; -import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType; import com.microsoft.aad.msal4j.ClientCredentialFactory; import com.microsoft.aad.msal4j.ConfidentialClientApplication; import com.microsoft.aad.msal4j.IClientSecret; @@ -19,11 +18,13 @@ import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.JwtBearerOAuth2AuthorizedClientProvider; import org.springframework.security.oauth2.client.OAuth2AuthorizationContext; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.AbstractOAuth2Token; +import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; @@ -45,18 +46,21 @@ import java.util.Optional; import java.util.concurrent.ExecutionException; +import static com.azure.spring.cloud.autoconfigure.aad.implementation.constants.Constants.ON_BEHALF_OF; + /** * A strategy for authorizing (or re-authorizing) an OAuth 2.0 Client. This implementations implement {@link - * AadAuthorizationGrantType "on_behalf_of" authorization grant type}. + * Constants#ON_BEHALF_OF "on_behalf_of" authorization grant type}. * - * @see AadAuthorizationGrantType + * @see AuthorizationGrantType * @see OAuth2AuthorizedClientProvider + * @deprecated use {@link JwtBearerOAuth2AuthorizedClientProvider} instead. */ +@Deprecated public class AadOboOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider { private static final Logger LOGGER = LoggerFactory.getLogger(AadOboOAuth2AuthorizedClientProvider.class); - private final Clock clock = Clock.systemUTC(); private final Duration clockSkew = Duration.ofSeconds(60); @@ -78,8 +82,7 @@ public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { Assert.notNull(context, "context cannot be null"); ClientRegistration clientRegistration = context.getClientRegistration(); - if (!AadAuthorizationGrantType.ON_BEHALF_OF - .isSameGrantType(clientRegistration.getAuthorizationGrantType())) { + if (!ON_BEHALF_OF.equals(clientRegistration.getAuthorizationGrantType())) { return null; } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapp/AadAzureDelegatedOAuth2AuthorizedClientProvider.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapp/AadAzureDelegatedOAuth2AuthorizedClientProvider.java index 1dc095426ae96..b952a61169c2c 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapp/AadAzureDelegatedOAuth2AuthorizedClientProvider.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapp/AadAzureDelegatedOAuth2AuthorizedClientProvider.java @@ -3,7 +3,7 @@ package com.azure.spring.cloud.autoconfigure.aad.implementation.webapp; -import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType; +import com.azure.spring.cloud.autoconfigure.aad.implementation.constants.Constants; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; import org.springframework.security.oauth2.client.OAuth2AuthorizationContext; @@ -12,6 +12,7 @@ import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2Token; import org.springframework.util.Assert; @@ -26,14 +27,15 @@ import java.util.Optional; import static com.azure.spring.cloud.autoconfigure.aad.AadClientRegistrationRepository.AZURE_CLIENT_REGISTRATION_ID; +import static com.azure.spring.cloud.autoconfigure.aad.implementation.constants.Constants.AZURE_DELEGATED; /** * A strategy for authorizing (or re-authorizing) an OAuth 2.0 Client. This implementation implement {@link - * AadAuthorizationGrantType "azure_delegated" authorization grant type}. + * Constants#AZURE_DELEGATED "azure_delegated" authorization grant type}. * * @see OAuth2AuthorizedClient * @see OAuth2AuthorizationContext - * @see AadAuthorizationGrantType + * @see AuthorizationGrantType * @see Section 1.3 Authorization Grant * @since 3.8.0 */ @@ -74,8 +76,7 @@ public AadAzureDelegatedOAuth2AuthorizedClientProvider( public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { Assert.notNull(context, "context cannot be null"); ClientRegistration clientRegistration = context.getClientRegistration(); - if (!AadAuthorizationGrantType.AZURE_DELEGATED.isSameGrantType( - clientRegistration.getAuthorizationGrantType())) { + if (!AZURE_DELEGATED.equals(clientRegistration.getAuthorizationGrantType())) { return null; } OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient(); diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/properties/AadAuthenticationProperties.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/properties/AadAuthenticationProperties.java index 48cfbbdd1bafb..e598b49e5daf2 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/properties/AadAuthenticationProperties.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/properties/AadAuthenticationProperties.java @@ -8,6 +8,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.util.StringUtils; import java.time.Duration; @@ -23,13 +24,15 @@ import java.util.stream.Stream; import static com.azure.spring.cloud.autoconfigure.aad.AadClientRegistrationRepository.AZURE_CLIENT_REGISTRATION_ID; +import static com.azure.spring.cloud.autoconfigure.aad.implementation.constants.Constants.AZURE_DELEGATED; +import static com.azure.spring.cloud.autoconfigure.aad.implementation.constants.Constants.ON_BEHALF_OF; import static com.azure.spring.cloud.autoconfigure.aad.properties.AadApplicationType.RESOURCE_SERVER; import static com.azure.spring.cloud.autoconfigure.aad.properties.AadApplicationType.RESOURCE_SERVER_WITH_OBO; import static com.azure.spring.cloud.autoconfigure.aad.properties.AadApplicationType.WEB_APPLICATION; import static com.azure.spring.cloud.autoconfigure.aad.properties.AadApplicationType.inferApplicationTypeByDependencies; -import static com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType.AUTHORIZATION_CODE; -import static com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType.AZURE_DELEGATED; -import static com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType.ON_BEHALF_OF; +import static org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE; +import static org.springframework.security.oauth2.core.AuthorizationGrantType.JWT_BEARER; +import static org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_JWT; /** * Configuration properties for Azure Active Directory Authentication. @@ -134,15 +137,15 @@ public class AadAuthenticationProperties implements InitializingBean { */ private AadApplicationType applicationType; - private static final Map> NON_COMPATIBLE_APPLICATION_TYPE_AND_GRANT_TYPES = initCompatibleApplicationTypeAndGrantTypes(); + private static final Map> NON_COMPATIBLE_APPLICATION_TYPE_AND_GRANT_TYPES = initCompatibleApplicationTypeAndGrantTypes(); - private static Map> initCompatibleApplicationTypeAndGrantTypes() { - Map> nonCompatibleApplicationTypeAndGrantTypes = + private static Map> initCompatibleApplicationTypeAndGrantTypes() { + Map> nonCompatibleApplicationTypeAndGrantTypes = new HashMap<>(); nonCompatibleApplicationTypeAndGrantTypes.put(WEB_APPLICATION, - Stream.of(ON_BEHALF_OF).collect(Collectors.toSet())); + Stream.of(ON_BEHALF_OF, JWT_BEARER).collect(Collectors.toSet())); nonCompatibleApplicationTypeAndGrantTypes.put(RESOURCE_SERVER, - Stream.of(AUTHORIZATION_CODE, ON_BEHALF_OF).collect(Collectors.toSet())); + Stream.of(AUTHORIZATION_CODE, ON_BEHALF_OF, JWT_BEARER).collect(Collectors.toSet())); nonCompatibleApplicationTypeAndGrantTypes.put(RESOURCE_SERVER_WITH_OBO, Stream.of(AUTHORIZATION_CODE).collect(Collectors.toSet())); @@ -590,22 +593,25 @@ private boolean isValidApplicationType(AadApplicationType configured, AadApplica private void validateAuthorizationClientProperties(String registrationId, AuthorizationClientProperties properties) { - AadAuthorizationGrantType grantType = Optional.of(properties) - .map(AuthorizationClientProperties::getAuthorizationGrantType) - .orElse(null); + if (CLIENT_SECRET_JWT.equals(properties.getClientAuthenticationMethod())) { + throw new IllegalStateException("The client authentication method of '" + + registrationId + "' is not supported."); + } + + AuthorizationGrantType grantType = properties.getAuthorizationGrantType(); if (grantType != null) { validateAuthorizationGrantType(registrationId, grantType); } else { grantType = decideDefaultGrantTypeFromApplicationType(registrationId, applicationType); properties.setAuthorizationGrantType(grantType); - LOGGER.debug("The client '{}' sets the default value of AADAuthorizationGrantType to '{}'.", grantType, + LOGGER.debug("The client '{}' sets the default value of AuthorizationGrantType to '{}'.", grantType, registrationId); } // Extract validated scopes from properties List scopes = extractValidatedScopes(registrationId, properties); - addNecessaryScopesForAuhtorizationCodeClients(properties, scopes); + addNecessaryScopesForAuthorizationCodeClients(registrationId, properties, scopes); } /** @@ -617,11 +623,17 @@ private void validateAuthorizationClientProperties(String registrationId, * "openid" : allows to request an ID token. * "profile" : allows returning additional claims in the ID token. * "offline_access" : allows to request a refresh token. + * @param registrationId client ID * @param properties AuthorizationClientProperties * @param scopes scopes for authorization_code clients. */ - private void addNecessaryScopesForAuhtorizationCodeClients(AuthorizationClientProperties properties, + private void addNecessaryScopesForAuthorizationCodeClients(String registrationId, + AuthorizationClientProperties properties, List scopes) { + if (AZURE_CLIENT_REGISTRATION_ID.equals(registrationId) && (scopes == null || scopes.isEmpty())) { + return; + } + if (properties.getAuthorizationGrantType().equals(AUTHORIZATION_CODE)) { String[] scopesNeeded = new String[] { "openid", "profile", "offline_access" }; for (String scope : scopesNeeded) { @@ -634,7 +646,7 @@ private void addNecessaryScopesForAuhtorizationCodeClients(AuthorizationClientPr private List extractValidatedScopes(String registrationId, AuthorizationClientProperties properties) { List scopes = properties.getScopes(); - if (scopes == null || scopes.isEmpty()) { + if (!AZURE_CLIENT_REGISTRATION_ID.equals(registrationId) && (scopes == null || scopes.isEmpty())) { throw new IllegalStateException( "'spring.cloud.azure.active-directory.authorization-clients." + registrationId + ".scopes' must be " + "configured"); @@ -642,11 +654,11 @@ private List extractValidatedScopes(String registrationId, Authorization return scopes; } - private void validateAuthorizationGrantType(String registrationId, AadAuthorizationGrantType grantType) { + private void validateAuthorizationGrantType(String registrationId, AuthorizationGrantType grantType) { if (NON_COMPATIBLE_APPLICATION_TYPE_AND_GRANT_TYPES.containsKey(applicationType)) { if (NON_COMPATIBLE_APPLICATION_TYPE_AND_GRANT_TYPES.get(applicationType).contains(grantType)) { throw new IllegalStateException(String.format(UNMATCHING_OAUTH_GRANT_TYPE_FROMAT, - applicationType.getValue(), registrationId, grantType)); + applicationType.getValue(), registrationId, grantType.getValue())); } LOGGER.debug("'spring.cloud.azure.active-directory.authorization-clients.{}.authorization-grant-type'" + " is valid.", registrationId); @@ -666,9 +678,9 @@ private void validateAuthorizationGrantType(String registrationId, AadAuthorizat * @param appType AadApplicationType * @return default grant type */ - private AadAuthorizationGrantType decideDefaultGrantTypeFromApplicationType(String registrationId, - AadApplicationType appType) { - AadAuthorizationGrantType grantType; + private AuthorizationGrantType decideDefaultGrantTypeFromApplicationType(String registrationId, + AadApplicationType appType) { + AuthorizationGrantType grantType; switch (appType) { case WEB_APPLICATION: if (registrationId.equals(AZURE_CLIENT_REGISTRATION_ID)) { @@ -679,7 +691,7 @@ private AadAuthorizationGrantType decideDefaultGrantTypeFromApplicationType(Stri break; case RESOURCE_SERVER: case RESOURCE_SERVER_WITH_OBO: - grantType = AadAuthorizationGrantType.ON_BEHALF_OF; + grantType = JWT_BEARER; break; case WEB_APPLICATION_AND_RESOURCE_SERVER: throw new IllegalStateException("spring.cloud.azure.active-directory.authorization-clients." + registrationId diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/properties/AadAuthorizationGrantType.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/properties/AadAuthorizationGrantType.java index 13beece11bdfa..196e11626da28 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/properties/AadAuthorizationGrantType.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/properties/AadAuthorizationGrantType.java @@ -6,7 +6,10 @@ /** * Defines grant types: client_credentials, authorization_code, on_behalf_of, azure_delegated. + * + * @deprecated use {@link AuthorizationGrantType} instead. */ +@Deprecated public enum AadAuthorizationGrantType { /** diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/properties/AadCredentialProperties.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/properties/AadCredentialProperties.java index 01fe2ac00abb6..8d03b0abc457a 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/properties/AadCredentialProperties.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/properties/AadCredentialProperties.java @@ -18,6 +18,16 @@ public class AadCredentialProperties { */ private String clientSecret; + /** + * Path of a PFX or P12 certificate file to use when performing service principal authentication with Azure. + */ + private String clientCertificatePath; + + /** + * Password of the certificate file. + */ + private String clientCertificatePassword; + /** * * @return The client ID. @@ -49,4 +59,32 @@ public String getClientSecret() { public void setClientSecret(String clientSecret) { this.clientSecret = clientSecret; } + + /** + * @return The client certificate path. + */ + public String getClientCertificatePath() { + return clientCertificatePath; + } + + /** + * @param clientCertificatePath The client certificate path. + */ + public void setClientCertificatePath(String clientCertificatePath) { + this.clientCertificatePath = clientCertificatePath; + } + + /** + * @return The client certificate password. + */ + public String getClientCertificatePassword() { + return clientCertificatePassword; + } + + /** + * @param clientCertificatePassword The client certificate password. + */ + public void setClientCertificatePassword(String clientCertificatePassword) { + this.clientCertificatePassword = clientCertificatePassword; + } } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/properties/AuthorizationClientProperties.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/properties/AuthorizationClientProperties.java index 6586b407be80d..607cbb77c6c06 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/properties/AuthorizationClientProperties.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/properties/AuthorizationClientProperties.java @@ -3,6 +3,9 @@ package com.azure.spring.cloud.autoconfigure.aad.properties; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; + import java.util.List; /** @@ -12,14 +15,16 @@ public class AuthorizationClientProperties { private List scopes; - private AadAuthorizationGrantType authorizationGrantType; + private AuthorizationGrantType authorizationGrantType; + + private ClientAuthenticationMethod clientAuthenticationMethod; /** * Gets the authorization grant type. * * @return the authorization grant type */ - public AadAuthorizationGrantType getAuthorizationGrantType() { + public AuthorizationGrantType getAuthorizationGrantType() { return authorizationGrantType; } @@ -28,7 +33,7 @@ public AadAuthorizationGrantType getAuthorizationGrantType() { * * @param authorizationGrantType the authorization grant type */ - public void setAuthorizationGrantType(AadAuthorizationGrantType authorizationGrantType) { + public void setAuthorizationGrantType(AuthorizationGrantType authorizationGrantType) { this.authorizationGrantType = authorizationGrantType; } @@ -49,4 +54,22 @@ public void setScopes(List scopes) { public List getScopes() { return scopes; } + + /** + * Gets the client authentication method. + * + * @return the client authentication method + */ + public ClientAuthenticationMethod getClientAuthenticationMethod() { + return clientAuthenticationMethod; + } + + /** + * Sets the client authentication method. + * + * @param clientAuthenticationMethod the client authentication method + */ + public void setClientAuthenticationMethod(ClientAuthenticationMethod clientAuthenticationMethod) { + this.clientAuthenticationMethod = clientAuthenticationMethod; + } } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aadb2c/configuration/AadB2cOAuth2ClientConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aadb2c/configuration/AadB2cOAuth2ClientConfiguration.java index c6ad6b4a67e24..f2378e2e70c8c 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aadb2c/configuration/AadB2cOAuth2ClientConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aadb2c/configuration/AadB2cOAuth2ClientConfiguration.java @@ -2,7 +2,6 @@ // Licensed under the MIT License. package com.azure.spring.cloud.autoconfigure.aadb2c.configuration; -import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType; import com.azure.spring.cloud.autoconfigure.aadb2c.implementation.AadB2cClientRegistrationRepository; import com.azure.spring.cloud.autoconfigure.aadb2c.implementation.AadB2cConditions; import com.azure.spring.cloud.autoconfigure.aadb2c.implementation.AadB2cUrl; @@ -29,7 +28,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.stream.Collectors; /** @@ -103,10 +101,7 @@ private ClientRegistration buildUserFlowClientRegistration(Map.Entry client) { - AuthorizationGrantType authGrantType = Optional.ofNullable(client.getValue().getAuthorizationGrantType()) - .map(AadAuthorizationGrantType::getValue) - .map(AuthorizationGrantType::new) - .orElse(null); + AuthorizationGrantType authGrantType = client.getValue().getAuthorizationGrantType(); if (!AuthorizationGrantType.CLIENT_CREDENTIALS.equals(authGrantType)) { LOGGER.warn("The authorization type of the {} client registration is not supported.", client.getKey()); } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aadb2c/properties/AadB2cProperties.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aadb2c/properties/AadB2cProperties.java index d37fb6d226cf2..9fb33aa99e41b 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aadb2c/properties/AadB2cProperties.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aadb2c/properties/AadB2cProperties.java @@ -15,7 +15,7 @@ import java.util.Map; import java.util.Optional; -import static com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType.CLIENT_CREDENTIALS; +import static org.springframework.security.oauth2.core.AuthorizationGrantType.CLIENT_CREDENTIALS; /** * Configuration properties for Azure Active Directory B2C. @@ -143,7 +143,7 @@ private void validateCommonProperties() { long credentialCount = authorizationClients.values() .stream() .map(AuthorizationClientProperties::getAuthorizationGrantType) - .filter(client -> CLIENT_CREDENTIALS == client) + .filter(client -> CLIENT_CREDENTIALS.equals(client)) .count(); if (credentialCount > 0 && !StringUtils.hasText(profile.getTenantId())) { throw new AadB2cConfigurationException("'tenant-id' must be configured " diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aadb2c/properties/AuthorizationClientProperties.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aadb2c/properties/AuthorizationClientProperties.java index e081899ec1fb9..8b100b71c2c14 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aadb2c/properties/AuthorizationClientProperties.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aadb2c/properties/AuthorizationClientProperties.java @@ -3,7 +3,7 @@ package com.azure.spring.cloud.autoconfigure.aadb2c.properties; -import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType; +import org.springframework.security.oauth2.core.AuthorizationGrantType; import java.util.List; @@ -14,14 +14,14 @@ public class AuthorizationClientProperties { private List scopes; - private AadAuthorizationGrantType authorizationGrantType; + private AuthorizationGrantType authorizationGrantType; /** * Gets the authorization grant type. * * @return the authorization grant type */ - public AadAuthorizationGrantType getAuthorizationGrantType() { + public AuthorizationGrantType getAuthorizationGrantType() { return authorizationGrantType; } @@ -30,7 +30,7 @@ public AadAuthorizationGrantType getAuthorizationGrantType() { * * @param authorizationGrantType the authorization grant type */ - public void setAuthorizationGrantType(AadAuthorizationGrantType authorizationGrantType) { + public void setAuthorizationGrantType(AuthorizationGrantType authorizationGrantType) { this.authorizationGrantType = authorizationGrantType; } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/AadClientRegistrationRepositoryTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/AadClientRegistrationRepositoryTests.java index 6f452719df933..24cb381666388 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/AadClientRegistrationRepositoryTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/AadClientRegistrationRepositoryTests.java @@ -3,9 +3,11 @@ package com.azure.spring.cloud.autoconfigure.aad; +import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthenticationProperties; import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationServerEndpoints; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; @@ -19,16 +21,21 @@ import java.util.List; import java.util.Set; -import static com.azure.spring.cloud.autoconfigure.aad.implementation.WebApplicationContextRunnerUtils.oauthClientRunner; -import static com.azure.spring.cloud.autoconfigure.aad.implementation.WebApplicationContextRunnerUtils.webApplicationContextRunner; import static com.azure.spring.cloud.autoconfigure.aad.AadClientRegistrationRepository.AZURE_CLIENT_REGISTRATION_ID; import static com.azure.spring.cloud.autoconfigure.aad.AadClientRegistrationRepository.resourceServerCount; -import static com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType.AUTHORIZATION_CODE; -import static com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType.AZURE_DELEGATED; +import static com.azure.spring.cloud.autoconfigure.aad.implementation.WebApplicationContextRunnerUtils.oauthClientRunner; +import static com.azure.spring.cloud.autoconfigure.aad.implementation.WebApplicationContextRunnerUtils.resourceServerWithOboContextRunner; +import static com.azure.spring.cloud.autoconfigure.aad.implementation.WebApplicationContextRunnerUtils.webApplicationAndResourceServerContextRunner; +import static com.azure.spring.cloud.autoconfigure.aad.implementation.WebApplicationContextRunnerUtils.webApplicationContextRunner; +import static com.azure.spring.cloud.autoconfigure.aad.implementation.constants.Constants.AZURE_DELEGATED; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE; +import static org.springframework.security.oauth2.core.AuthorizationGrantType.JWT_BEARER; +import static org.springframework.security.oauth2.core.ClientAuthenticationMethod.PRIVATE_KEY_JWT; class AadClientRegistrationRepositoryTests { @@ -42,7 +49,7 @@ void noClientsConfiguredTest() { repository.getAzureClientAccessTokenScopes()); ClientRegistration azure = repository.findByRegistrationId(AZURE_CLIENT_REGISTRATION_ID); - assertEquals(AUTHORIZATION_CODE.getValue(), azure.getAuthorizationGrantType().getValue()); + assertEquals(AUTHORIZATION_CODE, azure.getAuthorizationGrantType()); assertEquals(new HashSet<>(Arrays.asList("openid", "profile", "offline_access")), azure.getScopes()); List clients = collectClients(repository); @@ -55,16 +62,17 @@ void noClientsConfiguredTest() { void azureClientConfiguredTest() { webApplicationContextRunner() .withPropertyValues( - "spring.cloud.azure.active-directory.authorization-clients.azure.scopes = Azure.Scope" + "spring.cloud.azure.active-directory.authorization-clients.azure.scopes = Azure.Scope", + "spring.cloud.azure.active-directory.authorization-clients.azure.client-authentication-method = private_key_jwt" ) .run(context -> { AadClientRegistrationRepository repository = (AadClientRegistrationRepository) context.getBean(ClientRegistrationRepository.class); assertEquals(new HashSet<>(Arrays.asList("Azure.Scope", "openid", "profile", "offline_access")), repository.getAzureClientAccessTokenScopes()); - ClientRegistration azure = repository.findByRegistrationId(AZURE_CLIENT_REGISTRATION_ID); - assertEquals(AUTHORIZATION_CODE.getValue(), azure.getAuthorizationGrantType().getValue()); + assertEquals(PRIVATE_KEY_JWT, azure.getClientAuthenticationMethod()); + assertEquals(AUTHORIZATION_CODE, azure.getAuthorizationGrantType()); assertEquals(new HashSet<>(Arrays.asList("Azure.Scope", "openid", "profile", "offline_access")), azure.getScopes()); @@ -73,6 +81,28 @@ void azureClientConfiguredTest() { }); } + @Test + void azureClientInvalidedConfiguredTest() { + webApplicationContextRunner() + .withPropertyValues( + "spring.cloud.azure.active-directory.authorization-clients.azure.client-authentication-method = client_secret_jwt" + ) + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AadAuthenticationProperties.class)) + ); + } + + @Test + void otherClientInvalidedConfiguredTest() { + webApplicationContextRunner() + .withPropertyValues( + "spring.cloud.azure.active-directory.authorization-clients.other.client-authentication-method = client_secret_jwt" + ) + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AadAuthenticationProperties.class)) + ); + } + @Test void graphClientConfiguredTest() { webApplicationContextRunner() @@ -86,12 +116,12 @@ void graphClientConfiguredTest() { repository.getAzureClientAccessTokenScopes()); ClientRegistration azure = repository.findByRegistrationId(AZURE_CLIENT_REGISTRATION_ID); - assertEquals(AUTHORIZATION_CODE.getValue(), azure.getAuthorizationGrantType().getValue()); + assertEquals(AUTHORIZATION_CODE, azure.getAuthorizationGrantType()); assertEquals(new HashSet<>(Arrays.asList("Graph.Scope", "openid", "profile", "offline_access")), azure.getScopes()); ClientRegistration graph = repository.findByRegistrationId("graph"); - assertEquals(AZURE_DELEGATED.getValue(), graph.getAuthorizationGrantType().getValue()); + assertEquals(AZURE_DELEGATED, graph.getAuthorizationGrantType()); assertEquals(new HashSet<>(Collections.singletonList("Graph.Scope")), graph.getScopes()); List clients = collectClients(repository); @@ -113,12 +143,12 @@ void authorizationCodeGraphClientConfiguredTest() { repository.getAzureClientAccessTokenScopes()); ClientRegistration azure = repository.findByRegistrationId(AZURE_CLIENT_REGISTRATION_ID); - assertEquals(AUTHORIZATION_CODE.getValue(), azure.getAuthorizationGrantType().getValue()); + assertEquals(AUTHORIZATION_CODE, azure.getAuthorizationGrantType()); assertEquals(new HashSet<>(Arrays.asList("openid", "profile", "offline_access")), azure.getScopes()); ClientRegistration graph = repository.findByRegistrationId("graph"); - assertEquals(AUTHORIZATION_CODE.getValue(), graph.getAuthorizationGrantType().getValue()); + assertEquals(AUTHORIZATION_CODE, graph.getAuthorizationGrantType()); assertEquals(new HashSet<>(Arrays.asList("Graph.Scope", "openid", "profile", "offline_access")), graph.getScopes()); @@ -127,6 +157,25 @@ void authorizationCodeGraphClientConfiguredTest() { }); } + @Test + void clientWithJwtBearerAuthorizationGrantType() { + resourceServerWithOboContextRunner() + .withPropertyValues( + "spring.cloud.azure.active-directory.authorization-clients.webapi1.authorization-grant-type = on_behalf_of", + "spring.cloud.azure.active-directory.authorization-clients.webapi1.scopes = Test", + "spring.cloud.azure.active-directory.authorization-clients.webapi2.authorization-grant-type = urn:ietf:params:oauth:grant-type:jwt-bearer", + "spring.cloud.azure.active-directory.authorization-clients.webapi2.scopes = Test" + ) + .run(context -> { + ClientRegistrationRepository repository = context.getBean(ClientRegistrationRepository.class); + ClientRegistration webapi1 = repository.findByRegistrationId("webapi1"); + assertEquals(JWT_BEARER, webapi1.getAuthorizationGrantType()); + + ClientRegistration webapi2 = repository.findByRegistrationId("webapi2"); + assertEquals(JWT_BEARER, webapi2.getAuthorizationGrantType()); + }); + } + @Test void clientWithClientCredentialsPermissions() { webApplicationContextRunner() @@ -137,7 +186,7 @@ void clientWithClientCredentialsPermissions() { .run(context -> { ClientRegistrationRepository repository = context.getBean(ClientRegistrationRepository.class); assertEquals(repository.findByRegistrationId(AZURE_CLIENT_REGISTRATION_ID).getAuthorizationGrantType(), - AuthorizationGrantType.AUTHORIZATION_CODE); + AUTHORIZATION_CODE); assertEquals(repository.findByRegistrationId("graph").getAuthorizationGrantType(), AuthorizationGrantType.CLIENT_CREDENTIALS); }); @@ -257,6 +306,27 @@ void haveResourceServerScopeInAccessTokenWhenThereAreMultiResourceServerScopesIn }); } + @Test + void rewriteAuthorizationGrantTypeWhenIsOnBehalfOf() { + rewriteAuthorizationGrantTypeWhenIsOnBehalfOfByRunner(resourceServerWithOboContextRunner()); + rewriteAuthorizationGrantTypeWhenIsOnBehalfOfByRunner(webApplicationAndResourceServerContextRunner()); + } + + private void rewriteAuthorizationGrantTypeWhenIsOnBehalfOfByRunner(WebApplicationContextRunner runner) { + runner + .withPropertyValues( + "spring.cloud.azure.active-directory.authorization-clients.graph.scopes = fakeValue:/.default", + "spring.cloud.azure.active-directory.authorization-clients.graph.authorizationGrantType = on_behalf_of" + ) + .run(context -> { + AadClientRegistrationRepository repository = context.getBean(AadClientRegistrationRepository.class); + assertNotNull(repository); + ClientRegistration graph = repository.findByRegistrationId("graph"); + assertNotNull(graph); + assertTrue(JWT_BEARER.equals(graph.getAuthorizationGrantType())); + }); + } + // TODO (moary) Enable this test. // Related issue: https://github.com/Azure/azure-sdk-for-java/issues/23154 @Disabled diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/filter/AadAuthenticationFilterPropertiesTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/filter/AadAuthenticationFilterPropertiesTests.java index 9d551c94213cb..16e2288800a9f 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/filter/AadAuthenticationFilterPropertiesTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/filter/AadAuthenticationFilterPropertiesTests.java @@ -40,6 +40,8 @@ public void canSetProperties() { assertThat(properties.getCredential().getClientId()).isEqualTo(TestConstants.CLIENT_ID); assertThat(properties.getCredential().getClientSecret()).isEqualTo(TestConstants.CLIENT_SECRET); + assertThat(properties.getCredential().getClientCertificatePath()).isEqualTo(TestConstants.CLIENT_CERTIFICATE_PATH); + assertThat(properties.getCredential().getClientCertificatePassword()).isEqualTo(TestConstants.CLIENT_CERTIFICATE_PASSWORD); assertThat(properties.getUserGroup().getAllowedGroupNames() .toString()).isEqualTo(TestConstants.TARGETED_GROUPS.toString()); } @@ -51,6 +53,8 @@ private void configureAllRequiredProperties(AnnotationConfigApplicationContext c AAD_PROPERTY_PREFIX + "profile.tenant-id=demo-tenant-id", AAD_PROPERTY_PREFIX + "credential.client-id=" + TestConstants.CLIENT_ID, AAD_PROPERTY_PREFIX + "credential.client-secret=" + TestConstants.CLIENT_SECRET, + AAD_PROPERTY_PREFIX + "credential.client-certificate-path=" + TestConstants.CLIENT_CERTIFICATE_PATH, + AAD_PROPERTY_PREFIX + "credential.client-certificate-password=" + TestConstants.CLIENT_CERTIFICATE_PASSWORD, AAD_PROPERTY_PREFIX + "user-group.allowed-group-names=" + TestConstants.TARGETED_GROUPS.toString().replace("[", "").replace("]", "") ); diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/filter/TestConstants.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/filter/TestConstants.java index 2bb24631d8559..cf26ca27b1d25 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/filter/TestConstants.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/filter/TestConstants.java @@ -10,6 +10,8 @@ public class TestConstants { static final String CLIENT_ID = "real_client_id"; static final String CLIENT_SECRET = "real_client_secret"; + static final String CLIENT_CERTIFICATE_PATH = "client_certificate_path"; + static final String CLIENT_CERTIFICATE_PASSWORD = "client_certificate_password"; static final List TARGETED_GROUPS = Arrays.asList("group1", "group2", "group3"); static final String TOKEN_HEADER = "Authorization"; diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/TestClientRegistrations.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/TestClientRegistrations.java new file mode 100644 index 0000000000000..d6f512bacf2a4 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/TestClientRegistrations.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.aad.implementation; + +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; + +public class TestClientRegistrations { + + public static ClientRegistration.Builder clientRegistration(AuthorizationGrantType grantType, + ClientAuthenticationMethod method) { + return ClientRegistration + .withRegistrationId("test") + .clientId("test") + .clientSecret("test-secret") + .clientAuthenticationMethod(method) + .authorizationGrantType(grantType) + .tokenUri("http://localhost/token"); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/TestJwks.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/TestJwks.java new file mode 100644 index 0000000000000..5dca4d976bd8e --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/TestJwks.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.aad.implementation; + +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.util.Base64URL; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +public final class TestJwks { + + // @formatter:off + public static final RSAKey DEFAULT_RSA_JWK = + jwk( + TestKeys.DEFAULT_PUBLIC_KEY, + TestKeys.DEFAULT_PRIVATE_KEY, + TestKeys.DEFAULT_CERTIFICATE + ).build(); + // @formatter:on + + private TestJwks() { + } + + public static RSAKey.Builder jwk(RSAPublicKey publicKey, RSAPrivateKey privateKey, X509Certificate cert) { + // @formatter:off + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.update(cert.getEncoded()); + byte[] bytes = digest.digest(); + return new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .x509CertThumbprint(Base64URL.encode(bytes)) + .keyID("rsa-jwk-kid"); + } catch (CertificateEncodingException | NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return null; + // @formatter:on + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/TestJwtClaimsSets.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/TestJwtClaimsSets.java new file mode 100644 index 0000000000000..e48a36d9372df --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/TestJwtClaimsSets.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.aad.implementation; + +import org.springframework.security.oauth2.jwt.JwtClaimNames; + +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public final class TestJwtClaimsSets { + + private TestJwtClaimsSets() { + + } + + public static Map jwtClaimsSet() { + Map jwtClaimsSet = new HashMap<>(); + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plus(Duration.ofSeconds(60L)); + jwtClaimsSet.put(JwtClaimNames.ISS, "https://localhost"); + jwtClaimsSet.put(JwtClaimNames.SUB, "test"); + jwtClaimsSet.put(JwtClaimNames.AUD, Collections.singletonList("client-1")); + jwtClaimsSet.put(JwtClaimNames.JTI, UUID.randomUUID().toString()); + jwtClaimsSet.put(JwtClaimNames.IAT, issuedAt); + jwtClaimsSet.put(JwtClaimNames.EXP, expiresAt); + return jwtClaimsSet; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/TestKeys.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/TestKeys.java new file mode 100644 index 0000000000000..53b344c8ef98f --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/TestKeys.java @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.aad.implementation; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + + +public final class TestKeys { + + public static final KeyFactory KF; + static { + try { + KF = KeyFactory.getInstance("RSA"); + } catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException(ex); + } + } + + // @formatter:off + public static final String DEFAULT_RSA_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3FlqJr5TRskIQIgdE3Dd" + + "7D9lboWdcTUT8a+fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRv" + + "c5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4/1tfRgG6ii4Uhxh6" + + "iI8qNMJQX+fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2" + + "kJdJ/ZIV+WW4noDdzpKqHcwmB8FsrumlVY/DNVvUSDIipiq9PbP4H99TXN1o746o" + + "RaNa07rq1hoCgMSSy+85SagCoxlmyE+D+of9SsMY8Ol9t0rdzpobBuhyJ/o5dfvj" + + "KwIDAQAB"; + // @formatter:on + + public static final RSAPublicKey DEFAULT_PUBLIC_KEY; + static { + X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.getDecoder().decode(DEFAULT_RSA_PUBLIC_KEY)); + try { + DEFAULT_PUBLIC_KEY = (RSAPublicKey) KF.generatePublic(spec); + } catch (InvalidKeySpecException ex) { + throw new IllegalArgumentException(ex); + } + } + + // @formatter:off + public static final String DEFAULT_RSA_PRIVATE_KEY = "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDcWWomvlNGyQhA" + + "iB0TcN3sP2VuhZ1xNRPxr58lHswC9Cbtdc2hiSbe/sxAvU1i0O8vaXwICdzRZ1JM" + + "g1TohG9zkqqjZDhyw1f1Ic6YR/OhE6NCpqERy97WMFeW6gJd1i5inHj/W19GAbqK" + + "LhSHGHqIjyo0wlBf58t+qFt9h/EFBVE/LAGQBsg/jHUQCxsLoVI2aSELGIw2oSDF" + + "oiljwLaQl0n9khX5ZbiegN3OkqodzCYHwWyu6aVVj8M1W9RIMiKmKr09s/gf31Nc" + + "3WjvjqhFo1rTuurWGgKAxJLL7zlJqAKjGWbIT4P6h/1Kwxjw6X23St3OmhsG6HIn" + + "+jl1++MrAgMBAAECggEBAMf820wop3pyUOwI3aLcaH7YFx5VZMzvqJdNlvpg1jbE" + + "E2Sn66b1zPLNfOIxLcBG8x8r9Ody1Bi2Vsqc0/5o3KKfdgHvnxAB3Z3dPh2WCDek" + + "lCOVClEVoLzziTuuTdGO5/CWJXdWHcVzIjPxmK34eJXioiLaTYqN3XKqKMdpD0ZG" + + "mtNTGvGf+9fQ4i94t0WqIxpMpGt7NM4RHy3+Onggev0zLiDANC23mWrTsUgect/7" + + "62TYg8g1bKwLAb9wCBT+BiOuCc2wrArRLOJgUkj/F4/gtrR9ima34SvWUyoUaKA0" + + "bi4YBX9l8oJwFGHbU9uFGEMnH0T/V0KtIB7qetReywkCgYEA9cFyfBIQrYISV/OA" + + "+Z0bo3vh2aL0QgKrSXZ924cLt7itQAHNZ2ya+e3JRlTczi5mnWfjPWZ6eJB/8MlH" + + "Gpn12o/POEkU+XjZZSPe1RWGt5g0S3lWqyx9toCS9ACXcN9tGbaqcFSVI73zVTRA" + + "8J9grR0fbGn7jaTlTX2tnlOTQ60CgYEA5YjYpEq4L8UUMFkuj+BsS3u0oEBnzuHd" + + "I9LEHmN+CMPosvabQu5wkJXLuqo2TxRnAznsA8R3pCLkdPGoWMCiWRAsCn979TdY" + + "QbqO2qvBAD2Q19GtY7lIu6C35/enQWzJUMQE3WW0OvjLzZ0l/9mA2FBRR+3F9A1d" + + "rBdnmv0c3TcCgYEAi2i+ggVZcqPbtgrLOk5WVGo9F1GqUBvlgNn30WWNTx4zIaEk" + + "HSxtyaOLTxtq2odV7Kr3LGiKxwPpn/T+Ief+oIp92YcTn+VfJVGw4Z3BezqbR8lA" + + "Uf/+HF5ZfpMrVXtZD4Igs3I33Duv4sCuqhEvLWTc44pHifVloozNxYfRfU0CgYBN" + + "HXa7a6cJ1Yp829l62QlJKtx6Ymj95oAnQu5Ez2ROiZMqXRO4nucOjGUP55Orac1a" + + "FiGm+mC/skFS0MWgW8evaHGDbWU180wheQ35hW6oKAb7myRHtr4q20ouEtQMdQIF" + + "snV39G1iyqeeAsf7dxWElydXpRi2b68i3BIgzhzebQKBgQCdUQuTsqV9y/JFpu6H" + + "c5TVvhG/ubfBspI5DhQqIGijnVBzFT//UfIYMSKJo75qqBEyP2EJSmCsunWsAFsM" + + "TszuiGTkrKcZy9G0wJqPztZZl2F2+bJgnA6nBEV7g5PA4Af+QSmaIhRwqGDAuROR" + + "47jndeyIaMTNETEmOnms+as17g=="; + // @formatter:on + + public static final RSAPrivateKey DEFAULT_PRIVATE_KEY; + static { + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(DEFAULT_RSA_PRIVATE_KEY)); + try { + DEFAULT_PRIVATE_KEY = (RSAPrivateKey) KF.generatePrivate(spec); + } catch (InvalidKeySpecException ex) { + throw new IllegalArgumentException(ex); + } + } + + // @formatter:off + public static final String DEFAULT_CERTIFICATE_KEY = "-----BEGIN CERTIFICATE-----\n" + + "MIIDzzCCAregAwIBAgIUEc1Q6X2u3x6iGQ7RjVeDJIjQ1XMwDQYJKoZIhvcNAQEL\n" + + "BQAwdzELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjEPMA0GA1UEBwwGTG9u\n" + + "ZG9uMRgwFgYDVQQKDA9HbG9iYWwgU2VjdXJpdHkxFjAUBgNVBAsMDUlUIERlcGFy\n" + + "dG1lbnQxFDASBgNVBAMMC2V4YW1wbGUuY29tMB4XDTIyMDUzMDA5NDE1N1oXDTIz\n" + + "MDUzMDA5NDE1N1owdzELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjEPMA0G\n" + + "A1UEBwwGTG9uZG9uMRgwFgYDVQQKDA9HbG9iYWwgU2VjdXJpdHkxFjAUBgNVBAsM\n" + + "DUlUIERlcGFydG1lbnQxFDASBgNVBAMMC2V4YW1wbGUuY29tMIIBIjANBgkqhkiG\n" + + "9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3OG2pj0NhQhpZhajw4i4Viq4ys8rjqMSn8uF\n" + + "eMlpYflRCSBrWi3/+ll+th/vdQRoE/N2hq6oMIoZ1oAXsXEeCIcw2vJrU+2Fd0N8\n" + + "D9CFQIHatFUQsJmIhih7bv7DzFTpHOVU7tzzs4WVDnqJfMsMHCGh+oAx1nBEN5LS\n" + + "1nsQoDBw32fW6mcQnmD+aWByeb9rHpSO2+E1XotRYnSzsMJY2tuLyIqH8647fXq+\n" + + "9K/olDgtGsha+TDJ1CX6UGS28zQ3BCaj7h54+y7LqOtFdw8ICn6Hoj39mNEtVlxC\n" + + "4hUFLLvDphMFC7/+WNuYCq9iIr7xJCj6c+1mTLNduZ30UuEk1QIDAQABo1MwUTAd\n" + + "BgNVHQ4EFgQUjFN264nAPC1KW4zs408p8gizoQ0wHwYDVR0jBBgwFoAUjFN264nA\n" + + "PC1KW4zs408p8gizoQ0wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC\n" + + "AQEAAX3VT+cMzihRo/2n0z1SHvY3Ozv+l7C4S709EmvdZBc4KaOzm8kJ+MTohmg9\n" + + "8TmJMtnBLcOiiEyd2qXZkD64vIfuWNsSjGDy/YBEqNNuwX+8LDbKbqJyeb07E+Hb\n" + + "63c+nogfCGgGXxuuRODMmh1rFprx60owNaNXYAE/K0DljOg8onYqSidVb1gIdmKN\n" + + "8EUJPDpryEo4Zt95ruAPrnoWcLvjUlKDjrnvjnAOy1wkAtywLcQbmYWXrYQojNuJ\n" + + "0DGXu1zSGh8dLo96tlvWYRVE4MzPQvRjNDhuShlw3c9+gTEandAODQ5Kj0wL1q0i\n" + + "F051Y7HdrnfZ/mK58D4VPm+EbQ==\n" + + "-----END CERTIFICATE-----"; + // @formatter:on + + public static final X509Certificate DEFAULT_CERTIFICATE; + static { + try { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + InputStream cert = new ByteArrayInputStream(DEFAULT_CERTIFICATE_KEY.getBytes(StandardCharsets.UTF_8)); + DEFAULT_CERTIFICATE = (X509Certificate) certFactory.generateCertificate(cert); + } catch (CertificateException ex) { + throw new IllegalArgumentException(ex); + } + } + + private TestKeys() { + } + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/WebApplicationContextRunnerUtils.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/WebApplicationContextRunnerUtils.java index 594391601406c..a9136bcff64be 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/WebApplicationContextRunnerUtils.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/WebApplicationContextRunnerUtils.java @@ -51,6 +51,13 @@ public static WebApplicationContextRunner resourceServerWithOboContextRunner() { .withPropertyValues(withResourceServerPropertyValues()); } + public static WebApplicationContextRunner webApplicationAndResourceServerContextRunner() { + return oauthClientAndResourceServerRunner() + .withPropertyValues(withWebApplicationOrResourceServerWithOboPropertyValues()) + .withPropertyValues(withResourceServerPropertyValues()) + .withPropertyValues(withPropertyValueWebApplicationAndResourceServer()); + } + @SuppressWarnings("unchecked") public static MultiValueMap toMultiValueMap(RequestEntity entity) { return (MultiValueMap) Optional.ofNullable(entity) @@ -68,7 +75,14 @@ public static String[] withWebApplicationOrResourceServerWithOboPropertyValues() public static String[] withResourceServerPropertyValues() { return new String[] { + "spring.cloud.azure.active-directory.enabled = true", "spring.cloud.azure.active-directory.profile.tenant-id=fake-tenant-id", "spring.cloud.azure.active-directory.app-id-uri=fake-app-id-uri"}; } + + public static String[] withPropertyValueWebApplicationAndResourceServer() { + return new String[] { + "spring.cloud.azure.active-directory.application-type = web_application_and_resource_server" + }; + } } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/ClientCertificatePropertiesConditionTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/ClientCertificatePropertiesConditionTests.java new file mode 100644 index 0000000000000..54c1f956247b0 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/ClientCertificatePropertiesConditionTests.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.aad.implementation.conditions; + +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; + +public class ClientCertificatePropertiesConditionTests extends AbstractCondition { + + @Test + void match() { + this.contextRunner + .withPropertyValues( + "spring.cloud.azure.credential.client-certificate-path = client-certificate-path", + "spring.cloud.azure.credential.client-certificate-password = client-certificate-password" + ) + .withUserConfiguration(ClientCertificateProperties.class) + .run(assertConditionMatch(true)); + } + + @Test + void noMatch() { + this.contextRunner + .withUserConfiguration(ClientCertificateProperties.class) + .run(assertConditionMatch(false)); + } + + @Configuration + @Conditional(ClientCertificatePropertiesCondition.class) + static class ClientCertificateProperties extends Config { } + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/ResourceServerWithOboConditionTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/ResourceServerWithOboConditionTests.java deleted file mode 100644 index 84a06908487bc..0000000000000 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/ResourceServerWithOboConditionTests.java +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.spring.cloud.autoconfigure.aad.implementation.conditions; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.context.annotation.Conditional; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; - -class ResourceServerWithOboConditionTests extends AbstractCondition { - - @Test - void testConditionWhenApplicationTypeInferenceIsWebApplication() { - this.contextRunner - .withPropertyValues("spring.cloud.azure.active-directory.credential.client-id = fake-client-id") - .withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class)) - .withUserConfiguration(ResourceServerWithOBOConditionConfig.class) - .run(assertConditionMatch(false)); - } - - @Test - void testConditionWhenApplicationTypeIsWebApplication() { - this.contextRunner - .withPropertyValues( - "spring.cloud.azure.active-directory.credential.client-id = fake-client-id", - "spring.cloud.azure.active-directory.application-type=web_application") - .withUserConfiguration(ResourceServerWithOBOConditionConfig.class) - .run(assertConditionMatch(false)); - } - - @Test - void testConditionWhenApplicationTypeIsResourceServer() { - this.contextRunner - .withPropertyValues( - "spring.cloud.azure.active-directory.credential.client-id = fake-client-id", - "spring.cloud.azure.active-directory.application-type=resource_server") - .withUserConfiguration(ResourceServerWithOBOConditionConfig.class) - .run(assertConditionMatch(false)); - } - - @Test - void testConditionWhenApplicationTypeIsResourceServerWithOBO() { - this.contextRunner - .withPropertyValues( - "spring.cloud.azure.active-directory.credential.client-id = fake-client-id", - "spring.cloud.azure.active-directory.application-type=resource_server_with_obo") - .withUserConfiguration(ResourceServerWithOBOConditionConfig.class) - .run(assertConditionMatch(true)); - } - - @Test - void testConditionWhenApplicationTypeIsWebApplicationAndResourceServer() { - this.contextRunner - .withPropertyValues( - "spring.cloud.azure.active-directory.credential.client-id = fake-client-id", - "spring.cloud.azure.active-directory.application-type=web_application_and_resource_server") - .withUserConfiguration(ResourceServerWithOBOConditionConfig.class) - .run(assertConditionMatch(false)); - } - - @Configuration - @Conditional(ResourceServerWithOBOCondition.class) - static class ResourceServerWithOBOConditionConfig extends Config { } -} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/WebApplicationAndResourceServerConditionTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/WebApplicationAndResourceServerConditionTests.java deleted file mode 100644 index fcda5b5a8c7d1..0000000000000 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/WebApplicationAndResourceServerConditionTests.java +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.spring.cloud.autoconfigure.aad.implementation.conditions; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.context.annotation.Conditional; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; - -class WebApplicationAndResourceServerConditionTests extends AbstractCondition { - - @Test - void testConditionWhenApplicationTypeInferenceIsWebApplication() { - this.contextRunner - .withPropertyValues("spring.cloud.azure.active-directory.credential.client-id = fake-client-id") - .withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class)) - .withUserConfiguration(WebApplicationAndResourceServerConditionConfig.class) - .run(assertConditionMatch(false)); - } - - @Test - void testConditionWhenApplicationTypeIsWebApplication() { - this.contextRunner - .withPropertyValues( - "spring.cloud.azure.active-directory.credential.client-id = fake-client-id", - "spring.cloud.azure.active-directory.application-type=web_application") - .withUserConfiguration(WebApplicationAndResourceServerConditionConfig.class) - .run(assertConditionMatch(false)); - } - - @Test - void testConditionWhenApplicationTypeIsResourceServer() { - this.contextRunner - .withPropertyValues( - "spring.cloud.azure.active-directory.credential.client-id = fake-client-id", - "spring.cloud.azure.active-directory.application-type=resource_server") - .withUserConfiguration(WebApplicationAndResourceServerConditionConfig.class) - .run(assertConditionMatch(false)); - } - - @Test - void testConditionWhenApplicationTypeIsResourceServerWithOBO() { - this.contextRunner - .withPropertyValues( - "spring.cloud.azure.active-directory.credential.client-id = fake-client-id", - "spring.cloud.azure.active-directory.application-type=resource_server_with_obo") - .withUserConfiguration(WebApplicationAndResourceServerConditionConfig.class) - .run(assertConditionMatch(false)); - } - - @Test - void testConditionWhenApplicationTypeIsWebApplicationAndResourceServer() { - this.contextRunner - .withPropertyValues( - "spring.cloud.azure.active-directory.credential.client-id = fake-client-id", - "spring.cloud.azure.active-directory.application-type=web_application_and_resource_server") - .withUserConfiguration(WebApplicationAndResourceServerConditionConfig.class) - .run(assertConditionMatch(true)); - } - - @Configuration - @Conditional(WebApplicationAndResourceServerCondition.class) - static class WebApplicationAndResourceServerConditionConfig extends Config { } -} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/WebApplicationWithoutResourceServerConditionTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/WebApplicationWithoutResourceServerConditionTests.java deleted file mode 100644 index 37219d009abe3..0000000000000 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/conditions/WebApplicationWithoutResourceServerConditionTests.java +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.spring.cloud.autoconfigure.aad.implementation.conditions; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.context.annotation.Conditional; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; - -class WebApplicationWithoutResourceServerConditionTests extends AbstractCondition { - - @Test - void testConditionWhenApplicationTypeInferenceIsWebApplication() { - this.contextRunner - .withPropertyValues("spring.cloud.azure.active-directory.credential.client-id = fake-client-id") - .withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class)) - .withUserConfiguration(WebApplicationWithoutResourceServerConditionConfig.class) - .run(assertConditionMatch(true)); - } - - @Test - void testConditionWhenApplicationTypeIsWebApplication() { - this.contextRunner - .withPropertyValues( - "spring.cloud.azure.active-directory.credential.client-id = fake-client-id", - "spring.cloud.azure.active-directory.application-type=web_application") - .withUserConfiguration(WebApplicationWithoutResourceServerConditionConfig.class) - .run(assertConditionMatch(true)); - } - - @Test - void testConditionWhenApplicationTypeIsResourceServer() { - this.contextRunner - .withPropertyValues( - "spring.cloud.azure.active-directory.credential.client-id = fake-client-id", - "spring.cloud.azure.active-directory.application-type=resource_server") - .withUserConfiguration(WebApplicationWithoutResourceServerConditionConfig.class) - .run(assertConditionMatch(false)); - } - - @Test - void testConditionWhenApplicationTypeIsResourceServerWithOBO() { - this.contextRunner - .withPropertyValues( - "spring.cloud.azure.active-directory.credential.client-id = fake-client-id", - "spring.cloud.azure.active-directory.application-type=resource_server_with_obo") - .withUserConfiguration(WebApplicationWithoutResourceServerConditionConfig.class) - .run(assertConditionMatch(false)); - } - - @Test - void testConditionWhenApplicationTypeIsWebApplicationAndResourceServer() { - this.contextRunner - .withPropertyValues( - "spring.cloud.azure.active-directory.credential.client-id = fake-client-id", - "spring.cloud.azure.active-directory.application-type=web_application_and_resource_server") - .withUserConfiguration(WebApplicationWithoutResourceServerConditionConfig.class) - .run(assertConditionMatch(false)); - } - - @Configuration - @Conditional(WebApplicationWithoutResourceServerCondition.class) - static class WebApplicationWithoutResourceServerConditionConfig extends Config { } -} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/jwt/AadJwtClientAuthenticationParametersConverterTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/jwt/AadJwtClientAuthenticationParametersConverterTests.java new file mode 100644 index 0000000000000..495c61556e6ec --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/jwt/AadJwtClientAuthenticationParametersConverterTests.java @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.aad.implementation.jwt; + +import com.azure.spring.cloud.autoconfigure.aad.implementation.TestJwks; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.util.Base64URL; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTParser; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.OAuth2AuthorizationException; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.util.MultiValueMap; + +import java.text.ParseException; +import java.util.Collections; +import java.util.function.Function; + +import static com.azure.spring.cloud.autoconfigure.aad.implementation.TestClientRegistrations.clientRegistration; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.springframework.security.oauth2.core.AuthorizationGrantType.CLIENT_CREDENTIALS; +import static org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_JWT; +import static org.springframework.security.oauth2.core.ClientAuthenticationMethod.PRIVATE_KEY_JWT; + +public class AadJwtClientAuthenticationParametersConverterTests { + + private Function jwkResolver; + + private AadJwtClientAuthenticationParametersConverter converter; + + @BeforeEach + @SuppressWarnings("unchecked") + public void setup() { + this.jwkResolver = mock(Function.class); + this.converter = new AadJwtClientAuthenticationParametersConverter<>(this.jwkResolver); + } + + @Test + void convertNull() { + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(clientRegistration(CLIENT_CREDENTIALS, CLIENT_SECRET_JWT).build()); + assertNull(this.converter.convert(clientCredentialsGrantRequest)); + } + + @Test + void resolveNullJwkThenThrowOAuth2AuthorizationException() { + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(clientRegistration(CLIENT_CREDENTIALS, PRIVATE_KEY_JWT).build()); + assertThatExceptionOfType(OAuth2AuthorizationException.class) + .isThrownBy(() -> this.converter.convert(clientCredentialsGrantRequest)) + .withMessage("[invalid_key] Failed to resolve JWK signing key for client registration 'test'."); + } + + @Test + void testAssertion() throws ParseException { + RSAKey rsaJwk = spy(TestJwks.DEFAULT_RSA_JWK); + given(this.jwkResolver.apply(any())).willReturn(rsaJwk); + given(rsaJwk.getX509CertThumbprint()).willReturn(new Base64URL("dGVzdA")); + ClientRegistration registration = clientRegistration(CLIENT_CREDENTIALS, PRIVATE_KEY_JWT).build(); + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(registration); + MultiValueMap parameters = this.converter.convert(clientCredentialsGrantRequest); + + assertThat(parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE)) + .isEqualTo("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); + String encodedJws = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION); + assertThat(encodedJws).isNotNull(); + + JWT jwt = JWTParser.parse(encodedJws); + + JWSHeader header = (JWSHeader) jwt.getHeader(); + assertThat(header.getAlgorithm().getName()).isEqualTo(SignatureAlgorithm.RS256.getName()); + assertThat(jwt.getJWTClaimsSet().getIssuer().equals(registration.getClientId())); + assertThat(jwt.getJWTClaimsSet().getSubject()).isEqualTo(registration.getClientId()); + assertThat(jwt.getJWTClaimsSet().getAudience()) + .isEqualTo(Collections.singletonList(registration.getProviderDetails().getTokenUri())); + assertThat(jwt.getJWTClaimsSet().getJWTID()).isNotNull(); + assertThat(jwt.getJWTClaimsSet().getIssueTime()).isNotNull(); + assertThat(jwt.getJWTClaimsSet().getExpirationTime()).isNotNull(); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/jwt/AadJwtEncoderTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/jwt/AadJwtEncoderTests.java new file mode 100644 index 0000000000000..0855de81ecf8d --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/jwt/AadJwtEncoderTests.java @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.aad.implementation.jwt; + +import com.azure.spring.cloud.autoconfigure.aad.implementation.TestJwtClaimsSets; +import com.nimbusds.jose.KeySourceException; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.JwtException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +public class AadJwtEncoderTests { + + private List jwkList; + + private JWKSource jwkSource; + + private AadJwtEncoder jwtEncoder; + + @BeforeEach + public void setUp() { + this.jwkList = new ArrayList<>(); + this.jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(new JWKSet(this.jwkList)); + this.jwtEncoder = new AadJwtEncoder(this.jwkSource); + } + + @Test + public void constructorWhenJwkSourceNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> new AadJwtEncoder(null)) + .withMessage("jwkSource cannot be null"); + } + + @Test + public void encodeWhenHeadersNullThenThrowIllegalArgumentException() { + Map jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet(); + assertThatIllegalArgumentException().isThrownBy(() -> this.jwtEncoder.encode(null, jwtClaimsSet)) + .withMessage("jwsHeader cannot be null"); + } + + @Test + public void encodeWhenClaimsNullThenThrowIllegalArgumentException() { + Map jwsHeader = new HashMap<>(); + jwsHeader.put("alg", SignatureAlgorithm.RS256.getName()); + assertThatIllegalArgumentException().isThrownBy(() -> this.jwtEncoder.encode(jwsHeader, null)) + .withMessage("jwtClaimsSet cannot be null"); + } + + @Test + @SuppressWarnings("unchecked") + public void encodeWhenJwkSelectFailedThenThrowJwtEncodingException() throws Exception { + this.jwkSource = mock(JWKSource.class); + this.jwtEncoder = new AadJwtEncoder(this.jwkSource); + given(this.jwkSource.get(any(), any())).willThrow(new KeySourceException("key source error")); + + Map jwsHeader = new HashMap<>(); + jwsHeader.put("alg", SignatureAlgorithm.RS256.getName()); + Map jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet(); + + assertThatExceptionOfType(JwtException.class) + .isThrownBy(() -> this.jwtEncoder.encode(jwsHeader, jwtClaimsSet)) + .withMessageContaining("Failed to select a JWK signing key -> key source error"); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/oauth2/AadOAuth2ClientAuthenticationJwkResolverTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/oauth2/AadOAuth2ClientAuthenticationJwkResolverTests.java new file mode 100644 index 0000000000000..10b1f001c2814 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/oauth2/AadOAuth2ClientAuthenticationJwkResolverTests.java @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.aad.implementation.oauth2; + +import com.azure.spring.cloud.autoconfigure.aad.implementation.TestJwks; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.KeyType; +import com.nimbusds.jose.jwk.RSAKey; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.security.oauth2.client.registration.ClientRegistration; + +import javax.xml.bind.DatatypeConverter; +import java.util.function.Function; + +import static com.azure.spring.cloud.autoconfigure.aad.implementation.TestClientRegistrations.clientRegistration; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.security.oauth2.core.AuthorizationGrantType.JWT_BEARER; +import static org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_JWT; +import static org.springframework.security.oauth2.core.ClientAuthenticationMethod.NONE; + +public class AadOAuth2ClientAuthenticationJwkResolverTests { + + private Function jwkFunction; + + @BeforeEach + @SuppressWarnings("unchecked") + public void setup() { + this.jwkFunction = mock(Function.class); + } + + @Test + void resolveJwkFunction() { + AadOAuth2ClientAuthenticationJwkResolver jwkResolver = + new AadOAuth2ClientAuthenticationJwkResolver("D:\\test\\test.pfx", "test"); + assertNull(jwkResolver.resolve(clientRegistration(JWT_BEARER, CLIENT_SECRET_JWT).build())); + assertNull(jwkResolver.resolve(clientRegistration(JWT_BEARER, NONE).build())); + } + + @Test + void jwkValue() { + RSAKey rsaJwk = Mockito.spy(TestJwks.DEFAULT_RSA_JWK); + given(this.jwkFunction.apply(any())).willReturn(rsaJwk); + + JWK jwk = jwkFunction.apply(clientRegistration(JWT_BEARER, CLIENT_SECRET_JWT).build()); + assertEquals("F6A8558E721545972D3B8EE60BA22F913A322601", + DatatypeConverter.printHexBinary(jwk.getX509CertThumbprint().decode())); + assertEquals(KeyType.RSA, jwk.getKeyType()); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapi/AadJwtBearerGrantRequestEntityConverterTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapi/AadJwtBearerGrantRequestEntityConverterTests.java new file mode 100644 index 0000000000000..cea314080a091 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapi/AadJwtBearerGrantRequestEntityConverterTests.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.aad.implementation.webapi; + +import org.junit.jupiter.api.Test; +import org.springframework.http.RequestEntity; +import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.util.MultiValueMap; + +import java.time.Instant; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AadJwtBearerGrantRequestEntityConverterTests { + + @SuppressWarnings("unchecked") + @Test + void requestedTokenUseParameter() { + ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("test") + .clientId("test") + .clientSecret("test-secret") + .authorizationGrantType(AuthorizationGrantType.JWT_BEARER) + .tokenUri("http://localhost/token") + .build(); + Jwt jwt = Jwt.withTokenValue("jwt-token-value") + .header("alg", JwsAlgorithms.RS256) + .claim("sub", "test") + .issuedAt(Instant.ofEpochMilli(Instant.now().toEpochMilli())) + .expiresAt(Instant.ofEpochMilli(Instant.now().plusSeconds(60).toEpochMilli())) + .build(); + JwtBearerGrantRequest request = new JwtBearerGrantRequest(clientRegistration, jwt); + AadJwtBearerGrantRequestEntityConverter converter = + new AadJwtBearerGrantRequestEntityConverter(); + RequestEntity> entity = + (RequestEntity>) converter.convert(request); + MultiValueMap parameters = entity.getBody(); + assertTrue(parameters.containsKey("requested_token_use")); + assertEquals("on_behalf_of", parameters.getFirst("requested_token_use")); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapi/AadResourceServerConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapi/AadResourceServerConfigurationTests.java index 341c4f6db83d4..d92496cd5e59d 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapi/AadResourceServerConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapi/AadResourceServerConfigurationTests.java @@ -6,7 +6,6 @@ import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthenticationProperties; import com.nimbusds.jwt.proc.JWTClaimsSetAwareJWSKeySelector; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.oauth2.core.OAuth2TokenValidator; @@ -27,7 +26,7 @@ class AadResourceServerConfigurationTests { void testNotExistBearerTokenAuthenticationToken() { resourceServerContextRunner() .withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class)) - .run(context -> assertThrows(NoSuchBeanDefinitionException.class, + .run(context -> assertThrows(IllegalStateException.class, () -> context.getBean(JWTClaimsSetAwareJWSKeySelector.class))); } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapp/AadAzureDelegatedOAuth2AuthorizedClientProviderTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapp/AadAzureDelegatedOAuth2AuthorizedClientProviderTests.java index 11c5f655fae84..9e316927fe0cc 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapp/AadAzureDelegatedOAuth2AuthorizedClientProviderTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapp/AadAzureDelegatedOAuth2AuthorizedClientProviderTests.java @@ -3,7 +3,6 @@ package com.azure.spring.cloud.autoconfigure.aad.implementation.webapp; -import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType; import org.junit.jupiter.api.Test; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; @@ -12,36 +11,38 @@ import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; -import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import java.time.Instant; import static com.azure.spring.cloud.autoconfigure.aad.AadClientRegistrationRepository.AZURE_CLIENT_REGISTRATION_ID; +import static com.azure.spring.cloud.autoconfigure.aad.implementation.constants.Constants.AZURE_DELEGATED; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE; +import static org.springframework.security.oauth2.core.AuthorizationGrantType.CLIENT_CREDENTIALS; class AadAzureDelegatedOAuth2AuthorizedClientProviderTests { private static final ClientRegistration AZURE_CLIENT_REGISTRATION = toClientRegistrationBuilder(AZURE_CLIENT_REGISTRATION_ID) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .authorizationGrantType(AUTHORIZATION_CODE) .build(); private static final ClientRegistration DELEGATED_CLIENT_REGISTRATION = toClientRegistrationBuilder("delegated") - .authorizationGrantType(new AuthorizationGrantType(AadAuthorizationGrantType.AZURE_DELEGATED.getValue())) + .authorizationGrantType(AZURE_DELEGATED) .scope("testScope") .build(); private static final ClientRegistration CLIENT_CREDENTIALS_CLIENT_REGISTRATION = toClientRegistrationBuilder("clientCredentials") - .authorizationGrantType(new AuthorizationGrantType(AadAuthorizationGrantType.CLIENT_CREDENTIALS.getValue())) + .authorizationGrantType(CLIENT_CREDENTIALS) .build(); private static ClientRegistration.Builder toClientRegistrationBuilder(String registrationId) { diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapp/SerializerUtilsTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapp/SerializerUtilsTests.java index f862c5d372ab8..5e4fa1fc0ef33 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapp/SerializerUtilsTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapp/SerializerUtilsTests.java @@ -2,7 +2,7 @@ // Licensed under the MIT License. package com.azure.spring.cloud.autoconfigure.aad.implementation.webapp; -import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType; +import com.azure.spring.cloud.autoconfigure.aad.implementation.constants.Constants; import org.junit.jupiter.api.Test; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -18,6 +18,9 @@ import static com.azure.spring.cloud.autoconfigure.aad.implementation.jackson.SerializerUtils.serializeOAuth2AuthorizedClientMap; import static com.azure.spring.cloud.autoconfigure.aad.AadClientRegistrationRepository.AZURE_CLIENT_REGISTRATION_ID; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE; +import static org.springframework.security.oauth2.core.AuthorizationGrantType.CLIENT_CREDENTIALS; +import static org.springframework.security.oauth2.core.AuthorizationGrantType.JWT_BEARER; class SerializerUtilsTests { @@ -27,17 +30,14 @@ class SerializerUtilsTests { void serializeAndDeserializeTest() { Map authorizedClients = new HashMap<>(); authorizedClients.put(AZURE_CLIENT_REGISTRATION_ID, - createOAuth2AuthorizedClient(AZURE_CLIENT_REGISTRATION_ID, - AadAuthorizationGrantType.AUTHORIZATION_CODE.getValue())); + createOAuth2AuthorizedClient(AZURE_CLIENT_REGISTRATION_ID, AUTHORIZATION_CODE.getValue())); authorizedClients.put("graph", createOAuth2AuthorizedClient("graph", - AadAuthorizationGrantType.AZURE_DELEGATED.getValue())); + Constants.AZURE_DELEGATED.getValue())); authorizedClients.put("arm", - createOAuth2AuthorizedClient("arm", - AadAuthorizationGrantType.CLIENT_CREDENTIALS.getValue())); + createOAuth2AuthorizedClient("arm", CLIENT_CREDENTIALS.getValue())); authorizedClients.put("office", - createOAuth2AuthorizedClient("office", - AadAuthorizationGrantType.ON_BEHALF_OF.getValue())); + createOAuth2AuthorizedClient("office", JWT_BEARER.getValue())); String serializedOAuth2AuthorizedClients = serializeOAuth2AuthorizedClientMap(authorizedClients); LOGGER.info(serializedOAuth2AuthorizedClients); Map deserializedOAuth2AuthorizedClients = diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/properties/AadAuthenticationPropertiesTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/properties/AadAuthenticationPropertiesTests.java index 6eb4fe54a00dd..3720d997518fc 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/properties/AadAuthenticationPropertiesTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/properties/AadAuthenticationPropertiesTests.java @@ -3,18 +3,25 @@ package com.azure.spring.cloud.autoconfigure.aad.properties; +import com.azure.spring.cloud.autoconfigure.aad.implementation.jwt.AadJwtEncoder; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import java.util.Arrays; import java.util.Map; +import static com.azure.spring.cloud.autoconfigure.aad.AadClientRegistrationRepository.AZURE_CLIENT_REGISTRATION_ID; import static com.azure.spring.cloud.autoconfigure.aad.implementation.WebApplicationContextRunnerUtils.oauthClientRunner; import static com.azure.spring.cloud.autoconfigure.aad.implementation.WebApplicationContextRunnerUtils.resourceServerContextRunner; import static com.azure.spring.cloud.autoconfigure.aad.implementation.WebApplicationContextRunnerUtils.resourceServerWithOboContextRunner; import static com.azure.spring.cloud.autoconfigure.aad.implementation.WebApplicationContextRunnerUtils.webApplicationContextRunner; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; 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.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE; +import static org.springframework.security.oauth2.core.ClientAuthenticationMethod.PRIVATE_KEY_JWT; class AadAuthenticationPropertiesTests { @@ -32,7 +39,7 @@ void mapPropertiesSetting() { Map authorizationClients = properties.getAuthorizationClients(); assertTrue(authorizationClients.containsKey("test")); assertTrue(authorizationClients.get("test").getScopes().containsAll(Arrays.asList("test1", "test2"))); - assertEquals(authorizationClients.get("test").getAuthorizationGrantType(), AadAuthorizationGrantType.AUTHORIZATION_CODE); + assertEquals(authorizationClients.get("test").getAuthorizationGrantType(), AUTHORIZATION_CODE); Map authenticateAdditionalParameters = properties.getAuthenticateAdditionalParameters(); assertEquals(authenticateAdditionalParameters.size(), 1); @@ -50,6 +57,98 @@ void webAppWithOboWithExceptionTest() { assertThrows(IllegalStateException.class, () -> context.getBean(AadAuthenticationProperties.class))); } + @Test + void webAppWithJwtBearerWithExceptionTest() { + webApplicationContextRunner() + .withPropertyValues( + "spring.cloud.azure.active-directory.authorization-clients.graph.authorizationGrantType = urn:ietf:params:oauth:grant-type:jwt-bearer") + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AadAuthenticationProperties.class))); + } + + @Test + void resourceServerOboWithExceptionTest() { + resourceServerContextRunner() + .withPropertyValues( + "spring.cloud.azure.active-directory.authorization-clients.graph.authorizationGrantType = on_behalf_of") + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AadAuthenticationProperties.class))); + } + + @Test + void resourceServerJwtBearerWithExceptionTest() { + resourceServerContextRunner() + .withPropertyValues( + "spring.cloud.azure.active-directory.authorization-clients.graph.authorizationGrantType = urn:ietf:params:oauth:grant-type:jwt-bearer") + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AadAuthenticationProperties.class))); + } + + @Test + void supportedClientAuthenticationMethod() { + supportedClientAuthenticationMethodsByRunnerApplicationType(webApplicationContextRunner()); + supportedClientAuthenticationMethodsByRunnerApplicationType(resourceServerContextRunner()); + supportedClientAuthenticationMethodsByRunnerApplicationType(resourceServerWithOboContextRunner()); + } + + private void supportedClientAuthenticationMethodsByRunnerApplicationType(WebApplicationContextRunner runner) { + runner + .withPropertyValues( + "spring.cloud.azure.active-directory.authorization-clients.one.client-authentication-method = basic", + "spring.cloud.azure.active-directory.authorization-clients.one.scopes = test", + "spring.cloud.azure.active-directory.authorization-clients.two.client-authentication-method = client_secret_basic", + "spring.cloud.azure.active-directory.authorization-clients.two.scopes = test", + "spring.cloud.azure.active-directory.authorization-clients.three.client-authentication-method = post", + "spring.cloud.azure.active-directory.authorization-clients.three.scopes = test", + "spring.cloud.azure.active-directory.authorization-clients.four.client-authentication-method = client_secret_post", + "spring.cloud.azure.active-directory.authorization-clients.four.scopes = test", + "spring.cloud.azure.active-directory.authorization-clients.five.client-authentication-method = private_key_jwt", + "spring.cloud.azure.active-directory.authorization-clients.five.scopes = test" + ) + .run(context -> + assertThat(context.getBean(AadAuthenticationProperties.class)).isNotNull()); + } + + @Test + void unsupportedClientAuthenticationMethods() { + unsupportedClientAuthenticationMethodsByRunnerApplicationType(webApplicationContextRunner()); + unsupportedClientAuthenticationMethodsByRunnerApplicationType(resourceServerContextRunner()); + unsupportedClientAuthenticationMethodsByRunnerApplicationType(resourceServerWithOboContextRunner()); + } + + private void unsupportedClientAuthenticationMethodsByRunnerApplicationType(WebApplicationContextRunner runner) { + runner + .withPropertyValues( + "spring.cloud.azure.active-directory.authorization-clients.one.client-authentication-method = client_secret_jwt", + "spring.cloud.azure.active-directory.authorization-clients.one.scopes = test" + ) + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AadAuthenticationProperties.class))); + } + + @Test + void overrideClientAuthenticationMethodForClientAzure() { + webApplicationContextRunner() + .withPropertyValues( + "spring.cloud.azure.active-directory.authorization-clients.azure.client-authentication-method = private_key_jwt") + .run(context -> { + AadAuthenticationProperties properties = context.getBean(AadAuthenticationProperties.class); + assertThat(properties).isNotNull(); + Map authorizationClients = properties.getAuthorizationClients(); + assertEquals(PRIVATE_KEY_JWT, + authorizationClients.get(AZURE_CLIENT_REGISTRATION_ID).getClientAuthenticationMethod()); + }); + } + + @Test + void configureScopesNullWithExceptionTest() { + webApplicationContextRunner() + .withPropertyValues( + "spring.cloud.azure.active-directory.authorization-clients.test.client-authentication-method = private_key_jwt") + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AadAuthenticationProperties.class))); + } + @Test void graphUriConfigurationTest() { webApplicationContextRunner() @@ -226,7 +325,6 @@ void applicationTypeOfWebApplication() { @Test void applicationTypeWithResourceServer() { resourceServerContextRunner() - .withPropertyValues("spring.cloud.azure.active-directory.enabled=true") .run(context -> { AadAuthenticationProperties properties = context.getBean(AadAuthenticationProperties.class); assertEquals(properties.getApplicationType(), AadApplicationType.RESOURCE_SERVER); @@ -234,7 +332,6 @@ void applicationTypeWithResourceServer() { resourceServerContextRunner() .withPropertyValues( - "spring.cloud.azure.active-directory.enabled=true", "spring.cloud.azure.active-directory.application-type=resource_server" ) .run(context -> { @@ -284,7 +381,6 @@ void applicationTypeWithWebApplicationAndResourceServer() { void testInvalidApplicationType() { resourceServerContextRunner() .withPropertyValues( - "spring.cloud.azure.active-directory.enabled=true", "spring.cloud.azure.active-directory.application-type=web_application" ) .run(context -> assertThrows(IllegalStateException.class, () -> context.getBean(AadAuthenticationProperties.class))); @@ -304,12 +400,16 @@ void setDefaultValueFromAzureGlobalPropertiesTest() { "spring.cloud.azure.profile.tenant-id = global-tenant-id", "spring.cloud.azure.active-directory.credential.client-id = aad-client-id", "spring.cloud.azure.active-directory.credential.client-secret = aad-client-secret", + "spring.cloud.azure.active-directory.credential.client-certificate-path = certificate.pfx", + "spring.cloud.azure.active-directory.credential.client-certificate-password = aad-client-certificate-password", "spring.cloud.azure.active-directory.profile.tenant-id = aad-tenant-id" ) .run(context -> { AadAuthenticationProperties properties = context.getBean(AadAuthenticationProperties.class); assertEquals("aad-client-id", properties.getCredential().getClientId()); assertEquals("aad-client-secret", properties.getCredential().getClientSecret()); + assertEquals("certificate.pfx", properties.getCredential().getClientCertificatePath()); + assertEquals("aad-client-certificate-password", properties.getCredential().getClientCertificatePassword()); assertEquals("aad-tenant-id", properties.getProfile().getTenantId()); }); oauthClientRunner() @@ -317,13 +417,23 @@ void setDefaultValueFromAzureGlobalPropertiesTest() { "spring.cloud.azure.active-directory.enabled = true", "spring.cloud.azure.credential.client-id = global-client-id", "spring.cloud.azure.credential.client-secret = global-client-secret", + "spring.cloud.azure.credential.client-certificate-path = certificate.p12", + "spring.cloud.azure.credential.client-certificate-password = aad-client-certificate-password", "spring.cloud.azure.profile.tenant-id = global-tenant-id" ) .run(context -> { AadAuthenticationProperties properties = context.getBean(AadAuthenticationProperties.class); assertEquals("global-client-id", properties.getCredential().getClientId()); assertEquals("global-client-secret", properties.getCredential().getClientSecret()); + assertEquals("certificate.p12", properties.getCredential().getClientCertificatePath()); + assertEquals("aad-client-certificate-password", properties.getCredential().getClientCertificatePassword()); assertEquals("global-tenant-id", properties.getProfile().getTenantId()); }); } + + @Test + public void constructorWhenJwkSourceNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> new AadJwtEncoder(null)) + .withMessage("jwkSource cannot be null"); + } } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aadb2c/implementation/AadB2cAutoConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aadb2c/implementation/AadB2cAutoConfigurationTests.java index 862c09bda9f2c..bf362c8e230ab 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aadb2c/implementation/AadB2cAutoConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aadb2c/implementation/AadB2cAutoConfigurationTests.java @@ -2,12 +2,11 @@ // Licensed under the MIT License. package com.azure.spring.cloud.autoconfigure.aadb2c.implementation; -import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType; import com.azure.spring.cloud.autoconfigure.aadb2c.AadB2cAuthorizationRequestResolver; -import com.azure.spring.cloud.autoconfigure.aadb2c.AadB2cLogoutSuccessHandler; import com.azure.spring.cloud.autoconfigure.aadb2c.AadB2cAutoConfiguration; -import com.azure.spring.cloud.autoconfigure.aadb2c.properties.AadB2cProperties; +import com.azure.spring.cloud.autoconfigure.aadb2c.AadB2cLogoutSuccessHandler; import com.azure.spring.cloud.autoconfigure.aadb2c.AadB2cResourceServerAutoConfiguration; +import com.azure.spring.cloud.autoconfigure.aadb2c.properties.AadB2cProperties; import com.azure.spring.cloud.autoconfigure.aadb2c.properties.AuthorizationClientProperties; import com.azure.spring.cloud.autoconfigure.context.AzureGlobalPropertiesAutoConfiguration; import org.junit.jupiter.api.Assertions; @@ -18,6 +17,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; import java.util.Arrays; @@ -54,7 +54,7 @@ void mapPropertiesSetting() { Map authorizationClients = properties.getAuthorizationClients(); assertTrue(authorizationClients.containsKey("test")); assertTrue(authorizationClients.get("test").getScopes().containsAll(Arrays.asList("test1", "test2"))); - assertEquals(authorizationClients.get("test").getAuthorizationGrantType(), AadAuthorizationGrantType.CLIENT_CREDENTIALS); + assertEquals(authorizationClients.get("test").getAuthorizationGrantType(), AuthorizationGrantType.CLIENT_CREDENTIALS); Map authenticateAdditionalParameters = properties.getAuthenticateAdditionalParameters(); assertEquals(authenticateAdditionalParameters.size(), 2); diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aadb2c/implementation/AbstractAadB2cOAuth2ClientTestConfigurations.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aadb2c/implementation/AbstractAadB2cOAuth2ClientTestConfigurations.java index 8fdae8b5f024d..4171db1f2ca61 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aadb2c/implementation/AbstractAadB2cOAuth2ClientTestConfigurations.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aadb2c/implementation/AbstractAadB2cOAuth2ClientTestConfigurations.java @@ -5,7 +5,6 @@ import com.azure.spring.cloud.autoconfigure.aadb2c.configuration.AadB2cOAuth2ClientConfiguration; import com.azure.spring.cloud.autoconfigure.aadb2c.properties.AadB2cProperties; import com.azure.spring.cloud.autoconfigure.aadb2c.properties.AuthorizationClientProperties; -import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; @@ -16,6 +15,7 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; import java.util.Map; @@ -58,7 +58,7 @@ void testClientCredentialProperties() { Assertions.assertEquals(authorizationClients.get(clientName).getScopes().get(0), AadB2cConstants.TEST_CLIENT_CREDENTIAL_SCOPES); Assertions.assertEquals(authorizationClients.get(clientName).getAuthorizationGrantType(), - AadAuthorizationGrantType.CLIENT_CREDENTIALS); + AuthorizationGrantType.CLIENT_CREDENTIALS); } }); }