From 1a0510d7019fef000fd827afdc9ae3874317b35e Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Wed, 10 Nov 2021 14:16:20 +0800 Subject: [PATCH 1/4] 1. Move active directory related code from main branch to 4.0 branch. 2. Change package name. 3. Resolve the errors reported by "mvn clean install". --- .../checkstyle/checkstyle-suppressions.xml | 2 + .../spring-cloud-azure-autoconfigure/pom.xml | 58 +- .../aad/AADApplicationType.java | 74 +++ .../aad/AADAuthorizationGrantType.java | 35 + .../aad/AADAuthorizationServerEndpoints.java | 54 ++ .../aad/AADClientRegistrationRepository.java | 160 +++++ .../aad/AADIssuerJWSKeySelector.java | 81 +++ .../AADJwtDecoderProviderConfiguration.java | 72 ++ .../AADJwtGrantedAuthoritiesConverter.java | 64 ++ .../aad/AADOAuth2AuthenticatedPrincipal.java | 114 ++++ .../aad/AADOAuth2ClientConfiguration.java | 67 ++ .../aad/AADTrustedIssuerRepository.java | 132 ++++ ...zationCodeGrantRequestEntityConverter.java | 63 ++ ...ssionOAuth2AuthorizedClientRepository.java | 83 +++ .../constants/AADTokenClaim.java | 18 + .../constants/AuthorityPrefix.java | 15 + .../AADClientRegistrationDeserializer.java | 58 ++ .../jackson/AADClientRegistrationMixin.java | 19 + .../AADOAuth2ClientJackson2Module.java | 25 + .../implementation/jackson/JsonNodeUtils.java | 45 ++ .../jackson/SerializerUtils.java | 55 ++ .../implementation/jackson/StdConverters.java | 75 +++ .../implementation/aad/package-info.java | 6 + ...JwtBearerTokenAuthenticationConverter.java | 91 +++ .../AADOBOOAuth2AuthorizedClientProvider.java | 188 ++++++ .../AADResourceServerConfiguration.java | 98 +++ .../webapi/AADResourceServerProperties.java | 82 +++ ...rceServerWebSecurityConfigurerAdapter.java | 29 + .../aad/webapi/package-info.java | 6 + .../validator/AADJwtAudienceValidator.java | 38 ++ .../validator/AADJwtClaimValidator.java | 59 ++ .../validator/AADJwtIssuerValidator.java | 69 ++ .../aad/webapi/validator/package-info.java | 6 + ...legatedOAuth2AuthorizedClientProvider.java | 126 ++++ .../AADHandleConditionalAccessFilter.java | 82 +++ ...zationCodeGrantRequestEntityConverter.java | 53 ++ ...AADOAuth2AuthorizationRequestResolver.java | 78 +++ .../aad/webapp/AADOAuth2UserService.java | 173 +++++ .../AADWebApplicationConfiguration.java | 56 ++ .../AADWebSecurityConfigurerAdapter.java | 81 +++ .../webapp/AuthorizationClientProperties.java | 50 ++ .../aad/webapp/AzureClientRegistration.java | 33 + .../aad/webapp/GraphClient.java | 99 +++ .../aad/webapp/GroupInformation.java | 33 + .../aad/webapp/package-info.java | 6 + ...DAppRoleStatelessAuthenticationFilter.java | 114 ++++ .../aad/AADAuthenticationFilter.java | 164 +++++ ...AuthenticationFilterAutoConfiguration.java | 106 +++ .../aad/AADAuthenticationProperties.java | 622 ++++++++++++++++++ .../aad/AADAutoConfiguration.java | 33 + .../autoconfigure/aad/AzureADGraphClient.java | 185 ++++++ .../autoconfigure/aad/Constants.java | 27 + .../aad/JacksonObjectMapperFactory.java | 23 + .../autoconfigure/aad/Membership.java | 67 ++ .../autoconfigure/aad/Memberships.java | 58 ++ .../autoconfigure/aad/UserPrincipal.java | 122 ++++ .../aad/UserPrincipalManager.java | 210 ++++++ .../autoconfigure/aad/package-info.java | 6 + .../AADB2CAuthorizationRequestResolver.java | 119 ++++ .../b2c/AADB2CAutoConfiguration.java | 54 ++ .../AADB2CClientRegistrationRepository.java | 40 ++ .../autoconfigure/b2c/AADB2CConditions.java | 133 ++++ .../b2c/AADB2CConfigurationException.java | 17 + ...JwtBearerTokenAuthenticationConverter.java | 37 ++ .../b2c/AADB2CLogoutSuccessHandler.java | 39 ++ ...zationCodeGrantRequestEntityConverter.java | 19 + .../b2c/AADB2COAuth2ClientConfiguration.java | 118 ++++ .../b2c/AADB2COidcLoginConfigurer.java | 45 ++ .../autoconfigure/b2c/AADB2CProperties.java | 326 +++++++++ ...AADB2CResourceServerAutoConfiguration.java | 99 +++ .../b2c/AADB2CTrustedIssuerRepository.java | 50 ++ .../autoconfigure/b2c/AADB2CURL.java | 77 +++ .../b2c/AuthorizationClientProperties.java | 34 + .../autoconfigure/b2c/package-info.java | 6 + .../aad/ClientRegistrationCondition.java | 51 ++ .../aad/ResourceServerCondition.java | 45 ++ .../aad/WebApplicationCondition.java | 53 ++ .../condition/aad/package-info.java | 6 + .../aad/AADApplicationTypeTest.java | 71 ++ .../AADClientRegistrationRepositoryTest.java | 311 +++++++++ .../aad/AADOAuth2ClientConfigurationTest.java | 130 ++++ .../aad/WebApplicationContextRunnerUtils.java | 72 ++ .../jackson/SerializerUtilsTest.java | 71 ++ ...earerTokenAuthenticationConverterTest.java | 121 ++++ .../AADResourceServerConfigurationTest.java | 75 +++ .../AADResourceServerPropertiesTest.java | 39 ++ .../AADJwtAudienceValidatorTest.java | 67 ++ .../validator/AADJwtIssuerValidatorTest.java | 67 ++ ...ADAccessTokenGroupRolesExtractionTest.java | 193 ++++++ ...tedOAuth2AuthorizedClientProviderTest.java | 149 +++++ .../webapp/AADIdTokenRolesExtractionTest.java | 51 ++ ...onCodeGrantRequestEntityConverterTest.java | 140 ++++ .../AADAppRoleAuthenticationFilterTest.java | 231 +++++++ ...AADAuthenticationFilterPropertiesTest.java | 104 +++ .../aad/AADAuthenticationFilterTest.java | 133 ++++ .../aad/AADAuthenticationPropertiesTest.java | 290 ++++++++ .../aad/AzureADGraphClientTest.java | 61 ++ .../autoconfigure/aad/MembershipTest.java | 38 ++ .../aad/MicrosoftGraphConstants.java | 24 + .../aad/ResourceRetrieverTest.java | 57 ++ .../autoconfigure/aad/TestConstants.java | 30 + .../aad/UserPrincipalAzureADGraphTest.java | 69 ++ .../aad/UserPrincipalManagerAudienceTest.java | 145 ++++ .../aad/UserPrincipalManagerTest.java | 86 +++ .../aad/UserPrincipalMicrosoftGraphTest.java | 140 ++++ ...ADB2CAuthorizationRequestResolverTest.java | 78 +++ .../b2c/AADB2CAutoConfigurationTest.java | 147 +++++ .../autoconfigure/b2c/AADB2CConstants.java | 76 +++ .../b2c/AADB2CLogoutSuccessHandlerTest.java | 51 ++ ...2CResourceServerAutoConfigurationTest.java | 214 ++++++ .../autoconfigure/b2c/AADB2CURLTest.java | 122 ++++ .../b2c/AADB2CUserPrincipalTest.java | 162 +++++ ...ctAADB2COAuth2ClientTestConfiguration.java | 82 +++ .../condition/aad/AbstractCondition.java | 34 + .../aad/ClientRegistrationConditionTest.java | 77 +++ .../aad/ResourceServerConditionTest.java | 75 +++ .../aad/WebApplicationConditionTest.java | 85 +++ .../src/test/resources/aadb2c.enable.config | 1 + .../src/test/resources/jwt-bad-issuer.txt | 1 + .../src/test/resources/jwt-null-issuer.txt | 1 + .../src/test/resources/jwt-signed.txt | 1 + .../src/test/resources/jwt-valid-issuer.txt | 1 + 122 files changed, 10180 insertions(+), 9 deletions(-) create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADApplicationType.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADAuthorizationGrantType.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADAuthorizationServerEndpoints.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADClientRegistrationRepository.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADIssuerJWSKeySelector.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADJwtDecoderProviderConfiguration.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADJwtGrantedAuthoritiesConverter.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADOAuth2AuthenticatedPrincipal.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADOAuth2ClientConfiguration.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADTrustedIssuerRepository.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AbstractOAuth2AuthorizationCodeGrantRequestEntityConverter.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/JacksonHttpSessionOAuth2AuthorizedClientRepository.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/constants/AADTokenClaim.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/constants/AuthorityPrefix.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/AADClientRegistrationDeserializer.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/AADClientRegistrationMixin.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/AADOAuth2ClientJackson2Module.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/JsonNodeUtils.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/SerializerUtils.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/StdConverters.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/package-info.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADJwtBearerTokenAuthenticationConverter.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADOBOOAuth2AuthorizedClientProvider.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADResourceServerConfiguration.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADResourceServerProperties.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADResourceServerWebSecurityConfigurerAdapter.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/package-info.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/AADJwtAudienceValidator.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/AADJwtClaimValidator.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/AADJwtIssuerValidator.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/package-info.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADAzureDelegatedOAuth2AuthorizedClientProvider.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADHandleConditionalAccessFilter.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADOAuth2AuthorizationCodeGrantRequestEntityConverter.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADOAuth2AuthorizationRequestResolver.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADOAuth2UserService.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADWebApplicationConfiguration.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADWebSecurityConfigurerAdapter.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AuthorizationClientProperties.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AzureClientRegistration.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/GraphClient.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/GroupInformation.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/package-info.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAppRoleStatelessAuthenticationFilter.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationFilter.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationFilterAutoConfiguration.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationProperties.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAutoConfiguration.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AzureADGraphClient.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/Constants.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/JacksonObjectMapperFactory.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/Membership.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/Memberships.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipal.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipalManager.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/package-info.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CAuthorizationRequestResolver.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CAutoConfiguration.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CClientRegistrationRepository.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CConditions.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CConfigurationException.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CJwtBearerTokenAuthenticationConverter.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CLogoutSuccessHandler.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2COAuth2AuthorizationCodeGrantRequestEntityConverter.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2COAuth2ClientConfiguration.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2COidcLoginConfigurer.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CProperties.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CResourceServerAutoConfiguration.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CTrustedIssuerRepository.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CURL.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AuthorizationClientProperties.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/package-info.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/ClientRegistrationCondition.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/ResourceServerCondition.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/WebApplicationCondition.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/package-info.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADApplicationTypeTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADClientRegistrationRepositoryTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADOAuth2ClientConfigurationTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/WebApplicationContextRunnerUtils.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/SerializerUtilsTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADJwtBearerTokenAuthenticationConverterTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADResourceServerConfigurationTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADResourceServerPropertiesTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/AADJwtAudienceValidatorTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/AADJwtIssuerValidatorTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADAccessTokenGroupRolesExtractionTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADAzureDelegatedOAuth2AuthorizedClientProviderTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADIdTokenRolesExtractionTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADOAuth2AuthorizationCodeGrantRequestEntityConverterTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAppRoleAuthenticationFilterTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationFilterPropertiesTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationFilterTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationPropertiesTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AzureADGraphClientTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/MembershipTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/MicrosoftGraphConstants.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/ResourceRetrieverTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/TestConstants.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipalAzureADGraphTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipalManagerAudienceTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipalManagerTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CAuthorizationRequestResolverTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CAutoConfigurationTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CConstants.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CLogoutSuccessHandlerTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CResourceServerAutoConfigurationTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CURLTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CUserPrincipalTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AbstractAADB2COAuth2ClientTestConfiguration.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/AbstractCondition.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/ClientRegistrationConditionTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/ResourceServerConditionTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/WebApplicationConditionTest.java create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/resources/aadb2c.enable.config create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/resources/jwt-bad-issuer.txt create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/resources/jwt-null-issuer.txt create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/resources/jwt-signed.txt create mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/test/resources/jwt-valid-issuer.txt diff --git a/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml b/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml index 508a46c33ee9c..47d3507b92655 100755 --- a/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml +++ b/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml @@ -422,6 +422,8 @@ the main ServiceBusClientBuilder. --> /> + + diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml b/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml index 4bd4bd22a7258..baf9db1cbe7f8 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml +++ b/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml @@ -181,13 +181,18 @@ spring-boot-autoconfigure 2.5.4 - org.springframework spring-context-support 5.3.9 true + + org.springframework + spring-webflux + 5.3.9 + true + org.springframework.boot @@ -195,7 +200,6 @@ 2.5.4 true - org.springframework.boot spring-boot-configuration-processor @@ -203,6 +207,32 @@ true + + + org.springframework.security + spring-security-oauth2-client + 5.5.2 + true + + + org.springframework.security + spring-security-oauth2-resource-server + 5.5.2 + true + + + org.springframework.security + spring-security-oauth2-jose + 5.5.2 + true + + + org.springframework.security + spring-security-config + 5.5.2 + true + + @@ -212,6 +242,18 @@ 3.0.2 provided + + javax.servlet + javax.servlet-api + 4.0.1 + true + + + org.hibernate.validator + hibernate-validator + 6.2.0.Final + true + jakarta.validation @@ -250,13 +292,6 @@ - - - org.hibernate.validator - hibernate-validator - 6.2.0.Final - test - org.glassfish jakarta.el @@ -299,6 +334,11 @@ org.springframework.data:spring-data-redis:[2.5.4] org.springframework.kafka:spring-kafka:[2.7.6] org.springframework:spring-context-support:[5.3.9] + org.springframework:spring-webflux:[5.3.9] + org.springframework.security:spring-security-oauth2-client:[5.5.2] + org.springframework.security:spring-security-oauth2-resource-server:[5.5.2] + org.springframework.security:spring-security-oauth2-jose:[5.5.2] + org.springframework.security:spring-security-config:[5.5.2] org.springframework:spring-jms:[5.3.9] diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADApplicationType.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADApplicationType.java new file mode 100644 index 0000000000000..2283e8622e815 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADApplicationType.java @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad; + +import org.springframework.util.ClassUtils; + +/** + * AAD application type. + *

The value can be inferred by dependencies, only 'web_application_and_resource_server' must be configured manually.

+ *
+ * | Has dependency: spring-security-oauth2-client | Has dependency: spring-security-oauth2-resource-server | Valid values of application type                                                                       | Default value               |
+ * |-----------------------------------------------|--------------------------------------------------------|--------------------------------------------------------------------------------------------------------|-----------------------------|
+ * |                      Yes                      |                          No                            |  'web_application'                                                                                     |       'web_application'     |
+ * |                      No                       |                          Yes                           |  'resource_server'                                                                                     |       'resource_server'     |
+ * |                      Yes                      |                          Yes                           |  'web_application','resource_server','resource_server_with_obo', 'web_application_and_resource_server' | 'resource_server_with_obo'  |
+ * 
+ */ +public enum AADApplicationType { + + WEB_APPLICATION("web_application"), + RESOURCE_SERVER("resource_server"), + RESOURCE_SERVER_WITH_OBO("resource_server_with_obo"), + WEB_APPLICATION_AND_RESOURCE_SERVER("web_application_and_resource_server"); + + private final String applicationType; + + AADApplicationType(String applicationType) { + this.applicationType = applicationType; + } + + public String getValue() { + return applicationType; + } + + public static final String SPRING_SECURITY_OAUTH2_CLIENT_CLASS_NAME = + "org.springframework.security.oauth2.client.registration.ClientRegistration"; + public static final String SPRING_SECURITY_OAUTH2_RESOURCE_SERVER_CLASS_NAME = + "org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken"; + + /** + * Infer application type by dependencies + * + * @return AADApplicationType + */ + public static AADApplicationType inferApplicationTypeByDependencies() { + AADApplicationType type; + if (isOAuth2ClientAvailable()) { + if (isResourceServerAvailable()) { + type = AADApplicationType.RESOURCE_SERVER_WITH_OBO; + } else { + type = AADApplicationType.WEB_APPLICATION; + } + } else { + if (isResourceServerAvailable()) { + type = AADApplicationType.RESOURCE_SERVER; + } else { + type = null; + } + } + return type; + } + + private static boolean isOAuth2ClientAvailable() { + return isPresent(SPRING_SECURITY_OAUTH2_CLIENT_CLASS_NAME); + } + + private static boolean isResourceServerAvailable() { + return isPresent(SPRING_SECURITY_OAUTH2_RESOURCE_SERVER_CLASS_NAME); + } + + private static boolean isPresent(String className) { + return ClassUtils.isPresent(className, ClassUtils.getDefaultClassLoader()); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADAuthorizationGrantType.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADAuthorizationGrantType.java new file mode 100644 index 0000000000000..c876b836d4edf --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADAuthorizationGrantType.java @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad; + +import org.springframework.security.oauth2.core.AuthorizationGrantType; + +/** + * Defines grant types: client_credentials, authorization_code, on_behalf_of, azure_delegated. + */ +public enum AADAuthorizationGrantType { + + CLIENT_CREDENTIALS("client_credentials"), + AUTHORIZATION_CODE("authorization_code"), + ON_BEHALF_OF("on_behalf_of"), + AZURE_DELEGATED("azure_delegated"); + + private final String authorizationGrantType; + + AADAuthorizationGrantType(String authorizationGrantType) { + // For backward compatibility, we support 'on-behalf-of'. + if ("on-behalf-of".equals(authorizationGrantType)) { + this.authorizationGrantType = "on_behalf_of"; + } else { + this.authorizationGrantType = authorizationGrantType; + } + } + + public String getValue() { + return authorizationGrantType; + } + + public boolean isSameGrantType(AuthorizationGrantType grantType) { + return this.authorizationGrantType.equals(grantType.getValue()); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADAuthorizationServerEndpoints.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADAuthorizationServerEndpoints.java new file mode 100644 index 0000000000000..5c8d37887b3a1 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADAuthorizationServerEndpoints.java @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad; + +import com.nimbusds.oauth2.sdk.util.StringUtils; + +/** + * Used to get endpoints for Microsoft Identity authorization server. + */ +public class AADAuthorizationServerEndpoints { + + private static final String DEFAULT_BASE_URI = "https://login.microsoftonline.com/"; + + private static final String AUTHORIZATION_ENDPOINT = "/oauth2/v2.0/authorize"; + private static final String TOKEN_ENDPOINT = "/oauth2/v2.0/token"; + private static final String JWK_SET_ENDPOINT = "/discovery/v2.0/keys"; + private static final String END_SESSION_ENDPOINT = "/oauth2/v2.0/logout"; + + private final String baseUri; + private final String tenantId; + + public AADAuthorizationServerEndpoints(String baseUri, String tenantId) { + if (StringUtils.isBlank(baseUri)) { + baseUri = DEFAULT_BASE_URI; + } + this.baseUri = addSlash(baseUri); + this.tenantId = tenantId; + } + + public String getBaseUri() { + return this.baseUri; + } + + private String addSlash(String uri) { + return uri.endsWith("/") ? uri : uri + "/"; + } + + public String authorizationEndpoint() { + return baseUri + tenantId + AUTHORIZATION_ENDPOINT; + } + + public String tokenEndpoint() { + return baseUri + tenantId + TOKEN_ENDPOINT; + } + + public String jwkSetEndpoint() { + return baseUri + tenantId + JWK_SET_ENDPOINT; + } + + public String endSessionEndpoint() { + return baseUri + tenantId + END_SESSION_ENDPOINT; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADClientRegistrationRepository.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADClientRegistrationRepository.java new file mode 100644 index 0000000000000..428893268949f --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADClientRegistrationRepository.java @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp.AuthorizationClientProperties; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.AADAuthenticationProperties; +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.util.Assert; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationGrantType.AUTHORIZATION_CODE; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationGrantType.AZURE_DELEGATED; + + +/** + * Manage all AAD OAuth2 clients configured by property "azure.activedirectory.xxx". + * Do extra works: + * 1. Make "azure" client's scope contains all "azure_delegated" clients' scope. + * This scope is used to request authorize code. + * 2. Save azureClientAccessTokenScopes, this scope is used to request "azure" client's access_token. + */ +public class AADClientRegistrationRepository implements ClientRegistrationRepository, Iterable { + + public static final String AZURE_CLIENT_REGISTRATION_ID = "azure"; + + private final Set azureClientAccessTokenScopes; + private final Map allClients; + + public AADClientRegistrationRepository(AADAuthenticationProperties properties) { + Set accessTokenScopes = azureClientAccessTokenScopes(properties); // Used to get access_token + Set delegatedScopes = delegatedClientsAccessTokenScopes(properties); + Set authorizationCodeScopes = new HashSet<>(); // Used to get authorization code. + authorizationCodeScopes.addAll(accessTokenScopes); + authorizationCodeScopes.addAll(delegatedScopes); + if (resourceServerCount(accessTokenScopes) == 0 && resourceServerCount((authorizationCodeScopes)) > 1) { + // AAD server will return error if: + // 1. authorizationCodeScopes have more than one resource server. + // 2. accessTokenScopes have no resource server + String newScope = properties.getGraphBaseUri() + "User.Read"; + accessTokenScopes.add(newScope); + authorizationCodeScopes.add(newScope); + } + this.azureClientAccessTokenScopes = accessTokenScopes; + this.allClients = + properties.getAuthorizationClients() + .entrySet() + .stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> toClientRegistration(entry.getKey(), entry.getValue().getAuthorizationGrantType(), + entry.getValue().getScopes(), properties))); + ClientRegistration azureClient = + toClientRegistration(AZURE_CLIENT_REGISTRATION_ID, AUTHORIZATION_CODE, authorizationCodeScopes, properties); + allClients.put(AZURE_CLIENT_REGISTRATION_ID, azureClient); + } + + public Set getAzureClientAccessTokenScopes() { + return azureClientAccessTokenScopes; + } + + @Override + public ClientRegistration findByRegistrationId(String registrationId) { + Assert.hasText(registrationId, "registrationId cannot be empty"); + return allClients.get(registrationId); + } + + @Override + public Iterator iterator() { + return allClients.values() + .stream() + .filter(client -> + client.getAuthorizationGrantType().getValue().equals(AUTHORIZATION_CODE.getValue())) + .iterator(); + } + + private Set azureClientAccessTokenScopes(AADAuthenticationProperties properties) { + Set result = Optional.of(properties) + .map(AADAuthenticationProperties::getAuthorizationClients) + .map(clients -> clients.get(AZURE_CLIENT_REGISTRATION_ID)) + .map(AuthorizationClientProperties::getScopes) + .map(HashSet::new) + .orElseGet(HashSet::new); + if (!result.contains("openid")) { + result.add("openid"); // "openid" allows to request an ID token. + } + if (!result.contains("profile")) { + result.add("profile"); // "profile" allows to return additional claims in the ID token. + } + if (!result.contains("offline_access")) { + result.add("offline_access"); // "offline_access" allows to request a refresh token. + } + // About "Directory.Read.All" and "User.Read", please refer to: + // 1. https://docs.microsoft.com/en-us/graph/permissions-reference + // 2. https://github.com/Azure/azure-sdk-for-java/issues/21284#issuecomment-888725241 + if (properties.allowedGroupNamesConfigured()) { + // "Directory.Read.All" allows to get group id and group name. + result.add(properties.getGraphBaseUri() + "Directory.Read.All"); + } else if (properties.allowedGroupIdsConfigured()) { + // "User.Read" allows to get group id, but not allow to get group name. + result.add(properties.getGraphBaseUri() + "User.Read"); + } + return result; + } + + private Set delegatedClientsAccessTokenScopes(AADAuthenticationProperties properties) { + return properties.getAuthorizationClients() + .values() + .stream() + .filter(p -> AZURE_DELEGATED.getValue().equals(p.getAuthorizationGrantType().getValue())) + .flatMap(p -> p.getScopes().stream()) + .collect(Collectors.toSet()); + } + + private ClientRegistration toClientRegistration(String registrationId, + AADAuthorizationGrantType aadAuthorizationGrantType, + Collection scopes, + AADAuthenticationProperties properties) { + AADAuthorizationServerEndpoints endpoints = + new AADAuthorizationServerEndpoints(properties.getBaseUri(), properties.getTenantId()); + return ClientRegistration.withRegistrationId(registrationId) + .clientName(registrationId) + .authorizationGrantType(new AuthorizationGrantType((aadAuthorizationGrantType.getValue()))) + .scope(scopes) + .redirectUri(properties.getRedirectUriTemplate()) + .userNameAttributeName(properties.getUserNameAttribute()) + .clientId(properties.getClientId()) + .clientSecret(properties.getClientSecret()) + .authorizationUri(endpoints.authorizationEndpoint()) + .tokenUri(endpoints.tokenEndpoint()) + .jwkSetUri(endpoints.jwkSetEndpoint()) + .providerConfigurationMetadata(providerConfigurationMetadata(endpoints)) + .build(); + } + + private Map providerConfigurationMetadata(AADAuthorizationServerEndpoints endpoints) { + Map result = new LinkedHashMap<>(); + String endSessionEndpoint = endpoints.endSessionEndpoint(); + result.put("end_session_endpoint", endSessionEndpoint); + return result; + } + + public static int resourceServerCount(Set scopes) { + return (int) scopes.stream() + .filter(scope -> scope.contains("/")) + .map(scope -> scope.substring(0, scope.lastIndexOf('/'))) + .distinct() + .count(); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADIssuerJWSKeySelector.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADIssuerJWSKeySelector.java new file mode 100644 index 0000000000000..2f1ff190b0fa1 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADIssuerJWSKeySelector.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad; + + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.constants.AADTokenClaim; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.KeySourceException; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import com.nimbusds.jose.proc.JWSAlgorithmFamilyJWSKeySelector; +import com.nimbusds.jose.proc.JWSKeySelector; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jose.util.DefaultResourceRetriever; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.proc.JWTClaimsSetAwareJWSKeySelector; + +import java.net.URL; +import java.security.Key; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Selecting key candidates for processing a signed JWT which provides access to the JWT claims set in addition to the + * JWS header. + */ +public class AADIssuerJWSKeySelector implements JWTClaimsSetAwareJWSKeySelector { + + private final AADTrustedIssuerRepository trustedIssuerRepo; + + private final int connectTimeout; + + private final int readTimeout; + + private final int sizeLimit; + + private final Map> selectors = new ConcurrentHashMap<>(); + + public AADIssuerJWSKeySelector(AADTrustedIssuerRepository trustedIssuerRepo, + int connectTimeout, + int readTimeout, int sizeLimit) { + this.trustedIssuerRepo = trustedIssuerRepo; + this.connectTimeout = connectTimeout; + this.readTimeout = readTimeout; + this.sizeLimit = sizeLimit; + } + + @Override + public List selectKeys(JWSHeader header, JWTClaimsSet claimsSet, SecurityContext context) + throws KeySourceException { + String iss = (String) claimsSet.getClaim(AADTokenClaim.ISS); + if (trustedIssuerRepo.isTrusted(iss)) { + return selectors.computeIfAbsent(iss, this::fromIssuer).selectJWSKeys(header, context); + } + throw new IllegalArgumentException("The issuer: '" + iss + "' is not registered in trusted issuer repository," + + " so cannot create JWSKeySelector."); + } + + private JWSKeySelector fromIssuer(String issuer) { + Map configurationForOidcIssuerLocation = AADJwtDecoderProviderConfiguration + .getConfigurationForOidcIssuerLocation(getOidcIssuerLocation(issuer)); + String uri = configurationForOidcIssuerLocation.get("jwks_uri").toString(); + DefaultResourceRetriever jwkSetRetriever = + new DefaultResourceRetriever(connectTimeout, readTimeout, sizeLimit); + try { + JWKSource jwkSource = new RemoteJWKSet<>(new URL(uri), jwkSetRetriever); + return JWSAlgorithmFamilyJWSKeySelector.fromJWKSource(jwkSource); + } catch (Exception ex) { + throw new IllegalArgumentException(ex.getMessage(), ex); + } + } + + private String getOidcIssuerLocation(String issuer) { + if (trustedIssuerRepo.hasSpecialOidcIssuerLocation(issuer)) { + return trustedIssuerRepo.getSpecialOidcIssuerLocation(issuer); + } + return issuer; + } + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADJwtDecoderProviderConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADJwtDecoderProviderConfiguration.java new file mode 100644 index 0000000000000..d26418db350f1 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADJwtDecoderProviderConfiguration.java @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.util.Collections; +import java.util.Map; + +/** + * Allows resolving configuration from an + * OpenID Provider + * Configuration or + * Authorization Server Metadata Request based on + * provided issuer and method invoked. + */ +public class AADJwtDecoderProviderConfiguration { + + private static final String OIDC_METADATA_PATH = "/.well-known/openid-configuration"; + private static final RestTemplate REST = new RestTemplate(); + private static final ParameterizedTypeReference> TYPE_REFERENCE = + new ParameterizedTypeReference>() { + }; + + public static Map getConfigurationForOidcIssuerLocation(String oidcIssuerLocation) { + return getConfiguration(oidcIssuerLocation, oidc(URI.create(oidcIssuerLocation))); + } + + private static Map getConfiguration(String issuer, URI... uris) { + String errorMessage = "Unable to resolve the Configuration with the provided Issuer of " + issuer; + + for (URI uri : uris) { + try { + RequestEntity request = RequestEntity.get(uri).build(); + ResponseEntity> response = REST.exchange(request, + TYPE_REFERENCE); + Map configuration = response.getBody(); + if (configuration == null) { + throw new IllegalArgumentException("The configuration must not be null"); + } + if (configuration.get("jwks_uri") == null) { + throw new IllegalArgumentException("The public JWK set URI must not be null"); + } + + return configuration; + } catch (IllegalArgumentException e) { + throw e; + } catch (RuntimeException e) { + if (!(e instanceof HttpClientErrorException + && ((HttpClientErrorException) e).getStatusCode().is4xxClientError())) { + throw new IllegalArgumentException(errorMessage, e); + } + // else try another endpoint + } + } + throw new IllegalArgumentException(errorMessage); + } + + private static URI oidc(URI issuer) { + return UriComponentsBuilder.fromUri(issuer) + .replacePath(issuer.getPath() + OIDC_METADATA_PATH) + .build(Collections.emptyMap()); + } + +} + diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADJwtGrantedAuthoritiesConverter.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADJwtGrantedAuthoritiesConverter.java new file mode 100644 index 0000000000000..1eaebed2df024 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADJwtGrantedAuthoritiesConverter.java @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi.AADResourceServerProperties.DEFAULT_CLAIM_TO_AUTHORITY_PREFIX_MAP; + +/** + * Extracts the {@link GrantedAuthority}s from scope attributes typically found in a {@link Jwt}. + */ +public class AADJwtGrantedAuthoritiesConverter implements Converter> { + + private static final Logger LOGGER = LoggerFactory.getLogger(AADJwtGrantedAuthoritiesConverter.class); + + private final Map claimToAuthorityPrefixMap; + + public AADJwtGrantedAuthoritiesConverter() { + claimToAuthorityPrefixMap = DEFAULT_CLAIM_TO_AUTHORITY_PREFIX_MAP; + } + + public AADJwtGrantedAuthoritiesConverter(Map claimToAuthorityPrefixMap) { + this.claimToAuthorityPrefixMap = claimToAuthorityPrefixMap; + } + + @Override + public Collection convert(Jwt jwt) { + Collection grantedAuthorities = new ArrayList<>(); + claimToAuthorityPrefixMap.forEach((authoritiesClaimName, authorityPrefix) -> + Optional.of(authoritiesClaimName) + .map(jwt::getClaim) + .map(this::getClaimValueAsCollection) + .map(Collection::stream) + .orElseGet(Stream::empty) + .map(authority -> authorityPrefix + authority) + .map(SimpleGrantedAuthority::new) + .forEach(grantedAuthorities::add)); + LOGGER.debug("User {}'s authorities created from jwt token: {}.", jwt.getSubject(), grantedAuthorities); + return grantedAuthorities; + } + + private Collection getClaimValueAsCollection(Object claimValue) { + if (claimValue instanceof String) { + return Arrays.asList(((String) claimValue).split(" ")); + } else if (claimValue instanceof Collection) { + return (Collection) claimValue; + } else { + return Collections.emptyList(); + } + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADOAuth2AuthenticatedPrincipal.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADOAuth2AuthenticatedPrincipal.java new file mode 100644 index 0000000000000..d685e38d2c57c --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADOAuth2AuthenticatedPrincipal.java @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad; + +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.util.Assert; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Map.Entry; + +import static org.springframework.security.core.authority.AuthorityUtils.NO_AUTHORITIES; + +/** + * Entity class of AADOAuth2AuthenticatedPrincipal + */ +public class AADOAuth2AuthenticatedPrincipal implements OAuth2AuthenticatedPrincipal, Serializable { + + private static final long serialVersionUID = -3625690847771476854L; + + private static final String PERSONAL_ACCOUNT_TENANT_ID = "9188040d-6c67-4c5b-b112-36a304b66dad"; + + private final Collection authorities; + + private final Map headers; + + private final Map attributes; + + private final String tokenValue; + + private JWTClaimsSet jwtClaimsSet; + + private final String name; + + public AADOAuth2AuthenticatedPrincipal(Map headers, + Map attributes, + Collection authorities, + String tokenValue, + String name) { + Assert.notEmpty(attributes, "attributes cannot be empty"); + Assert.notEmpty(headers, "headers cannot be empty"); + this.headers = headers; + this.tokenValue = tokenValue; + this.attributes = Collections.unmodifiableMap(attributes); + this.authorities = authorities == null ? NO_AUTHORITIES : Collections.unmodifiableCollection(authorities); + this.name = name; + toJwtClaimsSet(attributes); + } + + private void toJwtClaimsSet(Map attributes) { + Builder builder = new Builder(); + for (Entry entry : attributes.entrySet()) { + builder.claim(entry.getKey(), entry.getValue()); + } + this.jwtClaimsSet = builder.build(); + } + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public String getName() { + return this.name; + } + + public String getTokenValue() { + return tokenValue; + } + + public Map getHeaders() { + return headers; + } + + public JWTClaimsSet getJwtClaimsSet() { + return jwtClaimsSet; + } + + public String getIssuer() { + return jwtClaimsSet == null ? null : jwtClaimsSet.getIssuer(); + } + + public String getSubject() { + return jwtClaimsSet == null ? null : jwtClaimsSet.getSubject(); + } + + public Map getClaims() { + return jwtClaimsSet == null ? null : jwtClaimsSet.getClaims(); + } + + public Object getClaim(String name) { + return jwtClaimsSet == null ? null : jwtClaimsSet.getClaim(name); + } + + public String getTenantId() { + return jwtClaimsSet == null ? null : (String) jwtClaimsSet.getClaim("tid"); + } + + public boolean isPersonalAccount() { + return PERSONAL_ACCOUNT_TENANT_ID.equals(getTenantId()); + } + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADOAuth2ClientConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADOAuth2ClientConfiguration.java new file mode 100644 index 0000000000000..49baec93b26b4 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADOAuth2ClientConfiguration.java @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi.AADOBOOAuth2AuthorizedClientProvider; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp.AADAzureDelegatedOAuth2AuthorizedClientProvider; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.AADAuthenticationProperties; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.condition.aad.ClientRegistrationCondition; +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.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.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; + +/** + *

+ * The configuration will not be activated if no {@link ClientRegistration} classes provided. + *

+ */ +@Configuration(proxyBeanMethods = false) +@Conditional(ClientRegistrationCondition.class) +public class AADOAuth2ClientConfiguration { + + @Bean + @ConditionalOnMissingBean + public ClientRegistrationRepository clientRegistrationRepository(AADAuthenticationProperties properties) { + return new AADClientRegistrationRepository(properties); + } + + @Bean + @ConditionalOnMissingBean + public OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository() { + return new JacksonHttpSessionOAuth2AuthorizedClientRepository(); + } + + @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; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADTrustedIssuerRepository.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADTrustedIssuerRepository.java new file mode 100644 index 0000000000000..fd3fdba501dba --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADTrustedIssuerRepository.java @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c.AADB2CTrustedIssuerRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.Assert; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.Locale.ROOT; + +/** + * A tenant id is used to construct the trusted issuer repository. + */ +public class AADTrustedIssuerRepository { + + private static final Logger LOGGER = LoggerFactory.getLogger(AADTrustedIssuerRepository.class); + + private static final String LOGIN_MICROSOFT_ONLINE_ISSUER = "https://login.microsoftonline.com/"; + + private static final String STS_WINDOWS_ISSUER = "https://sts.windows.net/"; + + private static final String STS_CHINA_CLOUD_API_ISSUER = "https://sts.chinacloudapi.cn/"; + + private static final String PATH_DELIMITER = "/"; + + private static final String PATH_DELIMITER_V2 = "/v2.0"; + + private final Set trustedIssuers = new HashSet<>(); + + /** + * Place a mapping that cannot access metadata through issuer splicing /.well-known/openid-configuration. + */ + private final Map specialOidcIssuerLocationMap = new HashMap<>(); + + protected String tenantId; + + public AADTrustedIssuerRepository(String tenantId) { + this.tenantId = tenantId; + trustedIssuers.addAll(buildAADIssuers(PATH_DELIMITER)); + trustedIssuers.addAll(buildAADIssuers(PATH_DELIMITER_V2)); + } + + private List buildAADIssuers(String delimiter) { + return Stream.of(LOGIN_MICROSOFT_ONLINE_ISSUER, STS_WINDOWS_ISSUER, STS_CHINA_CLOUD_API_ISSUER) + .map(s -> s + tenantId + delimiter) + .collect(Collectors.toList()); + } + + public Set getTrustedIssuers() { + return Collections.unmodifiableSet(trustedIssuers); + } + + public boolean addTrustedIssuer(String... issuers) { + return trustedIssuers.addAll(Arrays.asList(issuers)); + } + + public boolean addTrustedIssuer(String issuer, String oidcIssuerLocation) { + specialOidcIssuerLocationMap.put(issuer, oidcIssuerLocation); + return trustedIssuers.add(issuer); + } + + public boolean isTrusted(String issuer) { + return this.trustedIssuers.contains(issuer); + } + + public boolean hasSpecialOidcIssuerLocation(String issuer) { + return this.specialOidcIssuerLocationMap.containsKey(issuer); + } + + public String getSpecialOidcIssuerLocation(String issuer) { + return this.specialOidcIssuerLocationMap.get(issuer); + } + + @Deprecated + public void addB2CIssuer(String baseUri) { + Assert.notNull(baseUri, "baseUri cannot be null."); + String resolvedBaseUri = resolveBaseUri(baseUri); + trustedIssuers.add(String.format("%s/%s/v2.0/", resolvedBaseUri, tenantId)); + } + + /** + * Only the V2 version of Access Token is supported when using Azure AD B2C user flows. + * + * @param baseUri The base uri is the domain part of the endpoint. + * @param userFlows The all user flows mapping which is created under b2c tenant. + * @deprecated Is not recommended in {@link AADTrustedIssuerRepository} to add AAD B2C related content. See {@link + * AADB2CTrustedIssuerRepository}. + */ + @Deprecated + public void addB2CUserFlowIssuers(String baseUri, Map userFlows) { + Assert.notNull(userFlows, "userFlows cannot be null."); + String resolvedBaseUri = resolveBaseUri(baseUri); + userFlows.keySet().forEach(key -> createB2CUserFlowIssuer(resolvedBaseUri, userFlows.get(key))); + } + + @Deprecated + private void createB2CUserFlowIssuer(String resolvedBaseUri, String userFlowName) { + trustedIssuers.add(String.format("%s/tfp/%s/%s/v2.0/", resolvedBaseUri, tenantId, + userFlowName.toLowerCase(ROOT))); + } + + /** + * Resolve the base uri to get scheme and host. + * + * @param baseUri baseUri Base uri in the configuration file. + * @return the parsed base uri. + * @throws RuntimeException thrown if the uri is not valid. + */ + protected String resolveBaseUri(String baseUri) { + Assert.notNull(baseUri, "baseUri cannot be null"); + try { + URI uri = new URI(baseUri); + return uri.getScheme() + "://" + uri.getHost(); + } catch (URISyntaxException e) { + LOGGER.error("Resolve the base uri exception."); + throw new RuntimeException("Resolve the base uri:'" + baseUri + "' exception."); + } + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AbstractOAuth2AuthorizationCodeGrantRequestEntityConverter.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AbstractOAuth2AuthorizationCodeGrantRequestEntityConverter.java new file mode 100644 index 0000000000000..40b480cde5387 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AbstractOAuth2AuthorizationCodeGrantRequestEntityConverter.java @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad; + +import com.azure.spring.core.AzureSpringIdentifier; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.RequestEntity; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter; +import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; + +import java.util.Collections; +import java.util.Optional; +import java.util.UUID; + +/** + * When using "auth-code" in AAD and AAD B2C, it's used to expand head and body parameters of the request. + */ +public abstract class AbstractOAuth2AuthorizationCodeGrantRequestEntityConverter + extends OAuth2AuthorizationCodeGrantRequestEntityConverter { + + protected abstract String getApplicationId(); + + @Override + @SuppressWarnings("unchecked") + public RequestEntity convert(OAuth2AuthorizationCodeGrantRequest request) { + RequestEntity requestEntity = super.convert(request); + Assert.notNull(requestEntity, "requestEntity can not be null"); + + HttpHeaders httpHeaders = getHttpHeaders(); + Optional.of(requestEntity) + .map(HttpEntity::getHeaders) + .ifPresent(headers -> headers.forEach(httpHeaders::put)); + MultiValueMap body = (MultiValueMap) requestEntity.getBody(); + Assert.notNull(body, "body can not be null"); + Optional.ofNullable(getHttpBody(request)).ifPresent(body::putAll); + return new RequestEntity<>(body, httpHeaders, requestEntity.getMethod(), requestEntity.getUrl()); + } + + /** + * Additional default headers information. + * @return HttpHeaders + */ + public HttpHeaders getHttpHeaders() { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.put("x-client-SKU", Collections.singletonList(getApplicationId())); + httpHeaders.put("x-client-VER", Collections.singletonList(AzureSpringIdentifier.VERSION)); + httpHeaders.put("client-request-id", Collections.singletonList(UUID.randomUUID().toString())); + return httpHeaders; + } + + /** + * Default body of OAuth2AuthorizationCodeGrantRequest. + * @param request OAuth2AuthorizationCodeGrantRequest + * @return MultiValueMap + */ + public MultiValueMap getHttpBody(OAuth2AuthorizationCodeGrantRequest request) { + return null; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/JacksonHttpSessionOAuth2AuthorizedClientRepository.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/JacksonHttpSessionOAuth2AuthorizedClientRepository.java new file mode 100644 index 0000000000000..fd4d390a3576b --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/JacksonHttpSessionOAuth2AuthorizedClientRepository.java @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.util.Assert; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.jackson.SerializerUtils.deserializeOAuth2AuthorizedClientMap; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.jackson.SerializerUtils.serializeOAuth2AuthorizedClientMap; + +/** + * An implementation of an {@link OAuth2AuthorizedClientRepository} that stores {@link OAuth2AuthorizedClient}'s in the + * {@code HttpSession}. To make it compatible with different spring versions. Refs: + * https://github.com/spring-projects/spring-security/issues/9204 + * + * @see OAuth2AuthorizedClientRepository + * @see OAuth2AuthorizedClient + */ +public class JacksonHttpSessionOAuth2AuthorizedClientRepository implements OAuth2AuthorizedClientRepository { + private static final String AUTHORIZED_CLIENTS_ATTR_NAME = + JacksonHttpSessionOAuth2AuthorizedClientRepository.class.getName() + ".AUTHORIZED_CLIENTS"; + + @SuppressWarnings("unchecked") + @Override + public T loadAuthorizedClient(String clientRegistrationId, + Authentication principal, + HttpServletRequest request) { + Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty"); + Assert.notNull(request, "request cannot be null"); + return (T) this.getAuthorizedClients(request).get(clientRegistrationId); + } + + @Override + public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal, + HttpServletRequest request, HttpServletResponse response) { + Assert.notNull(authorizedClient, "authorizedClient cannot be null"); + Assert.notNull(request, "request cannot be null"); + Assert.notNull(response, "response cannot be null"); + Map authorizedClients = this.getAuthorizedClients(request); + authorizedClients.put(authorizedClient.getClientRegistration().getRegistrationId(), authorizedClient); + request.getSession().setAttribute(AUTHORIZED_CLIENTS_ATTR_NAME, + serializeOAuth2AuthorizedClientMap(authorizedClients)); + } + + @Override + public void removeAuthorizedClient(String clientRegistrationId, Authentication principal, + HttpServletRequest request, HttpServletResponse response) { + Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty"); + Assert.notNull(request, "request cannot be null"); + Map authorizedClients = this.getAuthorizedClients(request); + if (!authorizedClients.isEmpty()) { + if (authorizedClients.remove(clientRegistrationId) != null) { + if (!authorizedClients.isEmpty()) { + request.getSession().setAttribute(AUTHORIZED_CLIENTS_ATTR_NAME, + serializeOAuth2AuthorizedClientMap(authorizedClients)); + } else { + request.getSession().removeAttribute(AUTHORIZED_CLIENTS_ATTR_NAME); + } + } + } + } + + private Map getAuthorizedClients(HttpServletRequest request) { + HttpSession session = request.getSession(false); + String authorizedClientsString = (String) Optional.ofNullable(session) + .map(s -> s.getAttribute(AUTHORIZED_CLIENTS_ATTR_NAME)) + .orElse(null); + if (authorizedClientsString == null) { + return new HashMap<>(); + } + return deserializeOAuth2AuthorizedClientMap(authorizedClientsString); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/constants/AADTokenClaim.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/constants/AADTokenClaim.java new file mode 100644 index 0000000000000..44b5e5cc5722c --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/constants/AADTokenClaim.java @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.constants; + +/** + * Refs: https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens + */ +public class AADTokenClaim { + + public static final String AUD = "aud"; + public static final String ISS = "iss"; + public static final String NAME = "name"; + public static final String ROLES = "roles"; + public static final String SCP = "scp"; + public static final String SUB = "sub"; + public static final String TID = "tid"; +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/constants/AuthorityPrefix.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/constants/AuthorityPrefix.java new file mode 100644 index 0000000000000..bd1c78c79987c --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/constants/AuthorityPrefix.java @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.constants; + +/** + * Authority prefix + */ +public class AuthorityPrefix { + + public static final String APP_ROLE = "APPROLE_"; // Used for resource-server. + public static final String ROLE = "ROLE_"; // Used for web-application. (Except for AADAppRoleStatelessAuthenticationFilter, and AADAppRoleStatelessAuthenticationFilter is depreecated.) + public static final String SCOPE = "SCOPE_"; // Used for resource-server + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/AADClientRegistrationDeserializer.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/AADClientRegistrationDeserializer.java new file mode 100644 index 0000000000000..7f7ef5ba3d4ea --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/AADClientRegistrationDeserializer.java @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.util.StdConverter; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthenticationMethod; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; + +import java.io.IOException; + +public class AADClientRegistrationDeserializer extends JsonDeserializer { + + private static final StdConverter CLIENT_AUTHENTICATION_METHOD_CONVERTER = + new StdConverters.ClientAuthenticationMethodConverter(); + + private static final StdConverter AUTHORIZATION_GRANT_TYPE_CONVERTER = + new StdConverters.AuthorizationGrantTypeConverter(); + + private static final StdConverter AUTHENTICATION_METHOD_CONVERTER = + new StdConverters.AuthenticationMethodConverter(); + + @Override + public ClientRegistration deserialize(JsonParser parser, DeserializationContext context) throws IOException { + ObjectMapper mapper = (ObjectMapper) parser.getCodec(); + JsonNode clientRegistrationNode = mapper.readTree(parser); + JsonNode providerDetailsNode = JsonNodeUtils.findObjectNode(clientRegistrationNode, "providerDetails"); + JsonNode userInfoEndpointNode = JsonNodeUtils.findObjectNode(providerDetailsNode, "userInfoEndpoint"); + return ClientRegistration + .withRegistrationId(JsonNodeUtils.findStringValue(clientRegistrationNode, "registrationId")) + .clientId(JsonNodeUtils.findStringValue(clientRegistrationNode, "clientId")) + .clientSecret(JsonNodeUtils.findStringValue(clientRegistrationNode, "clientSecret")) + .clientAuthenticationMethod(CLIENT_AUTHENTICATION_METHOD_CONVERTER + .convert(JsonNodeUtils.findObjectNode(clientRegistrationNode, "clientAuthenticationMethod"))) + .authorizationGrantType(AUTHORIZATION_GRANT_TYPE_CONVERTER + .convert(JsonNodeUtils.findObjectNode(clientRegistrationNode, "authorizationGrantType"))) + .redirectUri(JsonNodeUtils.findStringValue(clientRegistrationNode, "redirectUri")) + .scope(JsonNodeUtils.findValue(clientRegistrationNode, "scopes", JsonNodeUtils.STRING_SET, mapper)) + .clientName(JsonNodeUtils.findStringValue(clientRegistrationNode, "clientName")) + .authorizationUri(JsonNodeUtils.findStringValue(providerDetailsNode, "authorizationUri")) + .tokenUri(JsonNodeUtils.findStringValue(providerDetailsNode, "tokenUri")) + .userInfoUri(JsonNodeUtils.findStringValue(userInfoEndpointNode, "uri")) + .userInfoAuthenticationMethod(AUTHENTICATION_METHOD_CONVERTER + .convert(JsonNodeUtils.findObjectNode(userInfoEndpointNode, "authenticationMethod"))) + .userNameAttributeName(JsonNodeUtils.findStringValue(userInfoEndpointNode, "userNameAttributeName")) + .jwkSetUri(JsonNodeUtils.findStringValue(providerDetailsNode, "jwkSetUri")) + .issuerUri(JsonNodeUtils.findStringValue(providerDetailsNode, "issuerUri")) + .providerConfigurationMetadata(JsonNodeUtils.findValue(providerDetailsNode, "configurationMetadata", + JsonNodeUtils.STRING_OBJECT_MAP, mapper)) + .build(); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/AADClientRegistrationMixin.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/AADClientRegistrationMixin.java new file mode 100644 index 0000000000000..3cec8b4cbd9f6 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/AADClientRegistrationMixin.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonDeserialize(using = AADClientRegistrationDeserializer.class) +@JsonAutoDetect( + fieldVisibility = JsonAutoDetect.Visibility.ANY, + getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class AADClientRegistrationMixin { + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/AADOAuth2ClientJackson2Module.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/AADOAuth2ClientJackson2Module.java new file mode 100644 index 0000000000000..b1c3b80986d7b --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/AADOAuth2ClientJackson2Module.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.jackson; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.springframework.security.oauth2.client.registration.ClientRegistration; + +/** + * Jackson {@code Module} for ClientRegistration + */ +public class AADOAuth2ClientJackson2Module extends SimpleModule { + + private static final long serialVersionUID = 30_80_00L; + + public AADOAuth2ClientJackson2Module() { + super(AADOAuth2ClientJackson2Module.class.getName(), new Version(3, 8, 0, null, null, null)); + } + + @Override + public void setupModule(SetupContext context) { + context.setMixInAnnotations(ClientRegistration.class, AADClientRegistrationMixin.class); + } + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/JsonNodeUtils.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/JsonNodeUtils.java new file mode 100644 index 0000000000000..56d6f125cee8e --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/JsonNodeUtils.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.jackson; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.Map; +import java.util.Set; + +abstract class JsonNodeUtils { + + static final TypeReference> STRING_SET = new TypeReference>() { + }; + + static final TypeReference> STRING_OBJECT_MAP = new TypeReference>() { + }; + + static String findStringValue(JsonNode jsonNode, String fieldName) { + if (jsonNode == null) { + return null; + } + JsonNode value = jsonNode.findValue(fieldName); + return (value != null && value.isTextual()) ? value.asText() : null; + } + + static T findValue(JsonNode jsonNode, String fieldName, TypeReference valueTypeReference, + ObjectMapper mapper) { + if (jsonNode == null) { + return null; + } + JsonNode value = jsonNode.findValue(fieldName); + return (value != null && value.isContainerNode()) ? mapper.convertValue(value, valueTypeReference) : null; + } + + static JsonNode findObjectNode(JsonNode jsonNode, String fieldName) { + if (jsonNode == null) { + return null; + } + JsonNode value = jsonNode.findValue(fieldName); + return (value != null && value.isObject()) ? value : null; + } + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/SerializerUtils.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/SerializerUtils.java new file mode 100644 index 0000000000000..af51b9ce1ce19 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/SerializerUtils.java @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.jackson; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.security.jackson2.CoreJackson2Module; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.jackson2.OAuth2ClientJackson2Module; + +import java.util.HashMap; +import java.util.Map; + +public class SerializerUtils { + private static final ObjectMapper OBJECT_MAPPER; + private static final TypeReference> TYPE_REFERENCE = + new TypeReference>() { + }; + + static { + OBJECT_MAPPER = new ObjectMapper(); + OBJECT_MAPPER.registerModule(new OAuth2ClientJackson2Module()); + // Use to handle problem: OAuth2ClientJackson2Module does not support self-defined ClientRegistration type. + // For example: "on_behalf_on" or "azure_delegated". + // TODO(rujche) Delete this after OAuth2ClientJackson2Module support self-defined ClientRegistration type. + OBJECT_MAPPER.registerModule(new AADOAuth2ClientJackson2Module()); + OBJECT_MAPPER.registerModule(new CoreJackson2Module()); + OBJECT_MAPPER.registerModule(new JavaTimeModule()); + } + + public static String serializeOAuth2AuthorizedClientMap(Map authorizedClients) { + String result; + try { + result = OBJECT_MAPPER.writeValueAsString(authorizedClients); + } catch (JsonProcessingException e) { + throw new IllegalStateException(e); + } + return result; + } + + public static Map deserializeOAuth2AuthorizedClientMap(String authorizedClientsString) { + if (authorizedClientsString == null) { + return new HashMap<>(); + } + Map authorizedClients; + try { + authorizedClients = OBJECT_MAPPER.readValue(authorizedClientsString, TYPE_REFERENCE); + } catch (JsonProcessingException e) { + throw new IllegalStateException(e); + } + return authorizedClients; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/StdConverters.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/StdConverters.java new file mode 100644 index 0000000000000..069cad9128ccc --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/StdConverters.java @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.jackson; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.util.StdConverter; +import org.springframework.security.oauth2.core.AuthenticationMethod; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; + +abstract class StdConverters { + + static final class ClientAuthenticationMethodConverter extends StdConverter { + + @Override + public ClientAuthenticationMethod convert(JsonNode jsonNode) { + String value = JsonNodeUtils.findStringValue(jsonNode, "value"); + if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue().equalsIgnoreCase(value) + || ClientAuthenticationMethod.BASIC.getValue().equalsIgnoreCase(value)) { + return ClientAuthenticationMethod.CLIENT_SECRET_BASIC; + } + if (ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue().equalsIgnoreCase(value) + || ClientAuthenticationMethod.POST.getValue().equalsIgnoreCase(value)) { + return ClientAuthenticationMethod.CLIENT_SECRET_POST; + } + if (ClientAuthenticationMethod.NONE.getValue().equalsIgnoreCase(value)) { + return ClientAuthenticationMethod.NONE; + } + return null; + } + + } + + static final class AuthorizationGrantTypeConverter extends StdConverter { + + @Override + public AuthorizationGrantType convert(JsonNode jsonNode) { + String value = JsonNodeUtils.findStringValue(jsonNode, "value"); + if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equalsIgnoreCase(value)) { + return AuthorizationGrantType.AUTHORIZATION_CODE; + } + if (AuthorizationGrantType.IMPLICIT.getValue().equalsIgnoreCase(value)) { + return AuthorizationGrantType.IMPLICIT; + } + if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equalsIgnoreCase(value)) { + return AuthorizationGrantType.CLIENT_CREDENTIALS; + } + if (AuthorizationGrantType.PASSWORD.getValue().equalsIgnoreCase(value)) { + return AuthorizationGrantType.PASSWORD; + } + return new AuthorizationGrantType(value); + } + + } + + static final class AuthenticationMethodConverter extends StdConverter { + + @Override + public AuthenticationMethod convert(JsonNode jsonNode) { + String value = JsonNodeUtils.findStringValue(jsonNode, "value"); + if (AuthenticationMethod.HEADER.getValue().equalsIgnoreCase(value)) { + return AuthenticationMethod.HEADER; + } + if (AuthenticationMethod.FORM.getValue().equalsIgnoreCase(value)) { + return AuthenticationMethod.FORM; + } + if (AuthenticationMethod.QUERY.getValue().equalsIgnoreCase(value)) { + return AuthenticationMethod.QUERY; + } + return null; + } + + } + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/package-info.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/package-info.java new file mode 100644 index 0000000000000..e03397cc583da --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/package-info.java @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/** + * Package com.azure.spring.aad + */ +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad; diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADJwtBearerTokenAuthenticationConverter.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADJwtBearerTokenAuthenticationConverter.java new file mode 100644 index 0000000000000..010d9608d4754 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADJwtBearerTokenAuthenticationConverter.java @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADJwtGrantedAuthoritiesConverter; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADOAuth2AuthenticatedPrincipal; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.constants.AADTokenClaim; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.constants.AuthorityPrefix; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +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.util.Assert; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + + +/** + * A {@link Converter} that takes a {@link Jwt} and converts it into a {@link BearerTokenAuthentication}. + */ +public class AADJwtBearerTokenAuthenticationConverter implements Converter { + + private final Converter> converter; + private final String principalClaimName; + + /** + * Construct AADJwtBearerTokenAuthenticationConverter by AADTokenClaim.SUB and + * DEFAULT_CLAIM_TO_AUTHORITY_PREFIX_MAP. + */ + public AADJwtBearerTokenAuthenticationConverter() { + this(AADTokenClaim.SUB, AADResourceServerProperties.DEFAULT_CLAIM_TO_AUTHORITY_PREFIX_MAP); + } + + /** + * Construct AADJwtBearerTokenAuthenticationConverter with the authority claim. + * + * @param authoritiesClaimName authority claim name + */ + public AADJwtBearerTokenAuthenticationConverter(String authoritiesClaimName) { + this(authoritiesClaimName, AuthorityPrefix.SCOPE); + } + + /** + * Construct AADJwtBearerTokenAuthenticationConverter with the authority claim name and prefix. + * + * @param authoritiesClaimName authority claim name + * @param authorityPrefix the prefix name of the authority + */ + public AADJwtBearerTokenAuthenticationConverter(String authoritiesClaimName, + String authorityPrefix) { + this(null, buildClaimToAuthorityPrefixMap(authoritiesClaimName, authorityPrefix)); + } + + /** + * Using spring security provides JwtGrantedAuthoritiesConverter, it can resolve the access token of scp or roles. + * + * @param principalClaimName authorities claim name + * @param claimToAuthorityPrefixMap the authority name and prefix map + */ + public AADJwtBearerTokenAuthenticationConverter(String principalClaimName, + Map claimToAuthorityPrefixMap) { + Assert.notNull(claimToAuthorityPrefixMap, "claimToAuthorityPrefixMap cannot be null"); + this.principalClaimName = principalClaimName; + this.converter = new AADJwtGrantedAuthoritiesConverter(claimToAuthorityPrefixMap); + } + + @Override + public AbstractAuthenticationToken convert(Jwt jwt) { + OAuth2AccessToken accessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt()); + Map claims = jwt.getClaims(); + Collection authorities = converter.convert(jwt); + OAuth2AuthenticatedPrincipal principal = new AADOAuth2AuthenticatedPrincipal( + jwt.getHeaders(), claims, authorities, jwt.getTokenValue(), (String) claims.get(principalClaimName)); + return new BearerTokenAuthentication(principal, accessToken, authorities); + } + + private static Map buildClaimToAuthorityPrefixMap(String authoritiesClaimName, + String authorityPrefix) { + Assert.notNull(authoritiesClaimName, "authoritiesClaimName cannot be null"); + Assert.notNull(authorityPrefix, "authorityPrefix cannot be null"); + Map claimToAuthorityPrefixMap = new HashMap<>(); + claimToAuthorityPrefixMap.put(authoritiesClaimName, authorityPrefix); + return claimToAuthorityPrefixMap; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADOBOOAuth2AuthorizedClientProvider.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADOBOOAuth2AuthorizedClientProvider.java new file mode 100644 index 0000000000000..bd0844b4a8c4d --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADOBOOAuth2AuthorizedClientProvider.java @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationGrantType; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.Constants; +import com.microsoft.aad.msal4j.ClientCredentialFactory; +import com.microsoft.aad.msal4j.ConfidentialClientApplication; +import com.microsoft.aad.msal4j.IClientSecret; +import com.microsoft.aad.msal4j.MsalInteractionRequiredException; +import com.microsoft.aad.msal4j.MsalServiceException; +import com.microsoft.aad.msal4j.OnBehalfOfParameters; +import com.microsoft.aad.msal4j.UserAssertion; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +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.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletResponse; +import java.net.MalformedURLException; +import java.text.ParseException; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +/** + * + */ +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); + + + @Override + public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { + Assert.notNull(context, "context cannot be null"); + ClientRegistration clientRegistration = context.getClientRegistration(); + + if (!AADAuthorizationGrantType.ON_BEHALF_OF + .isSameGrantType(clientRegistration.getAuthorizationGrantType())) { + return null; + } + + OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient(); + if (authorizedClient != null && !hasTokenExpired(authorizedClient.getAccessToken())) { + // If client is already authorized but access token is NOT expired than no need for re-authorization + return null; + } + + return getOboAuthorizedClient(context.getClientRegistration(), context.getPrincipal()); + } + + private boolean hasTokenExpired(AbstractOAuth2Token token) { + Instant expiresAt = token.getExpiresAt(); + if (expiresAt == null) { + return true; + } + + expiresAt = expiresAt.minus(this.clockSkew); + return this.clock.instant().isAfter(expiresAt); + } + + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private T getOboAuthorizedClient(ClientRegistration clientRegistration, + Authentication principal) { + if (principal instanceof AnonymousAuthenticationToken) { + LOGGER.debug("Found anonymous authentication."); + return null; + } + + if (!(principal instanceof AbstractOAuth2TokenAuthenticationToken)) { + throw new IllegalStateException("Unsupported token implementation " + principal.getClass()); + } + + try { + String accessToken = ((AbstractOAuth2TokenAuthenticationToken) principal).getToken() + .getTokenValue(); + OnBehalfOfParameters parameters = OnBehalfOfParameters + .builder(clientRegistration.getScopes(), new UserAssertion(accessToken)) + .build(); + ConfidentialClientApplication clientApplication = createApp(clientRegistration); + if (null == clientApplication) { + return null; + } + + String oboAccessToken = clientApplication.acquireToken(parameters).get().accessToken(); + JWT parser = JWTParser.parse(oboAccessToken); + Date iat = (Date) parser.getJWTClaimsSet().getClaim("iat"); + Date exp = (Date) parser.getJWTClaimsSet().getClaim("exp"); + OAuth2AccessToken oAuth2AccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + oboAccessToken, + Instant.ofEpochMilli(iat.getTime()), + Instant.ofEpochMilli(exp.getTime())); + OAuth2AuthorizedClient oAuth2AuthorizedClient = new OAuth2AuthorizedClient(clientRegistration, + principal.getName(), + oAuth2AccessToken); + LOGGER.info("load obo authorized client success"); + return (T) oAuth2AuthorizedClient; + } catch (ExecutionException exception) { + // Handle conditional access policy, step 1. + // A user interaction is required, but we are in a web API, and therefore, we need to report back to the + // client through a 'WWW-Authenticate' header https://tools.ietf.org/html/rfc6750#section-3.1 + Optional.of(exception) + .map(Throwable::getCause) + .filter(e -> e instanceof MsalInteractionRequiredException) + .map(e -> (MsalInteractionRequiredException) e) + .map(MsalServiceException::claims) + .filter(StringUtils::hasText) + .ifPresent(this::replyForbiddenWithWwwAuthenticateHeader); + LOGGER.error("Failed to load authorized client.", exception); + } catch (InterruptedException | ParseException exception) { + LOGGER.error("Failed to load authorized client.", exception); + } + return null; + } + + ConfidentialClientApplication createApp(ClientRegistration clientRegistration) { + String authorizationUri = clientRegistration.getProviderDetails().getAuthorizationUri(); + String authority = interceptAuthorizationUri(authorizationUri); + IClientSecret clientCredential = ClientCredentialFactory + .createFromSecret(clientRegistration.getClientSecret()); + try { + return ConfidentialClientApplication.builder(clientRegistration.getClientId(), clientCredential) + .authority(authority) + .build(); + } catch (MalformedURLException e) { + LOGGER.error("Failed to create ConfidentialClientApplication", e); + } + return null; + } + + private String interceptAuthorizationUri(String authorizationUri) { + int count = 0; + int slashNumber = 4; + for (int i = 0; i < authorizationUri.length(); i++) { + if (authorizationUri.charAt(i) == '/') { + count++; + } + if (count == slashNumber) { + return authorizationUri.substring(0, i + 1); + } + } + return null; + } + + private void replyForbiddenWithWwwAuthenticateHeader(String claims) { + ServletRequestAttributes attr = + (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + HttpServletResponse response = attr.getResponse(); + Assert.notNull(response, "HttpServletResponse should not be null."); + response.setStatus(HttpStatus.FORBIDDEN.value()); + Map parameters = new LinkedHashMap<>(); + parameters.put(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, claims); + parameters.put(OAuth2ParameterNames.ERROR, OAuth2ErrorCodes.INVALID_TOKEN); + parameters.put(OAuth2ParameterNames.ERROR_DESCRIPTION, "The resource server requires higher privileges " + + "than " + + "provided by the access token"); + response.addHeader(HttpHeaders.WWW_AUTHENTICATE, Constants.BEARER_PREFIX + parameters.toString()); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADResourceServerConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADResourceServerConfiguration.java new file mode 100644 index 0000000000000..e22723f225c0e --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADResourceServerConfiguration.java @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi; + + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationServerEndpoints; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi.validator.AADJwtAudienceValidator; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi.validator.AADJwtIssuerValidator; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.AADAuthenticationProperties; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.condition.aad.ResourceServerCondition; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +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.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtTimestampValidator; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + *

+ * The configuration will not be activated if no {@link BearerTokenAuthenticationToken} class provided. + *

+ * By default, creating a JwtDecoder through JwkKeySetUri will be auto-configured. + */ +@Configuration(proxyBeanMethods = false) +@Conditional(ResourceServerCondition.class) +public class AADResourceServerConfiguration { + + @Autowired + private AADAuthenticationProperties aadAuthenticationProperties; + + /** + * Use JwkKeySetUri to create JwtDecoder + * + * @return Get the jwtDecoder instance. + */ + @Bean + @ConditionalOnMissingBean(JwtDecoder.class) + public JwtDecoder jwtDecoder() { + AADAuthorizationServerEndpoints identityEndpoints = new AADAuthorizationServerEndpoints( + aadAuthenticationProperties.getBaseUri(), aadAuthenticationProperties.getTenantId()); + NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder + .withJwkSetUri(identityEndpoints.jwkSetEndpoint()).build(); + List> validators = createDefaultValidator(); + nimbusJwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators)); + return nimbusJwtDecoder; + } + + public List> createDefaultValidator() { + List> validators = new ArrayList<>(); + List validAudiences = new ArrayList<>(); + if (StringUtils.hasText(aadAuthenticationProperties.getAppIdUri())) { + validAudiences.add(aadAuthenticationProperties.getAppIdUri()); + } + if (StringUtils.hasText(aadAuthenticationProperties.getClientId())) { + validAudiences.add(aadAuthenticationProperties.getClientId()); + } + if (!validAudiences.isEmpty()) { + validators.add(new AADJwtAudienceValidator(validAudiences)); + } + validators.add(new AADJwtIssuerValidator()); + validators.add(new JwtTimestampValidator()); + return validators; + } + + /** + * Default configuration class for using AAD authentication and authorization. User can write another configuration + * bean to override it. + */ + @Configuration + @EnableWebSecurity + @EnableGlobalMethodSecurity(prePostEnabled = true) + @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) + @ConditionalOnExpression("!'${azure.activedirectory.application-type}'.equalsIgnoreCase('web_application_and_resource_server')") + public static class DefaultAADResourceServerWebSecurityConfigurerAdapter extends + AADResourceServerWebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + super.configure(http); + } + } +} + diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADResourceServerProperties.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADResourceServerProperties.java new file mode 100644 index 0000000000000..21fc07549ee9e --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADResourceServerProperties.java @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.constants.AADTokenClaim; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.constants.AuthorityPrefix; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.StringUtils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Configuration properties for Azure Active Directory B2C. + */ +@ConfigurationProperties("azure.activedirectory.resource-server") +public class AADResourceServerProperties implements InitializingBean { + + public static final Map DEFAULT_CLAIM_TO_AUTHORITY_PREFIX_MAP; + + static { + Map claimAuthorityMap = new HashMap<>(); + claimAuthorityMap.put(AADTokenClaim.SCP, AuthorityPrefix.SCOPE); + claimAuthorityMap.put(AADTokenClaim.ROLES, AuthorityPrefix.APP_ROLE); + DEFAULT_CLAIM_TO_AUTHORITY_PREFIX_MAP = Collections.unmodifiableMap(claimAuthorityMap); + } + + /** + *
+     * Configure which claim in access token be returned in AuthenticatedPrincipal#getName.
+     * Default value is "sub".
+     *
+     * Example:
+     * If use the default value, and the access_token's "sub" scope value is "testValue",
+     * then AuthenticatedPrincipal#getName will return "testValue".
+     * 
+ * @see org.springframework.security.core.AuthenticatedPrincipal#getName + */ + private String principalClaimName; + /** + *
+     * Configure which claim will be used to build GrantedAuthority, and prefix of the GrantedAuthority's string value.
+     * Default value is: "scp" -> "SCOPE_", "roles" -> "APPROLE_".
+     *
+     * Example:
+     * If use the default value, and the access_token's "scp" scope value is "testValue",
+     * then GrantedAuthority with "SCOPE_testValue" will be created..
+     * 
+ * @see org.springframework.security.core.GrantedAuthority + */ + private Map claimToAuthorityPrefixMap; + + public String getPrincipalClaimName() { + return principalClaimName; + } + + public void setPrincipalClaimName(String principalClaimName) { + this.principalClaimName = principalClaimName; + } + + public Map getClaimToAuthorityPrefixMap() { + return claimToAuthorityPrefixMap; + } + + public void setClaimToAuthorityPrefixMap(Map claimToAuthorityPrefixMap) { + this.claimToAuthorityPrefixMap = claimToAuthorityPrefixMap; + } + + @Override + public void afterPropertiesSet() { + if (!StringUtils.hasText(principalClaimName)) { + principalClaimName = AADTokenClaim.SUB; + } + if (claimToAuthorityPrefixMap == null || claimToAuthorityPrefixMap.isEmpty()) { + claimToAuthorityPrefixMap = DEFAULT_CLAIM_TO_AUTHORITY_PREFIX_MAP; + } + } + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADResourceServerWebSecurityConfigurerAdapter.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADResourceServerWebSecurityConfigurerAdapter.java new file mode 100644 index 0000000000000..c117e3db08b0a --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADResourceServerWebSecurityConfigurerAdapter.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi; + +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; + +/** + * Abstract configuration class, used to make JwtConfigurer and AADJwtBearerTokenAuthenticationConverter take effect. + */ +public abstract class AADResourceServerWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { + + @Autowired + AADResourceServerProperties properties; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http.oauth2ResourceServer() + .jwt() + .jwtAuthenticationConverter( + new AADJwtBearerTokenAuthenticationConverter( + properties.getPrincipalClaimName(), properties.getClaimToAuthorityPrefixMap())); + // @formatter:off + } + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/package-info.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/package-info.java new file mode 100644 index 0000000000000..2e64679339ae1 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/package-info.java @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/** + * Package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi + */ +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi; diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/AADJwtAudienceValidator.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/AADJwtAudienceValidator.java new file mode 100644 index 0000000000000..a742225b6f1ac --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/AADJwtAudienceValidator.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi.validator; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.constants.AADTokenClaim; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.util.Assert; + +import java.util.List; + +/** + * Validates the "aud" claim in a {@link Jwt}, that is matches a configured value + */ +public class AADJwtAudienceValidator implements OAuth2TokenValidator { + + private final AADJwtClaimValidator> validator; + + /** + * Constructs a {@link AADJwtAudienceValidator} using the provided parameters + * + * @param audiences - The audience that each {@link Jwt} should have. + */ + public AADJwtAudienceValidator(List audiences) { + Assert.notNull(audiences, "audiences cannot be null"); + this.validator = new AADJwtClaimValidator<>(AADTokenClaim.AUD, audiences::containsAll); + } + + /** + * {@inheritDoc} + */ + @Override + public OAuth2TokenValidatorResult validate(Jwt token) { + Assert.notNull(token, "token cannot be null"); + return this.validator.validate(token); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/AADJwtClaimValidator.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/AADJwtClaimValidator.java new file mode 100644 index 0000000000000..095acb026838b --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/AADJwtClaimValidator.java @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi.validator; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.util.Assert; + +import java.util.function.Predicate; + +/** + * Validates a claim in a {@link Jwt} against a provided {@link Predicate}. + * + * Note: Current implementation is not required, this is only used for compatibility with the + * Spring Boot 2.2.x version. Once support version is more than 2.2.X, then we can use + * "org.springframework.security.oauth2.jwt.JwtClaimValidator" instead. + */ +public final class AADJwtClaimValidator implements OAuth2TokenValidator { + private static final Logger LOGGER = LoggerFactory.getLogger(AADJwtClaimValidator.class); + private final String claim; + private final OAuth2Error error; + private final Predicate test; + + /** + * Constructs a {@link AADJwtClaimValidator} using the provided parameters + * + * @param claim - is the name of the claim in {@link Jwt} to validate. + * @param test - is the predicate function for the claim to test against. + */ + public AADJwtClaimValidator(String claim, Predicate test) { + Assert.notNull(claim, "claim can not be null"); + Assert.notNull(test, "test can not be null"); + this.claim = claim; + this.test = test; + this.error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, + "The " + this.claim + " claim is not valid", + "https://tools.ietf.org/html/rfc6750#section-3.1"); + } + + /** + * {@inheritDoc} + */ + @Override + public OAuth2TokenValidatorResult validate(Jwt token) { + Assert.notNull(token, "token cannot be null"); + T claimValue = token.getClaim(this.claim); + if (test.test(claimValue)) { + return OAuth2TokenValidatorResult.success(); + } else { + LOGGER.debug(error.getDescription()); + return OAuth2TokenValidatorResult.failure(error); + } + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/AADJwtIssuerValidator.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/AADJwtIssuerValidator.java new file mode 100644 index 0000000000000..f62b8d728a9ce --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/AADJwtIssuerValidator.java @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi.validator; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADTrustedIssuerRepository; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.constants.AADTokenClaim; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.util.Assert; + +import java.util.function.Predicate; + +/** + * Validates the "iss" claim in a {@link Jwt}, that is matches a configured value + */ +public class AADJwtIssuerValidator implements OAuth2TokenValidator { + + private static final String LOGIN_MICROSOFT_ONLINE_ISSUER = "https://login.microsoftonline.com/"; + + private static final String STS_WINDOWS_ISSUER = "https://sts.windows.net/"; + + private static final String STS_CHINA_CLOUD_API_ISSUER = "https://sts.chinacloudapi.cn/"; + + private final AADJwtClaimValidator validator; + + private final AADTrustedIssuerRepository trustedIssuerRepo; + + /** + * Constructs a {@link AADJwtIssuerValidator} using the provided parameters + */ + public AADJwtIssuerValidator() { + this(null); + } + + /** + * Constructs a {@link AADJwtIssuerValidator} using the provided parameters + * + * @param aadTrustedIssuerRepository trusted issuer repository. + */ + public AADJwtIssuerValidator(AADTrustedIssuerRepository aadTrustedIssuerRepository) { + this.trustedIssuerRepo = aadTrustedIssuerRepository; + this.validator = new AADJwtClaimValidator<>(AADTokenClaim.ISS, trustedIssuerRepoValidIssuer()); + } + + private Predicate trustedIssuerRepoValidIssuer() { + return iss -> { + if (iss == null) { + return false; + } + if (trustedIssuerRepo == null) { + return iss.startsWith(LOGIN_MICROSOFT_ONLINE_ISSUER) + || iss.startsWith(STS_WINDOWS_ISSUER) + || iss.startsWith(STS_CHINA_CLOUD_API_ISSUER); + } + return trustedIssuerRepo.getTrustedIssuers().contains(iss); + }; + } + + /** + * {@inheritDoc} + */ + @Override + public OAuth2TokenValidatorResult validate(Jwt token) { + Assert.notNull(token, "token cannot be null"); + return this.validator.validate(token); + } + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/package-info.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/package-info.java new file mode 100644 index 0000000000000..12ee27a8e0581 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/package-info.java @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/** + * Package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.resource.server.validator + */ +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi.validator; diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADAzureDelegatedOAuth2AuthorizedClientProvider.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADAzureDelegatedOAuth2AuthorizedClientProvider.java new file mode 100644 index 0000000000000..bd6407f42116b --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADAzureDelegatedOAuth2AuthorizedClientProvider.java @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationGrantType; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; +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.RefreshTokenOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.util.Assert; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Optional; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADClientRegistrationRepository.AZURE_CLIENT_REGISTRATION_ID; + +/** + * A strategy for authorizing (or re-authorizing) an OAuth 2.0 Client. This implementations implement {@link + * AADAuthorizationGrantType "azure_delegated" authorization grant type}. + * + * @author RujunChen + * @see OAuth2AuthorizedClient + * @see OAuth2AuthorizationContext + * @see AADAuthorizationGrantType + * @see Section 1.3 Authorization Grant + * @since 3.8.0 + */ +public class AADAzureDelegatedOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider { + + private final Clock clock; + private final Duration clockSkew; + private final OAuth2AuthorizedClientProvider provider; + private final OAuth2AuthorizedClientRepository authorizedClientRepository; + + public AADAzureDelegatedOAuth2AuthorizedClientProvider( + RefreshTokenOAuth2AuthorizedClientProvider provider, + OAuth2AuthorizedClientRepository authorizedClientRepository) { + this.clock = Clock.systemUTC(); + this.clockSkew = Duration.ofSeconds(60); + this.provider = provider; + this.authorizedClientRepository = authorizedClientRepository; + } + + @Override + public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { + Assert.notNull(context, "context cannot be null"); + ClientRegistration clientRegistration = context.getClientRegistration(); + if (!AADAuthorizationGrantType.AZURE_DELEGATED.isSameGrantType( + clientRegistration.getAuthorizationGrantType())) { + return null; + } + OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient(); + if (authorizedClient != null && tokenNotExpired(authorizedClient.getAccessToken())) { + return null; + } + Authentication principal = context.getPrincipal(); + OAuth2AuthorizedClient azureClient = authorizedClientRepository.loadAuthorizedClient( + AZURE_CLIENT_REGISTRATION_ID, + principal, + getHttpServletRequestOrDefault(context)); + if (azureClient == null) { + throw new ClientAuthorizationRequiredException(AZURE_CLIENT_REGISTRATION_ID); + } + OAuth2AuthorizedClient clientWithExpiredToken = + createClientWithExpiredToken(azureClient, clientRegistration, principal); + String[] scopes = clientRegistration.getScopes().toArray(new String[0]); + OAuth2AuthorizationContext refreshTokenAuthorizationContext = + OAuth2AuthorizationContext.withAuthorizedClient(clientWithExpiredToken) + .principal(principal) + .attributes(attributes -> attributes.put(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME, scopes)) + .build(); + return provider.authorize(refreshTokenAuthorizationContext); + } + + private boolean tokenNotExpired(OAuth2Token token) { + return Optional.ofNullable(token) + .map(OAuth2Token::getExpiresAt) + .map(expiredAt -> this.clock.instant().isBefore(expiredAt.minus(this.clockSkew))) + .orElse(false); + } + + private OAuth2AuthorizedClient createClientWithExpiredToken(OAuth2AuthorizedClient azureClient, + ClientRegistration clientRegistration, + Authentication principal) { + Assert.notNull(azureClient, "azureClient cannot be null"); + Assert.notNull(clientRegistration, "clientRegistration cannot be null"); + OAuth2AccessToken accessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, + "non-access-token", + Instant.MIN, + Instant.now().minus(100, ChronoUnit.DAYS)); + return new OAuth2AuthorizedClient( + clientRegistration, + principal.getName(), + accessToken, + azureClient.getRefreshToken() + ); + } + + private static HttpServletRequest getHttpServletRequestOrDefault(OAuth2AuthorizationContext context) { + return Optional.ofNullable(context) + .map(OAuth2AuthorizationContext::getAttributes) + .map(attributes -> (HttpServletRequest) attributes.get(HttpServletRequest.class.getName())) + .orElseGet(AADAzureDelegatedOAuth2AuthorizedClientProvider::getDefaultHttpServletRequest); + } + + private static HttpServletRequest getDefaultHttpServletRequest() { + return Optional.ofNullable(RequestContextHolder.getRequestAttributes()) + .filter(attributes -> attributes instanceof ServletRequestAttributes) + .map(attributes -> ((ServletRequestAttributes) attributes).getRequest()) + .orElse(null); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADHandleConditionalAccessFilter.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADHandleConditionalAccessFilter.java new file mode 100644 index 0000000000000..33cccedab5d56 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADHandleConditionalAccessFilter.java @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADClientRegistrationRepository; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.Constants; +import org.springframework.http.HttpHeaders; +import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Handle the {@link WebClientResponseException} in On-Behalf-Of flow. + * + *

+ * When the resource-server needs re-acquire token(The request requires higher privileges than provided by the access + * token in On-Behalf-Of flow.), it can sent a 403 with information in the WWW-Authenticate header to web client ,web + * client will throw {@link WebClientResponseException}, web-application can handle this exception to challenge the + * user. + */ +public class AADHandleConditionalAccessFilter extends OncePerRequestFilter { + + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws IOException, ServletException { + // Handle conditional access policy, step 2. + try { + filterChain.doFilter(request, response); + } catch (Exception exception) { + Map authParameters = + Optional.of(exception) + .map(Throwable::getCause) + .filter(e -> e instanceof WebClientResponseException) + .map(e -> (WebClientResponseException) e) + .map(WebClientResponseException::getHeaders) + .map(httpHeaders -> httpHeaders.get(HttpHeaders.WWW_AUTHENTICATE)) + .map(list -> list.get(0)) + .map(this::parseAuthParameters) + .orElse(null); + if (authParameters != null && authParameters.containsKey(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)) { + request.getSession().setAttribute(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, + authParameters.get(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)); + // OAuth2AuthorizationRequestRedirectFilter will catch this exception to re-authorize. + throw new ClientAuthorizationRequiredException(AADClientRegistrationRepository.AZURE_CLIENT_REGISTRATION_ID); + } + throw exception; + } + } + + /** + * Get claims filed form the header to re-authorize. + * + * @param wwwAuthenticateHeader httpHeader + * @return authParametersMap + */ + private Map parseAuthParameters(String wwwAuthenticateHeader) { + return Stream.of(wwwAuthenticateHeader) + .filter(header -> StringUtils.hasText(header)) + .filter(header -> header.startsWith(Constants.BEARER_PREFIX)) + .map(str -> str.substring(Constants.BEARER_PREFIX.length() + 1, str.length() - 1)) + .map(str -> str.split(", ")) + .flatMap(Stream::of) + .map(parameter -> parameter.split("=")) + .filter(parameter -> parameter.length > 1) + .collect(Collectors.toMap( + parameters -> parameters[0], + parameters -> parameters[1])); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADOAuth2AuthorizationCodeGrantRequestEntityConverter.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADOAuth2AuthorizationCodeGrantRequestEntityConverter.java new file mode 100644 index 0000000000000..67407808cfb18 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADOAuth2AuthorizationCodeGrantRequestEntityConverter.java @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AbstractOAuth2AuthorizationCodeGrantRequestEntityConverter; +import com.azure.spring.core.AzureSpringIdentifier; +import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import java.util.Optional; +import java.util.Set; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADClientRegistrationRepository.AZURE_CLIENT_REGISTRATION_ID; + +/** + * Used to set "scope" parameter when use "auth-code" to get "access_token". + */ +public class AADOAuth2AuthorizationCodeGrantRequestEntityConverter + extends AbstractOAuth2AuthorizationCodeGrantRequestEntityConverter { + + private final Set azureClientAccessTokenScopes; + + public AADOAuth2AuthorizationCodeGrantRequestEntityConverter(Set azureClientAccessTokenScopes) { + this.azureClientAccessTokenScopes = azureClientAccessTokenScopes; + } + + @Override + protected String getApplicationId() { + return AzureSpringIdentifier.AZURE_SPRING_AAD; + } + + @Override + public MultiValueMap getHttpBody(OAuth2AuthorizationCodeGrantRequest request) { + MultiValueMap body = new LinkedMultiValueMap<>(); + String scopes = String.join(" ", isRequestForAzureClient(request) + ? azureClientAccessTokenScopes + : request.getClientRegistration().getScopes()); + body.add("scope", scopes); + return body; + } + + private boolean isRequestForAzureClient(OAuth2AuthorizationCodeGrantRequest request) { + return Optional.of(request) + .map(AbstractOAuth2AuthorizationGrantRequest::getClientRegistration) + .map(ClientRegistration::getRegistrationId) + .map(id -> id.equals(AZURE_CLIENT_REGISTRATION_ID)) + .orElse(false); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADOAuth2AuthorizationRequestResolver.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADOAuth2AuthorizationRequestResolver.java new file mode 100644 index 0000000000000..a8328b50162a7 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADOAuth2AuthorizationRequestResolver.java @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.AADAuthenticationProperties; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.Constants; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * To add conditional policy claims to authorization URL. + */ +public class AADOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { + private final OAuth2AuthorizationRequestResolver defaultResolver; + + private final AADAuthenticationProperties properties; + + public AADOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository, + AADAuthenticationProperties properties) { + this.defaultResolver = new DefaultOAuth2AuthorizationRequestResolver( + clientRegistrationRepository, + OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + ); + this.properties = properties; + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { + return addClaims(request, defaultResolver.resolve(request)); + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) { + return addClaims(request, defaultResolver.resolve(request, clientRegistrationId)); + } + + // Add claims to authorization-url + private OAuth2AuthorizationRequest addClaims(HttpServletRequest httpServletRequest, + OAuth2AuthorizationRequest oAuth2AuthorizationRequest) { + if (oAuth2AuthorizationRequest == null || httpServletRequest == null) { + return oAuth2AuthorizationRequest; + } + // Handle conditional access policy, step 3. + final String conditionalAccessPolicyClaims = + Optional.of(httpServletRequest) + .map(HttpServletRequest::getSession) + .map(httpSession -> { + String claims = (String) httpSession.getAttribute(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS); + if (claims != null) { + httpSession.removeAttribute(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS); + } + return claims; + }) + .orElse(null); + final Map additionalParameters = new HashMap<>(); + if (conditionalAccessPolicyClaims != null) { + additionalParameters.put(Constants.CLAIMS, conditionalAccessPolicyClaims); + } + Optional.ofNullable(properties) + .map(AADAuthenticationProperties::getAuthenticateAdditionalParameters) + .ifPresent(additionalParameters::putAll); + Optional.of(oAuth2AuthorizationRequest) + .map(OAuth2AuthorizationRequest::getAdditionalParameters) + .ifPresent(additionalParameters::putAll); + return OAuth2AuthorizationRequest.from(oAuth2AuthorizationRequest) + .additionalParameters(additionalParameters) + .build(); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADOAuth2UserService.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADOAuth2UserService.java new file mode 100644 index 0000000000000..384c3a0d242f9 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADOAuth2UserService.java @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.constants.AADTokenClaim; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.constants.AuthorityPrefix; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.AADAuthenticationProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.AbstractOAuth2Token; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpSession; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.Constants.DEFAULT_AUTHORITY_SET; + +/** + * This implementation will retrieve group info of user from Microsoft Graph. Then map group to {@link + * GrantedAuthority}. + */ +public class AADOAuth2UserService implements OAuth2UserService { + + private static final Logger LOGGER = LoggerFactory.getLogger(AADOAuth2UserService.class); + + private final OidcUserService oidcUserService; + private final List allowedGroupNames; + private final Set allowedGroupIds; + private final boolean enableFullList; + private final GraphClient graphClient; + private static final String DEFAULT_OIDC_USER = "defaultOidcUser"; + private static final String ROLES = "roles"; + + public AADOAuth2UserService(AADAuthenticationProperties properties) { + this(properties, new GraphClient(properties)); + } + + public AADOAuth2UserService(AADAuthenticationProperties properties, GraphClient graphClient) { + allowedGroupNames = Optional.ofNullable(properties) + .map(AADAuthenticationProperties::getUserGroup) + .map(AADAuthenticationProperties.UserGroupProperties::getAllowedGroupNames) + .orElseGet(Collections::emptyList); + allowedGroupIds = Optional.ofNullable(properties) + .map(AADAuthenticationProperties::getUserGroup) + .map(AADAuthenticationProperties.UserGroupProperties::getAllowedGroupIds) + .orElseGet(Collections::emptySet); + enableFullList = Optional.ofNullable(properties) + .map(AADAuthenticationProperties::getUserGroup) + .map(AADAuthenticationProperties.UserGroupProperties::getEnableFullList) + .orElse(false); + this.oidcUserService = new OidcUserService(); + this.graphClient = graphClient; + } + + @Override + public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { + // Delegate to the default implementation for loading a user + OidcUser oidcUser = oidcUserService.loadUser(userRequest); + OidcIdToken idToken = oidcUser.getIdToken(); + Set authorityStrings = new HashSet<>(); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + HttpSession session = attr.getRequest().getSession(true); + + if (authentication != null) { + LOGGER.debug("User {}'s authorities saved from session: {}.", authentication.getName(), authentication.getAuthorities()); + return (DefaultOidcUser) session.getAttribute(DEFAULT_OIDC_USER); + } + + authorityStrings.addAll(extractRolesFromIdToken(idToken)); + authorityStrings.addAll(extractGroupRolesFromAccessToken(userRequest.getAccessToken())); + Set authorities = authorityStrings.stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + if (authorities.isEmpty()) { + authorities = DEFAULT_AUTHORITY_SET; + } + String nameAttributeKey = + Optional.of(userRequest) + .map(OAuth2UserRequest::getClientRegistration) + .map(ClientRegistration::getProviderDetails) + .map(ClientRegistration.ProviderDetails::getUserInfoEndpoint) + .map(ClientRegistration.ProviderDetails.UserInfoEndpoint::getUserNameAttributeName) + .filter(StringUtils::hasText) + .orElse(AADTokenClaim.NAME); + LOGGER.debug("User {}'s authorities extracted by id token and access token: {}.", oidcUser.getClaim(nameAttributeKey), authorities); + // Create a copy of oidcUser but use the mappedAuthorities instead + DefaultOidcUser defaultOidcUser = new DefaultOidcUser(authorities, idToken, nameAttributeKey); + + session.setAttribute(DEFAULT_OIDC_USER, defaultOidcUser); + return defaultOidcUser; + } + + Set extractRolesFromIdToken(OidcIdToken idToken) { + return Optional.ofNullable(idToken) + .map(token -> (Collection) token.getClaim(ROLES)) + .filter(obj -> obj instanceof List) + .map(Collection::stream) + .orElseGet(Stream::empty) + .filter(s -> StringUtils.hasText(s.toString())) + .map(role -> AuthorityPrefix.APP_ROLE + role) + .collect(Collectors.toSet()); + } + + Set extractGroupRolesFromAccessToken(OAuth2AccessToken accessToken) { + if (allowedGroupNames.isEmpty() && allowedGroupIds.isEmpty()) { + return Collections.emptySet(); + } + Set roles = new HashSet<>(); + GroupInformation groupInformation = getGroupInformation(accessToken); + if (!allowedGroupNames.isEmpty()) { + Optional.of(groupInformation) + .map(GroupInformation::getGroupsNames) + .map(Collection::stream) + .orElseGet(Stream::empty) + .filter(allowedGroupNames::contains) + .forEach(roles::add); + } + if (!allowedGroupIds.isEmpty()) { + Optional.of(groupInformation) + .map(GroupInformation::getGroupsIds) + .map(Collection::stream) + .orElseGet(Stream::empty) + .filter(this::isAllowedGroupId) + .forEach(roles::add); + } + return roles.stream() + .map(roleStr -> AuthorityPrefix.ROLE + roleStr) + .collect(Collectors.toSet()); + } + + private boolean isAllowedGroupId(String groupId) { + if (enableFullList) { + return true; + } + if (allowedGroupIds.size() == 1 && allowedGroupIds.contains("all")) { + return true; + } + return allowedGroupIds.contains(groupId); + } + + private GroupInformation getGroupInformation(OAuth2AccessToken accessToken) { + return Optional.of(accessToken) + .map(AbstractOAuth2Token::getTokenValue) + .map(graphClient::getGroupInformation) + .orElseGet(GroupInformation::new); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADWebApplicationConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADWebApplicationConfiguration.java new file mode 100644 index 0000000000000..98319da7706fe --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADWebApplicationConfiguration.java @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.AADAuthenticationProperties; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.condition.aad.WebApplicationCondition; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +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.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; + +/** + * Configure the necessary beans used for aad authentication and authorization. + */ +@Configuration(proxyBeanMethods = false) +@Conditional(WebApplicationCondition.class) +public class AADWebApplicationConfiguration { + + @Autowired + private AADAuthenticationProperties properties; + + @Bean + @ConditionalOnMissingBean + public OAuth2UserService oidcUserService(AADAuthenticationProperties properties) { + return new AADOAuth2UserService(properties); + } + + /** + * Sample configuration to make AzureActiveDirectoryOAuth2UserService take effect. + */ + @Configuration + @EnableWebSecurity + @EnableGlobalMethodSecurity(prePostEnabled = true) + @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) + @ConditionalOnExpression("!'${azure.activedirectory.application-type}'.equalsIgnoreCase('web_application_and_resource_server')") + public static class DefaultAADWebSecurityConfigurerAdapter extends AADWebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + super.configure(http); + http.authorizeRequests() + .antMatchers("/login").permitAll() + .anyRequest().authenticated(); + } + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADWebSecurityConfigurerAdapter.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADWebSecurityConfigurerAdapter.java new file mode 100644 index 0000000000000..5d9cc49611c00 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADWebSecurityConfigurerAdapter.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADClientRegistrationRepository; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.AADAuthenticationProperties; +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; +import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.util.StringUtils; + +/** + * Abstract configuration class, used to make AzureClientRegistrationRepository and AuthzCodeGrantRequestEntityConverter + * take effect. + */ +public abstract class AADWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { + + @Autowired + private ClientRegistrationRepository repo; + @Autowired + private OAuth2UserService oidcUserService; + @Autowired + protected AADAuthenticationProperties properties; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http.oauth2Login() + .authorizationEndpoint() + .authorizationRequestResolver(requestResolver()) + .and() + .tokenEndpoint() + .accessTokenResponseClient(accessTokenResponseClient()) + .and() + .userInfoEndpoint() + .oidcUserService(oidcUserService) + .and() + .and() + .logout() + .logoutSuccessHandler(oidcLogoutSuccessHandler()) + .and() + .addFilterAfter(new AADHandleConditionalAccessFilter(), OAuth2AuthorizationRequestRedirectFilter.class); + // @formatter:off + } + + protected LogoutSuccessHandler oidcLogoutSuccessHandler() { + OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler = + new OidcClientInitiatedLogoutSuccessHandler(this.repo); + String uri = this.properties.getPostLogoutRedirectUri(); + if (StringUtils.hasText(uri)) { + oidcLogoutSuccessHandler.setPostLogoutRedirectUri(uri); + } + return oidcLogoutSuccessHandler; + } + + protected OAuth2AccessTokenResponseClient accessTokenResponseClient() { + DefaultAuthorizationCodeTokenResponseClient result = new DefaultAuthorizationCodeTokenResponseClient(); + if (repo instanceof AADClientRegistrationRepository) { + result.setRequestEntityConverter( + new AADOAuth2AuthorizationCodeGrantRequestEntityConverter( + ((AADClientRegistrationRepository) repo).getAzureClientAccessTokenScopes())); + } + return result; + } + + protected OAuth2AuthorizationRequestResolver requestResolver() { + return new AADOAuth2AuthorizationRequestResolver(this.repo, properties); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AuthorizationClientProperties.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AuthorizationClientProperties.java new file mode 100644 index 0000000000000..86200f6a75307 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AuthorizationClientProperties.java @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationGrantType; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; + +import java.util.List; + +/** + * Properties for an oauth2 client. + */ +public class AuthorizationClientProperties { + + private List scopes; + + private boolean onDemand = false; + + private AADAuthorizationGrantType authorizationGrantType; + + public AADAuthorizationGrantType getAuthorizationGrantType() { + return authorizationGrantType; + } + + public void setAuthorizationGrantType(AADAuthorizationGrantType authorizationGrantType) { + this.authorizationGrantType = authorizationGrantType; + } + + public void setScopes(List scopes) { + this.scopes = scopes; + } + + public List getScopes() { + return scopes; + } + + @Deprecated + @DeprecatedConfigurationProperty( + reason = "The AuthorizationGrantType of on-demand clients should be authorization_code.", + replacement = "Set oauth client AuthorizationGrantType to authorization_code, which means it's on-demand.") + public boolean isOnDemand() { + return onDemand; + } + + @Deprecated + public void setOnDemand(boolean onDemand) { + this.onDemand = onDemand; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AzureClientRegistration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AzureClientRegistration.java new file mode 100644 index 0000000000000..b55ba5754efab --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AzureClientRegistration.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp; + +import org.springframework.security.oauth2.client.registration.ClientRegistration; + +import java.util.Set; + +/** + * Azure oauth2 client registration. + * It has 2 kind of scopes: + * 1. AzureClientRegistration.client.scopes: used to authorize. + * 2. AzureClientRegistration.accessTokenScopes: used to get access_token. + */ +public class AzureClientRegistration { + + private final ClientRegistration client; + private final Set accessTokenScopes; + + public AzureClientRegistration(ClientRegistration client, Set scopes) { + this.client = client; + this.accessTokenScopes = scopes; + } + + public ClientRegistration getClient() { + return client; + } + + public Set getAccessTokenScopes() { + return accessTokenScopes; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/GraphClient.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/GraphClient.java new file mode 100644 index 0000000000000..38e6d3ac39147 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/GraphClient.java @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.AADAuthenticationProperties; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.JacksonObjectMapperFactory; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.Membership; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.Memberships; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.oauth2.sdk.http.HTTPResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +/** + * GraphClient is used to access graph server. Mainly used to get groups information of a user. + */ +public class GraphClient { + private static final Logger LOGGER = LoggerFactory.getLogger(GraphClient.class); + + private final AADAuthenticationProperties properties; + + public GraphClient(AADAuthenticationProperties properties) { + this.properties = properties; + } + + public GroupInformation getGroupInformation(String accessToken) { + GroupInformation groupInformation = new GroupInformation(); + final ObjectMapper objectMapper = JacksonObjectMapperFactory.getInstance(); + String aadMembershipRestUri = properties.getGraphMembershipUri(); + while (aadMembershipRestUri != null) { + Memberships memberships; + try { + String membershipsJson = getUserMemberships(accessToken, aadMembershipRestUri); + memberships = objectMapper.readValue(membershipsJson, Memberships.class); + } catch (IOException ioException) { + LOGGER.error("Can not get group information from graph server.", ioException); + break; + } + for (Membership membership : memberships.getValue()) { + if (isGroupObject(membership)) { + groupInformation.getGroupsIds().add(membership.getObjectID()); + groupInformation.getGroupsNames().add(membership.getDisplayName()); + } + } + aadMembershipRestUri = Optional.of(memberships) + .map(Memberships::getOdataNextLink) + .orElse(null); + } + return groupInformation; + } + + private String getUserMemberships(String accessToken, String urlString) throws IOException { + URL url = new URL(urlString); + final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod(HttpMethod.GET.toString()); + connection.setRequestProperty(HttpHeaders.AUTHORIZATION, String.format("Bearer %s", accessToken)); + connection.setRequestProperty(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + connection.setRequestProperty(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + final String responseInJson = getResponseString(connection); + final int responseCode = connection.getResponseCode(); + if (responseCode == HTTPResponse.SC_OK) { + return responseInJson; + } else { + throw new IllegalStateException( + "Response is not " + HTTPResponse.SC_OK + ", response json: " + responseInJson); + } + } + + private String getResponseString(HttpURLConnection connection) throws IOException { + try (BufferedReader reader = + new BufferedReader( + new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8) + ) + ) { + final StringBuilder stringBuffer = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + stringBuffer.append(line); + } + return stringBuffer.toString(); + } + } + + private boolean isGroupObject(final Membership membership) { + return membership.getObjectType().equals(Membership.OBJECT_TYPE_GROUP); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/GroupInformation.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/GroupInformation.java new file mode 100644 index 0000000000000..a9baea815eb84 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/GroupInformation.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp; + +import java.util.HashSet; +import java.util.Set; + +/** + * Contains information about the group. + */ +public class GroupInformation { + + private Set groupsIds = new HashSet<>(); + + private Set groupsNames = new HashSet<>(); + + public Set getGroupsIds() { + return groupsIds; + } + + public void setGroupsIds(Set groupsIds) { + this.groupsIds = groupsIds; + } + + public Set getGroupsNames() { + return groupsNames; + } + + public void setGroupsNames(Set groupsNames) { + this.groupsNames = groupsNames; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/package-info.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/package-info.java new file mode 100644 index 0000000000000..ea86a7125287d --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/package-info.java @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/** + * Package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp + */ +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp; diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAppRoleStatelessAuthenticationFilter.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAppRoleStatelessAuthenticationFilter.java new file mode 100644 index 0000000000000..0793fe96a32db --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAppRoleStatelessAuthenticationFilter.java @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.constants.AuthorityPrefix; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jwt.proc.BadJWTException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.text.ParseException; +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.Constants.DEFAULT_AUTHORITY_SET; + +/** + * A stateless authentication filter which uses app roles feature of Azure Active Directory. Since it's a stateless + * implementation so the principal will not be stored in session. By using roles claim in the token it will not call + * Microsoft Graph to retrieve users' groups. + *

+ * + * @deprecated See the Alternative method. + */ +@Deprecated +public class AADAppRoleStatelessAuthenticationFilter extends OncePerRequestFilter { + + private static final Logger LOGGER = LoggerFactory.getLogger(AADAppRoleStatelessAuthenticationFilter.class); + + private final UserPrincipalManager principalManager; + + public AADAppRoleStatelessAuthenticationFilter(UserPrincipalManager principalManager) { + this.principalManager = principalManager; + } + + @Override + protected void doFilterInternal(HttpServletRequest httpServletRequest, + HttpServletResponse httpServletResponse, + FilterChain filterChain) throws ServletException, IOException { + String aadIssuedBearerToken = Optional.of(httpServletRequest) + .map(r -> r.getHeader(HttpHeaders.AUTHORIZATION)) + .map(String::trim) + .filter(s -> s.startsWith(Constants.BEARER_PREFIX)) + .map(s -> s.replace(Constants.BEARER_PREFIX, "")) + .filter(principalManager::isTokenIssuedByAAD) + .orElse(null); + if (aadIssuedBearerToken == null || alreadyAuthenticated()) { + filterChain.doFilter(httpServletRequest, httpServletResponse); + return; + } + try { + final UserPrincipal userPrincipal = principalManager.buildUserPrincipal(aadIssuedBearerToken); + final Authentication authentication = new PreAuthenticatedAuthenticationToken( + userPrincipal, + null, + toSimpleGrantedAuthoritySet(userPrincipal) + ); + LOGGER.info("Request token verification success. {}", authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + try { + filterChain.doFilter(httpServletRequest, httpServletResponse); + } finally { + //Clear context after execution + SecurityContextHolder.clearContext(); + } + } catch (BadJWTException ex) { + // Invalid JWT. Either expired or not yet valid. + httpServletResponse.sendError(HttpStatus.UNAUTHORIZED.value()); + } catch (ParseException | BadJOSEException | JOSEException ex) { + LOGGER.error("Failed to initialize UserPrincipal.", ex); + throw new ServletException(ex); + } + } + + private boolean alreadyAuthenticated() { + return Optional.of(SecurityContextHolder.getContext()) + .map(SecurityContext::getAuthentication) + .map(Authentication::isAuthenticated) + .orElse(false); + } + + protected Set toSimpleGrantedAuthoritySet(UserPrincipal userPrincipal) { + Set simpleGrantedAuthoritySet = + Optional.of(userPrincipal) + .map(UserPrincipal::getRoles) + .map(Collection::stream) + .orElseGet(Stream::empty) + .filter(StringUtils::hasText) + .map(s -> new SimpleGrantedAuthority(AuthorityPrefix.ROLE + s)) + .collect(Collectors.toSet()); + return Optional.of(simpleGrantedAuthoritySet) + .filter(r -> !r.isEmpty()) + .orElse(DEFAULT_AUTHORITY_SET); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationFilter.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationFilter.java new file mode 100644 index 0000000000000..211038de53a7d --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationFilter.java @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationServerEndpoints; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.constants.AADTokenClaim; +import com.microsoft.aad.msal4j.MsalServiceException; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.source.JWKSetCache; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jose.util.ResourceRetriever; +import com.nimbusds.jwt.proc.BadJWTException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.naming.ServiceUnavailableException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.net.MalformedURLException; +import java.text.ParseException; +import java.util.Optional; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.Constants.BEARER_PREFIX; + +/** + * A stateful authentication filter which uses Microsoft Graph groups to authorize. Both ID token and access token are + * supported. In the case of access token, only access token issued for the exact same application this filter used for + * could be accepted, e.g. access token issued for Microsoft Graph could not be processed by users' application. + *

+ * + * @deprecated See the Alternative method. + */ +@Deprecated +public class AADAuthenticationFilter extends OncePerRequestFilter { + private static final Logger LOGGER = LoggerFactory.getLogger(AADAuthenticationFilter.class); + private static final String CURRENT_USER_PRINCIPAL = "CURRENT_USER_PRINCIPAL"; + + private final UserPrincipalManager userPrincipalManager; + private final AzureADGraphClient azureADGraphClient; + + public AADAuthenticationFilter(AADAuthenticationProperties aadAuthenticationProperties, + AADAuthorizationServerEndpoints endpoints, + ResourceRetriever resourceRetriever) { + this( + aadAuthenticationProperties, + endpoints, + new UserPrincipalManager( + endpoints, + aadAuthenticationProperties, + resourceRetriever, + false + ) + ); + } + + public AADAuthenticationFilter(AADAuthenticationProperties aadAuthenticationProperties, + AADAuthorizationServerEndpoints endpoints, + ResourceRetriever resourceRetriever, + JWKSetCache jwkSetCache) { + this( + aadAuthenticationProperties, + endpoints, + new UserPrincipalManager( + endpoints, + aadAuthenticationProperties, + resourceRetriever, + false, + jwkSetCache + ) + ); + } + + public AADAuthenticationFilter(AADAuthenticationProperties aadAuthenticationProperties, + AADAuthorizationServerEndpoints endpoints, + UserPrincipalManager userPrincipalManager) { + this.userPrincipalManager = userPrincipalManager; + this.azureADGraphClient = new AzureADGraphClient( + aadAuthenticationProperties.getClientId(), + aadAuthenticationProperties.getClientSecret(), + aadAuthenticationProperties, + endpoints + ); + } + + @Override + protected void doFilterInternal(HttpServletRequest httpServletRequest, + HttpServletResponse httpServletResponse, + FilterChain filterChain) throws ServletException, IOException { + String aadIssuedBearerToken = Optional.of(httpServletRequest) + .map(r -> r.getHeader(HttpHeaders.AUTHORIZATION)) + .map(String::trim) + .filter(s -> s.startsWith(BEARER_PREFIX)) + .map(s -> s.replace(BEARER_PREFIX, "")) + .filter(userPrincipalManager::isTokenIssuedByAAD) + .orElse(null); + if (aadIssuedBearerToken == null || alreadyAuthenticated()) { + filterChain.doFilter(httpServletRequest, httpServletResponse); + return; + } + try { + HttpSession httpSession = httpServletRequest.getSession(); + UserPrincipal userPrincipal = (UserPrincipal) httpSession.getAttribute(CURRENT_USER_PRINCIPAL); + if (userPrincipal == null + || !userPrincipal.getAadIssuedBearerToken().equals(aadIssuedBearerToken) + || userPrincipal.getAccessTokenForGraphApi() == null + ) { + userPrincipal = userPrincipalManager.buildUserPrincipal(aadIssuedBearerToken); + String tenantId = userPrincipal.getClaim(AADTokenClaim.TID).toString(); + String accessTokenForGraphApi = azureADGraphClient + .acquireTokenForGraphApi(aadIssuedBearerToken, tenantId) + .accessToken(); + userPrincipal.setAccessTokenForGraphApi(accessTokenForGraphApi); + userPrincipal.setGroups(azureADGraphClient.getGroups(accessTokenForGraphApi)); + httpSession.setAttribute(CURRENT_USER_PRINCIPAL, userPrincipal); + } + final Authentication authentication = new PreAuthenticatedAuthenticationToken( + userPrincipal, + null, + azureADGraphClient.toGrantedAuthoritySet(userPrincipal.getGroups()) + ); + LOGGER.info("Request token verification success. {}", authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + } catch (BadJWTException ex) { + // Invalid JWT. Either expired or not yet valid. + httpServletResponse.sendError(HttpStatus.UNAUTHORIZED.value()); + return; + } catch (MalformedURLException | ParseException | JOSEException | BadJOSEException ex) { + LOGGER.error("Failed to initialize UserPrincipal.", ex); + throw new ServletException(ex); + } catch (ServiceUnavailableException ex) { + LOGGER.error("Failed to acquire graph api token.", ex); + throw new ServletException(ex); + } catch (MsalServiceException ex) { + // Handle conditional access policy, step 2. + // No step 3 any more, because ServletException will not be caught. + // TODO: Do we need to return 401 instead of 500? + if (ex.claims() != null && !ex.claims().isEmpty()) { + throw new ServletException("Handle conditional access policy", ex); + } else { + throw ex; + } + } + filterChain.doFilter(httpServletRequest, httpServletResponse); + } + + private boolean alreadyAuthenticated() { + return Optional.of(SecurityContextHolder.getContext()) + .map(SecurityContext::getAuthentication) + .map(Authentication::isAuthenticated) + .orElse(false); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationFilterAutoConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationFilterAutoConfiguration.java new file mode 100644 index 0000000000000..2cca181a3859c --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationFilterAutoConfiguration.java @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationServerEndpoints; +import com.nimbusds.jose.jwk.source.DefaultJWKSetCache; +import com.nimbusds.jose.jwk.source.JWKSetCache; +import com.nimbusds.jose.util.DefaultResourceRetriever; +import com.nimbusds.jose.util.ResourceRetriever; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.TimeUnit; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Azure Active Authentication filters. + *

+ * The configuration will not be activated if no {@literal azure.activedirectory.client-id} property provided. + *

+ * A stateless filter {@link AADAppRoleStatelessAuthenticationFilter} will be auto-configured by specifying {@literal + * azure.activedirectory.session-stateless=true}. Otherwise, {@link AADAuthenticationFilter} will be configured. + */ +@Configuration +@ConditionalOnWebApplication +@ConditionalOnResource(resources = "classpath:aad.enable.config") +@ConditionalOnMissingClass({ "org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken" }) +@ConditionalOnProperty(prefix = AADAuthenticationFilterAutoConfiguration.PROPERTY_PREFIX, value = { "client-id" }) +@EnableConfigurationProperties({ AADAuthenticationProperties.class }) +public class AADAuthenticationFilterAutoConfiguration { + public static final String PROPERTY_PREFIX = "azure.activedirectory"; + private static final Logger LOG = LoggerFactory.getLogger(AADAuthenticationProperties.class); + + private final AADAuthenticationProperties properties; + private final AADAuthorizationServerEndpoints endpoints; + + public AADAuthenticationFilterAutoConfiguration(AADAuthenticationProperties properties) { + this.properties = properties; + this.endpoints = new AADAuthorizationServerEndpoints(properties.getBaseUri(), properties.getTenantId()); + } + + /** + * Declare AADAuthenticationFilter bean. + * + * @return AADAuthenticationFilter bean + */ + @Bean + @ConditionalOnMissingBean(AADAuthenticationFilter.class) + @ConditionalOnExpression("${azure.activedirectory.session-stateless:false} == false") + // client-id and client-secret used to: get graphApiToken -> groups + @ConditionalOnProperty(prefix = PROPERTY_PREFIX, value = { "client-id", "client-secret" }) + public AADAuthenticationFilter azureADJwtTokenFilter() { + LOG.info("AzureADJwtTokenFilter Constructor."); + return new AADAuthenticationFilter( + properties, + endpoints, + getJWTResourceRetriever(), + getJWKSetCache() + ); + } + + @Bean + @ConditionalOnMissingBean(AADAppRoleStatelessAuthenticationFilter.class) + @ConditionalOnExpression("${azure.activedirectory.session-stateless:false} == true") + // client-id used to: userPrincipalManager.getValidator + @ConditionalOnProperty(prefix = PROPERTY_PREFIX, value = { "client-id" }) + public AADAppRoleStatelessAuthenticationFilter azureADStatelessAuthFilter(ResourceRetriever resourceRetriever) { + LOG.info("Creating AzureADStatelessAuthFilter bean."); + return new AADAppRoleStatelessAuthenticationFilter( + new UserPrincipalManager( + endpoints, + properties, + resourceRetriever, + true + ) + ); + } + + @Bean + @ConditionalOnMissingBean(ResourceRetriever.class) + public ResourceRetriever getJWTResourceRetriever() { + return new DefaultResourceRetriever( + properties.getJwtConnectTimeout(), + properties.getJwtReadTimeout(), + properties.getJwtSizeLimit() + ); + } + + @Bean + @ConditionalOnMissingBean(JWKSetCache.class) + public JWKSetCache getJWKSetCache() { + long lifespan = properties.getJwkSetCacheLifespan(); + long refreshTime = properties.getJwkSetCacheRefreshTime(); + return new DefaultJWKSetCache(lifespan, refreshTime, TimeUnit.MILLISECONDS); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationProperties.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationProperties.java new file mode 100644 index 0000000000000..820f4c9a5f0f0 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationProperties.java @@ -0,0 +1,622 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADApplicationType; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationGrantType; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp.AuthorizationClientProperties; +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADApplicationType.inferApplicationTypeByDependencies; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationGrantType.AUTHORIZATION_CODE; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationGrantType.AZURE_DELEGATED; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationGrantType.ON_BEHALF_OF; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADClientRegistrationRepository.AZURE_CLIENT_REGISTRATION_ID; + +/** + * Configuration properties for Azure Active Directory Authentication. + */ +@Validated +@ConfigurationProperties("azure.activedirectory") +public class AADAuthenticationProperties implements InitializingBean { + + private static final Logger LOGGER = LoggerFactory.getLogger(AADAuthenticationProperties.class); + + private static final long DEFAULT_JWK_SET_CACHE_LIFESPAN = TimeUnit.MINUTES.toMillis(5); + private static final long DEFAULT_JWK_SET_CACHE_REFRESH_TIME = DEFAULT_JWK_SET_CACHE_LIFESPAN; + + /** + * Default UserGroup configuration. + */ + private UserGroupProperties userGroup = new UserGroupProperties(); + + /** + * Registered application ID in Azure AD. Must be configured when OAuth2 authentication is done in front end + */ + private String clientId; + + /** + * API Access Key of the registered application. Must be configured when OAuth2 authentication is done in front end + */ + private String clientSecret; + + /** + * Decide which claim to be principal's name.. + */ + private String userNameAttribute; + + /** + * Redirection Endpoint: Used by the authorization server to return responses containing authorization credentials + * to the client via the resource owner user-agent. + */ + private String redirectUriTemplate; + + /** + * App ID URI which might be used in the "aud" claim of an id_token. + */ + private String appIdUri; + + /** + * Add additional parameters to the Authorization URL. + */ + private Map authenticateAdditionalParameters; + + /** + * Connection Timeout for the JWKSet Remote URL call. + */ + private int jwtConnectTimeout = RemoteJWKSet.DEFAULT_HTTP_CONNECT_TIMEOUT; /* milliseconds */ + + /** + * Read Timeout for the JWKSet Remote URL call. + */ + private int jwtReadTimeout = RemoteJWKSet.DEFAULT_HTTP_READ_TIMEOUT; /* milliseconds */ + + /** + * Size limit in Bytes of the JWKSet Remote URL call. + */ + private int jwtSizeLimit = RemoteJWKSet.DEFAULT_HTTP_SIZE_LIMIT; /* bytes */ + + /** + * The lifespan of the cached JWK set before it expires, default is 5 minutes. + */ + private long jwkSetCacheLifespan = DEFAULT_JWK_SET_CACHE_LIFESPAN; + + /** + * The refresh time of the cached JWK set before it expires, default is 5 minutes. + */ + private long jwkSetCacheRefreshTime = DEFAULT_JWK_SET_CACHE_REFRESH_TIME; + + /** + * Azure Tenant ID. + */ + private String tenantId; + + private String postLogoutRedirectUri; + + /** + * If Telemetry events should be published to Azure AD. + */ + private boolean allowTelemetry = true; + + /** + * If true activates the stateless auth filter {@link AADAppRoleStatelessAuthenticationFilter}. The + * default is false which activates {@link AADAuthenticationFilter}. + */ + private Boolean sessionStateless = false; + + private String baseUri; + + private String graphBaseUri; + + private String graphMembershipUri; + + private Map authorizationClients = new HashMap<>(); + + private AADApplicationType applicationType; + + public AADApplicationType getApplicationType() { + return applicationType; + } + + public void setApplicationType(AADApplicationType applicationType) { + this.applicationType = applicationType; + } + + @DeprecatedConfigurationProperty( + reason = "Configuration moved to UserGroup class to keep UserGroup properties together", + replacement = "azure.activedirectory.user-group.allowed-group-names") + public List getActiveDirectoryGroups() { + return userGroup.getAllowedGroups(); + } + + /** + * Properties dedicated to changing the behavior of how the groups are mapped from the Azure AD response. Depending + * on the graph API used the object will not be the same. + */ + public static class UserGroupProperties { + + private final Log logger = LogFactory.getLog(UserGroupProperties.class); + + /** + * Expected UserGroups that an authority will be granted to if found in the response from the MemeberOf Graph + * API Call. + */ + private List allowedGroupNames = new ArrayList<>(); + + private Set allowedGroupIds = new HashSet<>(); + + /** + * enableFullList is used to control whether to list all group id, default is false + */ + private Boolean enableFullList = false; + + public Set getAllowedGroupIds() { + return allowedGroupIds; + } + + /** + * Set the allowed group ids. + * + * @param allowedGroupIds Allowed group ids. + */ + public void setAllowedGroupIds(Set allowedGroupIds) { + this.allowedGroupIds = allowedGroupIds; + } + + public List getAllowedGroupNames() { + return allowedGroupNames; + } + + public void setAllowedGroupNames(List allowedGroupNames) { + this.allowedGroupNames = allowedGroupNames; + } + + @Deprecated + @DeprecatedConfigurationProperty( + reason = "enable-full-list is not easy to understand.", + replacement = "allowed-group-ids: all") + public Boolean getEnableFullList() { + return enableFullList; + } + + @Deprecated + public void setEnableFullList(Boolean enableFullList) { + logger.warn(" 'azure.activedirectory.user-group.enable-full-list' property detected! " + + "Use 'azure.activedirectory.user-group.allowed-group-ids: all' instead!"); + this.enableFullList = enableFullList; + } + + @Deprecated + @DeprecatedConfigurationProperty( + reason = "In order to distinguish between allowed-group-ids and allowed-group-names, set allowed-groups " + + "deprecated.", + replacement = "azure.activedirectory.user-group.allowed-group-names") + public List getAllowedGroups() { + return allowedGroupNames; + } + + @Deprecated + public void setAllowedGroups(List allowedGroups) { + logger.warn(" 'azure.activedirectory.user-group.allowed-groups' property detected! " + " Use 'azure" + + ".activedirectory.user-group.allowed-group-names' instead!"); + this.allowedGroupNames = allowedGroups; + } + + } + + public boolean allowedGroupNamesConfigured() { + return Optional.of(this.getUserGroup()) + .map(UserGroupProperties::getAllowedGroupNames) + .map(allowedGroupNames -> !allowedGroupNames.isEmpty()) + .orElse(false); + } + + public boolean allowedGroupIdsConfigured() { + return Optional.of(this.getUserGroup()) + .map(UserGroupProperties::getAllowedGroupIds) + .map(allowedGroupIds -> !allowedGroupIds.isEmpty()) + .orElse(false); + } + + public UserGroupProperties getUserGroup() { + return userGroup; + } + + public void setUserGroup(UserGroupProperties userGroup) { + this.userGroup = userGroup; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClientSecret() { + return clientSecret; + } + + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + public String getUserNameAttribute() { + return userNameAttribute; + } + + public void setUserNameAttribute(String userNameAttribute) { + this.userNameAttribute = userNameAttribute; + } + + public String getRedirectUriTemplate() { + return redirectUriTemplate; + } + + public void setRedirectUriTemplate(String redirectUriTemplate) { + this.redirectUriTemplate = redirectUriTemplate; + } + + @Deprecated + public void setActiveDirectoryGroups(List activeDirectoryGroups) { + this.userGroup.setAllowedGroups(activeDirectoryGroups); + } + + public String getAppIdUri() { + return appIdUri; + } + + public void setAppIdUri(String appIdUri) { + this.appIdUri = appIdUri; + } + + public Map getAuthenticateAdditionalParameters() { + return authenticateAdditionalParameters; + } + + public void setAuthenticateAdditionalParameters(Map authenticateAdditionalParameters) { + this.authenticateAdditionalParameters = authenticateAdditionalParameters; + } + + public int getJwtConnectTimeout() { + return jwtConnectTimeout; + } + + public void setJwtConnectTimeout(int jwtConnectTimeout) { + this.jwtConnectTimeout = jwtConnectTimeout; + } + + public int getJwtReadTimeout() { + return jwtReadTimeout; + } + + public void setJwtReadTimeout(int jwtReadTimeout) { + this.jwtReadTimeout = jwtReadTimeout; + } + + public int getJwtSizeLimit() { + return jwtSizeLimit; + } + + public void setJwtSizeLimit(int jwtSizeLimit) { + this.jwtSizeLimit = jwtSizeLimit; + } + + public long getJwkSetCacheLifespan() { + return jwkSetCacheLifespan; + } + + public void setJwkSetCacheLifespan(long jwkSetCacheLifespan) { + this.jwkSetCacheLifespan = jwkSetCacheLifespan; + } + + public long getJwkSetCacheRefreshTime() { + return jwkSetCacheRefreshTime; + } + + public void setJwkSetCacheRefreshTime(long jwkSetCacheRefreshTime) { + this.jwkSetCacheRefreshTime = jwkSetCacheRefreshTime; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public String getPostLogoutRedirectUri() { + return postLogoutRedirectUri; + } + + public void setPostLogoutRedirectUri(String postLogoutRedirectUri) { + this.postLogoutRedirectUri = postLogoutRedirectUri; + } + + @Deprecated + @DeprecatedConfigurationProperty( + reason = "Deprecate the telemetry endpoint and use HTTP header User Agent instead.") + public boolean isAllowTelemetry() { + return allowTelemetry; + } + + public void setAllowTelemetry(boolean allowTelemetry) { + this.allowTelemetry = allowTelemetry; + } + + public Boolean getSessionStateless() { + return sessionStateless; + } + + public void setSessionStateless(Boolean sessionStateless) { + this.sessionStateless = sessionStateless; + } + + public String getBaseUri() { + return baseUri; + } + + public void setBaseUri(String baseUri) { + this.baseUri = baseUri; + } + + public String getGraphBaseUri() { + return graphBaseUri; + } + + public void setGraphBaseUri(String graphBaseUri) { + this.graphBaseUri = graphBaseUri; + } + + public String getGraphMembershipUri() { + return graphMembershipUri; + } + + public void setGraphMembershipUri(String graphMembershipUri) { + this.graphMembershipUri = graphMembershipUri; + } + + public Map getAuthorizationClients() { + return authorizationClients; + } + + public void setAuthorizationClients(Map authorizationClients) { + this.authorizationClients = authorizationClients; + } + + public boolean isAllowedGroup(String group) { + return Optional.ofNullable(getUserGroup()) + .map(UserGroupProperties::getAllowedGroupNames) + .orElseGet(Collections::emptyList) + .contains(group) + || Optional.ofNullable(getUserGroup()) + .map(UserGroupProperties::getAllowedGroupIds) + .orElseGet(Collections::emptySet) + .contains(group); + } + + @Override + public void afterPropertiesSet() { + + if (!StringUtils.hasText(baseUri)) { + baseUri = "https://login.microsoftonline.com/"; + } else { + baseUri = addSlash(baseUri); + } + + if (!StringUtils.hasText(redirectUriTemplate)) { + redirectUriTemplate = "{baseUrl}/login/oauth2/code/"; + } + + if (!StringUtils.hasText(graphBaseUri)) { + graphBaseUri = "https://graph.microsoft.com/"; + } else { + graphBaseUri = addSlash(graphBaseUri); + } + + if (!StringUtils.hasText(graphMembershipUri)) { + graphMembershipUri = graphBaseUri + "v1.0/me/memberOf"; + } + + if (!graphMembershipUri.startsWith(graphBaseUri)) { + throw new IllegalStateException("azure.activedirectory.graph-base-uri should be " + + "the prefix of azure.activedirectory.graph-membership-uri. " + + "azure.activedirectory.graph-base-uri = " + graphBaseUri + ", " + + "azure.activedirectory.graph-membership-uri = " + graphMembershipUri + "."); + } + + Set allowedGroupIds = userGroup.getAllowedGroupIds(); + if (allowedGroupIds.size() > 1 && allowedGroupIds.contains("all")) { + throw new IllegalStateException("When azure.activedirectory.user-group.allowed-group-ids contains 'all', " + + "no other group ids can be configured. " + + "But actually azure.activedirectory.user-group.allowed-group-ids=" + + allowedGroupIds); + } + + validateTenantId(); + validateApplicationType(); // This must before validateAuthorizationClients(). + validateAuthorizationClients(); + } + + private void validateAuthorizationClients() { + authorizationClients.forEach(this::validateAuthorizationClientProperties); + } + + private void validateTenantId() { + if (!StringUtils.hasText(tenantId)) { + tenantId = "common"; + } + + if (isMultiTenantsApplication(tenantId) && !userGroup.getAllowedGroups().isEmpty()) { + throw new IllegalStateException("When azure.activedirectory.tenant-id is 'common/organizations/consumers', " + + "azure.activedirectory.user-group.allowed-groups/allowed-group-names should be empty. " + + "But actually azure.activedirectory.tenant-id=" + tenantId + + ", and azure.activedirectory.user-group.allowed-groups/allowed-group-names=" + + userGroup.getAllowedGroups()); + } + + if (isMultiTenantsApplication(tenantId) && !userGroup.getAllowedGroupIds().isEmpty()) { + throw new IllegalStateException("When azure.activedirectory.tenant-id is 'common/organizations/consumers', " + + "azure.activedirectory.user-group.allowed-group-ids should be empty. " + + "But actually azure.activedirectory.tenant-id=" + tenantId + + ", and azure.activedirectory.user-group.allowed-group-ids=" + userGroup.getAllowedGroupIds()); + } + } + + /** + * Validate configured application type or set default value. + * + * @throws IllegalStateException Invalid property 'azure.activedirectory.application-type' + */ + private void validateApplicationType() { + AADApplicationType inferred = inferApplicationTypeByDependencies(); + if (applicationType != null) { + if (!isValidApplicationType(applicationType, inferred)) { + throw new IllegalStateException( + "Invalid property 'azure.activedirectory.application-type', the configured value is '" + + applicationType.getValue() + "', " + "but the inferred value is '" + + inferred.getValue() + "'."); + } + } else { + applicationType = inferred; + } + } + + private boolean isValidApplicationType(AADApplicationType configured, AADApplicationType inferred) { + return inferred == configured || inferred == AADApplicationType.RESOURCE_SERVER_WITH_OBO; + } + + private void validateAuthorizationClientProperties(String registrationId, + AuthorizationClientProperties properties) { + String grantType = Optional.of(properties) + .map(AuthorizationClientProperties::getAuthorizationGrantType) + .map(AADAuthorizationGrantType::getValue) + .orElse(null); + if (null == grantType) { + // Set default value for authorization grant grantType + switch (applicationType) { + case WEB_APPLICATION: + if (properties.isOnDemand()) { + properties.setAuthorizationGrantType(AUTHORIZATION_CODE); + } else { + properties.setAuthorizationGrantType(AZURE_DELEGATED); + } + LOGGER.debug("The client '{}' sets the default value of AADAuthorizationGrantType to " + + "'authorization_code'.", registrationId); + break; + case RESOURCE_SERVER: + case RESOURCE_SERVER_WITH_OBO: + properties.setAuthorizationGrantType(AADAuthorizationGrantType.ON_BEHALF_OF); + LOGGER.debug("The client '{}' sets the default value of AADAuthorizationGrantType to " + + "'on_behalf_of'.", registrationId); + break; + case WEB_APPLICATION_AND_RESOURCE_SERVER: + throw new IllegalStateException("azure.activedirectory.authorization-clients." + registrationId + + ".authorization-grant-grantType must be configured. "); + default: + throw new IllegalStateException("Unsupported authorization grantType " + applicationType.getValue()); + } + } else { + // Validate authorization grant grantType + switch (applicationType) { + case WEB_APPLICATION: + if (ON_BEHALF_OF.getValue().equals(grantType)) { + throw new IllegalStateException("When 'azure.activedirectory.application-type=web_application'," + + " 'azure.activedirectory.authorization-clients." + registrationId + + ".authorization-grant-type' can not be 'on_behalf_of'."); + } + break; + case RESOURCE_SERVER: + if (AUTHORIZATION_CODE.getValue().equals(grantType)) { + throw new IllegalStateException("When 'azure.activedirectory.application-type=resource_server'," + + " 'azure.activedirectory.authorization-clients." + registrationId + + ".authorization-grant-type' can not be 'authorization_code'."); + } + if (ON_BEHALF_OF.getValue().equals(grantType)) { + throw new IllegalStateException("When 'azure.activedirectory.application-type=resource_server'," + + " 'azure.activedirectory.authorization-clients." + registrationId + + ".authorization-grant-type' can not be 'on_behalf_of'."); + } + break; + case RESOURCE_SERVER_WITH_OBO: + if (AUTHORIZATION_CODE.getValue().equals(grantType)) { + throw new IllegalStateException("When 'azure.activedirectory" + + ".application-type=resource_server_with_obo'," + + " 'azure.activedirectory.authorization-clients." + registrationId + + ".authorization-grant-type' can not be 'authorization_code'."); + } + break; + case WEB_APPLICATION_AND_RESOURCE_SERVER: + default: + LOGGER.debug("'azure.activedirectory.authorization-clients." + registrationId + + ".authorization-grant-type' is valid."); + } + + if (properties.isOnDemand() + && !AUTHORIZATION_CODE.getValue().equals(grantType)) { + throw new IllegalStateException("onDemand only support authorization_code grant grantType. Please set " + + "'azure.activedirectory.authorization-clients." + registrationId + + ".authorization-grant-grantType=authorization_code'" + + " or 'azure.activedirectory.authorization-clients." + registrationId + ".on-demand=false'."); + } + + if (AZURE_CLIENT_REGISTRATION_ID.equals(registrationId) + && !AUTHORIZATION_CODE.equals(properties.getAuthorizationGrantType())) { + throw new IllegalStateException("azure.activedirectory.authorization-clients." + + AZURE_CLIENT_REGISTRATION_ID + + ".authorization-grant-grantType must be configured to 'authorization_code'."); + } + } + + // Validate scopes. + List scopes = properties.getScopes(); + if (scopes == null || scopes.isEmpty()) { + throw new IllegalStateException( + "'azure.activedirectory.authorization-clients." + registrationId + ".scopes' must be configured"); + } + // Add necessary scopes for authorization_code clients. + // https://docs.microsoft.com/en-us/graph/permissions-reference#remarks-17 + // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes + if (properties.getAuthorizationGrantType().getValue().equals(AUTHORIZATION_CODE.getValue())) { + if (!scopes.contains("openid")) { + scopes.add("openid"); // "openid" allows to request an ID token. + } + if (!scopes.contains("profile")) { + scopes.add("profile"); // "profile" allows to return additional claims in the ID token. + } + if (!scopes.contains("offline_access")) { + scopes.add("offline_access"); // "offline_access" allows to request a refresh token. + } + } + } + + private boolean isMultiTenantsApplication(String tenantId) { + return "common".equals(tenantId) || "organizations".equals(tenantId) || "consumers".equals(tenantId); + } + + private String addSlash(String uri) { + return uri.endsWith("/") ? uri : uri + "/"; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAutoConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAutoConfiguration.java new file mode 100644 index 0000000000000..076974633a35d --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAutoConfiguration.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADOAuth2ClientConfiguration; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi.AADResourceServerConfiguration; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi.AADResourceServerProperties; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp.AADWebApplicationConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + *

+ * Auto configure beans required for AAD. + *

+ */ +@Configuration +@ConditionalOnResource(resources = "classpath:aad.enable.config") +@EnableConfigurationProperties({ + AADAuthenticationProperties.class, + AADResourceServerProperties.class +}) +@Import({ + AADWebApplicationConfiguration.class, + AADResourceServerConfiguration.class, + AADOAuth2ClientConfiguration.class +}) +public class AADAutoConfiguration { + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AzureADGraphClient.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AzureADGraphClient.java new file mode 100644 index 0000000000000..5dce01c64fb50 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AzureADGraphClient.java @@ -0,0 +1,185 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationServerEndpoints; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.constants.AuthorityPrefix; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.aad.msal4j.ClientCredentialFactory; +import com.microsoft.aad.msal4j.ConfidentialClientApplication; +import com.microsoft.aad.msal4j.IAuthenticationResult; +import com.microsoft.aad.msal4j.IClientCredential; +import com.microsoft.aad.msal4j.MsalServiceException; +import com.microsoft.aad.msal4j.OnBehalfOfParameters; +import com.microsoft.aad.msal4j.UserAssertion; +import com.nimbusds.oauth2.sdk.http.HTTPResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import javax.naming.ServiceUnavailableException; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.Constants.DEFAULT_AUTHORITY_SET; + + +/** + * Microsoft Graph client encapsulation. + */ +public class AzureADGraphClient { + + private static final Logger LOGGER = LoggerFactory.getLogger(AzureADGraphClient.class); + private static final String MICROSOFT_GRAPH_SCOPE = "User.Read"; + // We use "aadfeed5" as suffix when client library is ADAL, upgrade to "aadfeed6" for MSAL + private static final String REQUEST_ID_SUFFIX = "aadfeed6"; + + private final String clientId; + private final String clientSecret; + private final AADAuthorizationServerEndpoints endpoints; + private final AADAuthenticationProperties aadAuthenticationProperties; + + public AzureADGraphClient(String clientId, + String clientSecret, + AADAuthenticationProperties aadAuthenticationProperties, + AADAuthorizationServerEndpoints endpoints) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.aadAuthenticationProperties = aadAuthenticationProperties; + this.endpoints = endpoints; + } + + private String getUserMemberships(String accessToken, String urlString) throws IOException { + URL url = new URL(urlString); + final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + + connection.setRequestMethod(HttpMethod.GET.toString()); + connection.setRequestProperty(HttpHeaders.AUTHORIZATION, String.format("Bearer %s", accessToken)); + connection.setRequestProperty(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + connection.setRequestProperty(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + + final String responseInJson = getResponseString(connection); + final int responseCode = connection.getResponseCode(); + if (responseCode == HTTPResponse.SC_OK) { + return responseInJson; + } else { + throw new IllegalStateException( + "Response is not " + HTTPResponse.SC_OK + ", response json: " + responseInJson); + } + } + + private static String getResponseString(HttpURLConnection connection) throws IOException { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) { + final StringBuilder stringBuffer = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + stringBuffer.append(line); + } + return stringBuffer.toString(); + } + } + + /** + * @param graphApiToken token used to access graph api. + * @return groups in graph api. + * @throws IOException throw exception if get groups failed by IOException. + */ + public Set getGroups(String graphApiToken) throws IOException { + final Set groups = new LinkedHashSet<>(); + final ObjectMapper objectMapper = JacksonObjectMapperFactory.getInstance(); + String aadMembershipRestUri = this.aadAuthenticationProperties.getGraphMembershipUri(); + while (aadMembershipRestUri != null) { + String membershipsJson = getUserMemberships(graphApiToken, aadMembershipRestUri); + Memberships memberships = objectMapper.readValue(membershipsJson, Memberships.class); + memberships.getValue() + .stream() + .filter(this::isGroupObject) + .map(Membership::getDisplayName) + .forEach(groups::add); + aadMembershipRestUri = Optional.of(memberships) + .map(Memberships::getOdataNextLink) + .orElse(null); + } + return groups; + } + + private boolean isGroupObject(final Membership membership) { + return membership.getObjectType().equals(Membership.OBJECT_TYPE_GROUP); + } + + public Set toGrantedAuthoritySet(final Set groups) { + Set grantedAuthoritySet = + groups.stream() + .filter(aadAuthenticationProperties::isAllowedGroup) + .map(group -> new SimpleGrantedAuthority(AuthorityPrefix.ROLE + group)) + .collect(Collectors.toSet()); + return Optional.of(grantedAuthoritySet) + .filter(g -> !g.isEmpty()) + .orElse(DEFAULT_AUTHORITY_SET); + } + + /** + * Acquire access token for calling Graph API. + * + * @param idToken The token used to perform an OBO request. + * @param tenantId The tenant id. + * @return The access token for Graph service. + * @throws ServiceUnavailableException If fail to acquire the token. + * @throws MsalServiceException If {@link MsalServiceException} has occurred. + */ + public IAuthenticationResult acquireTokenForGraphApi(String idToken, String tenantId) + throws ServiceUnavailableException { + final IClientCredential clientCredential = + ClientCredentialFactory.createFromSecret(clientSecret); + final UserAssertion assertion = new UserAssertion(idToken); + IAuthenticationResult result = null; + try { + final ConfidentialClientApplication application = ConfidentialClientApplication + .builder(clientId, clientCredential) + .authority(endpoints.getBaseUri() + tenantId + "/") + .correlationId(getCorrelationId()) + .build(); + final Set scopes = new HashSet<>(); + scopes.add(MICROSOFT_GRAPH_SCOPE); + final OnBehalfOfParameters onBehalfOfParameters = OnBehalfOfParameters.builder(scopes, assertion).build(); + result = application.acquireToken(onBehalfOfParameters).get(); + } catch (ExecutionException | InterruptedException | MalformedURLException e) { + // Handle conditional access policy, step 1. + final Throwable cause = e.getCause(); + if (cause instanceof MsalServiceException) { + final MsalServiceException exception = (MsalServiceException) cause; + if (exception.claims() != null && !exception.claims().isEmpty()) { + throw exception; + } + } + LOGGER.error("acquire on behalf of token for graph api error", e); + } + if (result == null) { + throw new ServiceUnavailableException("unable to acquire on_behalf_of token for client " + + clientId); + } + return result; + } + + private static String getCorrelationId() { + final String uuid = UUID.randomUUID().toString(); + return uuid.substring(0, uuid.length() - REQUEST_ID_SUFFIX.length()) + REQUEST_ID_SUFFIX; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/Constants.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/Constants.java new file mode 100644 index 0000000000000..b182980a24aab --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/Constants.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.constants.AuthorityPrefix; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Constants used for AAD related logic. + */ +public class Constants { + public static final String BEARER_PREFIX = "Bearer "; // Whitespace at the end is necessary. + public static final String CONDITIONAL_ACCESS_POLICY_CLAIMS = "CONDITIONAL_ACCESS_POLICY_CLAIMS"; + public static final String CLAIMS = "claims"; + public static final Set DEFAULT_AUTHORITY_SET; + + static { + Set authoritySet = new HashSet<>(); + authoritySet.add(new SimpleGrantedAuthority(AuthorityPrefix.ROLE + "USER")); + DEFAULT_AUTHORITY_SET = Collections.unmodifiableSet(authoritySet); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/JacksonObjectMapperFactory.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/JacksonObjectMapperFactory.java new file mode 100644 index 0000000000000..e2e2543af884a --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/JacksonObjectMapperFactory.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * factoty class of JacksonObjectMapper + */ +public final class JacksonObjectMapperFactory { + + private JacksonObjectMapperFactory() { + } + + public static ObjectMapper getInstance() { + return SingletonHelper.INSTANCE; + } + + private static class SingletonHelper { + private static final ObjectMapper INSTANCE = new ObjectMapper(); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/Membership.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/Membership.java new file mode 100644 index 0000000000000..7a95465a097f3 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/Membership.java @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; +import java.util.Objects; + +/** + * This class is used to deserialize json to object. + * Refs: https://docs.microsoft.com/en-us/previous-versions/azure/ad/graph/api/api-catalog + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Membership implements Serializable { + private static final long serialVersionUID = 9064197572478554735L; + public static final String OBJECT_TYPE_GROUP = "#microsoft.graph.group"; + + private final String objectID; + private final String objectType; + private final String displayName; + + @JsonCreator + public Membership( + @JsonProperty("objectId") @JsonAlias("id") String objectID, + @JsonProperty("objectType") @JsonAlias("@odata.type") String objectType, + @JsonProperty("displayName") String displayName) { + this.objectID = objectID; + this.objectType = objectType; + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + + public String getObjectType() { + return objectType; + } + + public String getObjectID() { + return objectID; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof Membership)) { + return false; + } + final Membership group = (Membership) o; + return this.getDisplayName().equals(group.getDisplayName()) + && this.getObjectID().equals(group.getObjectID()) + && this.getObjectType().equals(group.getObjectType()); + } + + @Override + public int hashCode() { + return Objects.hash(objectID, objectType, displayName); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/Memberships.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/Memberships.java new file mode 100644 index 0000000000000..7126e750a36ba --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/Memberships.java @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Objects; + +/** + * This class is used to deserialize json to object. + * Refs: https://docs.microsoft.com/en-us/previous-versions/azure/ad/graph/api/api-catalog + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Memberships { + + private final String odataNextLink; + private final List value; + + @JsonCreator + public Memberships( + @JsonAlias("odata.nextLink") + @JsonProperty("@odata.nextLink") String odataNextLink, + @JsonProperty("value") List value) { + this.odataNextLink = odataNextLink; + this.value = value; + } + + public String getOdataNextLink() { + return odataNextLink; + } + + public List getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof Memberships)) { + return false; + } + final Memberships groups = (Memberships) o; + return this.getOdataNextLink().equals(groups.getOdataNextLink()) + && this.getValue().equals(groups.getValue()); + } + + @Override + public int hashCode() { + return Objects.hash(odataNextLink, value); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipal.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipal.java new file mode 100644 index 0000000000000..915ace9b5eb33 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipal.java @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jwt.JWTClaimsSet; + +import java.io.Serializable; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * entity class of UserPrincipal + */ +public class UserPrincipal implements Serializable { + private static final long serialVersionUID = -3725690847771476854L; + + private static final String PERSONAL_ACCOUNT_TENANT_ID = "9188040d-6c67-4c5b-b112-36a304b66dad"; + + private String aadIssuedBearerToken; // id_token or access_token + + private final JWSObject jwsObject; + + private final JWTClaimsSet jwtClaimsSet; + + /** + * All groups in aadIssuedBearerToken. Including the ones not exist in aadAuthenticationProperties.getUserGroup() + * .getAllowedGroups() + */ + private Set groups; + + /** + * All roles in aadIssuedBearerToken. + */ + private Set roles; + + private String accessTokenForGraphApi; + + public UserPrincipal(String aadIssuedBearerToken, JWSObject jwsObject, JWTClaimsSet jwtClaimsSet) { + this.aadIssuedBearerToken = aadIssuedBearerToken; + this.jwsObject = jwsObject; + this.jwtClaimsSet = jwtClaimsSet; + } + + public String getAadIssuedBearerToken() { + return aadIssuedBearerToken; + } + + public void setAadIssuedBearerToken(String aadIssuedBearerToken) { + this.aadIssuedBearerToken = aadIssuedBearerToken; + } + + public Set getGroups() { + return this.groups; + } + + public void setGroups(Set groups) { + this.groups = groups; + } + + public Set getRoles() { + return roles; + } + + public void setRoles(Set roles) { + this.roles = roles; + } + + public String getAccessTokenForGraphApi() { + return accessTokenForGraphApi; + } + + public void setAccessTokenForGraphApi(String accessTokenForGraphApi) { + this.accessTokenForGraphApi = accessTokenForGraphApi; + } + + public boolean isMemberOf(AADAuthenticationProperties aadAuthenticationProperties, String group) { + return aadAuthenticationProperties.isAllowedGroup(group) + && Optional.of(groups) + .map(g -> g.contains(group)) + .orElse(false); + } + + public String getKid() { + return jwsObject == null ? null : jwsObject.getHeader().getKeyID(); + } + + public String getIssuer() { + return jwtClaimsSet == null ? null : jwtClaimsSet.getIssuer(); + } + + public String getSubject() { + return jwtClaimsSet == null ? null : jwtClaimsSet.getSubject(); + } + + public Map getClaims() { + return jwtClaimsSet == null ? null : jwtClaimsSet.getClaims(); + } + + public Object getClaim(String name) { + return jwtClaimsSet == null ? null : jwtClaimsSet.getClaim(name); + } + + public String getName() { + return jwtClaimsSet == null ? null : (String) jwtClaimsSet.getClaim("name"); + } + + public String getTenantId() { + return jwtClaimsSet == null ? null : (String) jwtClaimsSet.getClaim("tid"); + } + + public String getUserPrincipalName() { + return jwtClaimsSet == null ? null : (String) jwtClaimsSet.getClaim("preferred_username"); + } + + public boolean isPersonalAccount() { + return PERSONAL_ACCOUNT_TENANT_ID.equals(getTenantId()); + } +} + diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipalManager.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipalManager.java new file mode 100644 index 0000000000000..c8cd90799d7f3 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipalManager.java @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationServerEndpoints; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.constants.AADTokenClaim; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.jwk.source.JWKSetCache; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jose.proc.JWSKeySelector; +import com.nimbusds.jose.proc.JWSVerificationKeySelector; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jose.shaded.json.JSONArray; +import com.nimbusds.jose.util.ResourceRetriever; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.proc.BadJWTException; +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.MalformedURLException; +import java.net.URL; +import java.text.ParseException; +import java.util.Collection; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * A user principal manager to load user info from JWT. + */ +public class UserPrincipalManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(UserPrincipalManager.class); + private static final String LOGIN_MICROSOFT_ONLINE_ISSUER = "https://login.microsoftonline.com/"; + private static final String STS_WINDOWS_ISSUER = "https://sts.windows.net/"; + private static final String STS_CHINA_CLOUD_API_ISSUER = "https://sts.chinacloudapi.cn/"; + + private final JWKSource keySource; + private final AADAuthenticationProperties aadAuthenticationProperties; + private final Boolean explicitAudienceCheck; + private final Set validAudiences = new HashSet<>(); + + /** + * ø Creates a new {@link UserPrincipalManager} with a predefined {@link JWKSource}. + *

+ * This is helpful in cases the JWK is not a remote JWKSet or for unit testing. + * + * @param keySource - {@link JWKSource} containing at least one key + */ + public UserPrincipalManager(JWKSource keySource) { + this.keySource = keySource; + this.explicitAudienceCheck = false; + this.aadAuthenticationProperties = null; + } + + /** + * Create a new {@link UserPrincipalManager} based of the + * {@link AADAuthorizationServerEndpoints#jwkSetEndpoint()} + * + * @param endpoints - used to retrieve the JWKS URL + * @param aadAuthenticationProperties - used to retrieve the environment. + * @param resourceRetriever - configures the {@link RemoteJWKSet} call. + * @param explicitAudienceCheck Whether explicitly check the audience. + * @throws IllegalArgumentException If AAD key discovery URI is malformed. + */ + public UserPrincipalManager(AADAuthorizationServerEndpoints endpoints, + AADAuthenticationProperties aadAuthenticationProperties, + ResourceRetriever resourceRetriever, + boolean explicitAudienceCheck) { + this.aadAuthenticationProperties = aadAuthenticationProperties; + this.explicitAudienceCheck = explicitAudienceCheck; + if (explicitAudienceCheck) { + // client-id for "normal" check + this.validAudiences.add(this.aadAuthenticationProperties.getClientId()); + // app id uri for client credentials flow (server to server communication) + this.validAudiences.add(this.aadAuthenticationProperties.getAppIdUri()); + } + try { + String jwkSetEndpoint = + endpoints.jwkSetEndpoint(); + keySource = new RemoteJWKSet<>(new URL(jwkSetEndpoint), resourceRetriever); + } catch (MalformedURLException e) { + LOGGER.error("Failed to parse active directory key discovery uri.", e); + throw new IllegalArgumentException("Failed to parse active directory key discovery uri.", e); + } + } + + /** + * Create a new {@link UserPrincipalManager} based of the + * {@link AADAuthorizationServerEndpoints#jwkSetEndpoint()} + * ()} + * + * @param endpoints - used to retrieve the JWKS URL + * @param aadAuthenticationProperties - used to retrieve the environment. + * @param resourceRetriever - configures the {@link RemoteJWKSet} call. + * @param jwkSetCache - used to cache the JWK set for a finite time, default set to 5 minutes which matches + * constructor above if no jwkSetCache is passed in + * @param explicitAudienceCheck Whether explicitly check the audience. + * @throws IllegalArgumentException If AAD key discovery URI is malformed. + */ + public UserPrincipalManager(AADAuthorizationServerEndpoints endpoints, + AADAuthenticationProperties aadAuthenticationProperties, + ResourceRetriever resourceRetriever, + boolean explicitAudienceCheck, + JWKSetCache jwkSetCache) { + this.aadAuthenticationProperties = aadAuthenticationProperties; + this.explicitAudienceCheck = explicitAudienceCheck; + if (explicitAudienceCheck) { + // client-id for "normal" check + this.validAudiences.add(this.aadAuthenticationProperties.getClientId()); + // app id uri for client credentials flow (server to server communication) + this.validAudiences.add(this.aadAuthenticationProperties.getAppIdUri()); + } + try { + String jwkSetEndpoint = endpoints.jwkSetEndpoint(); + keySource = new RemoteJWKSet<>(new URL(jwkSetEndpoint), resourceRetriever, jwkSetCache); + } catch (MalformedURLException e) { + LOGGER.error("Failed to parse active directory key discovery uri.", e); + throw new IllegalArgumentException("Failed to parse active directory key discovery uri.", e); + } + } + + /** + * Parse the id token to {@link UserPrincipal}. + * + * @param aadIssuedBearerToken The token issued by AAD. + * @return The parsed {@link UserPrincipal}. + * @throws ParseException If the token couldn't be parsed to a valid JWS object. + * @throws JOSEException If an internal processing exception is encountered. + * @throws BadJOSEException If the JWT is rejected. + */ + public UserPrincipal buildUserPrincipal(String aadIssuedBearerToken) throws ParseException, JOSEException, + BadJOSEException { + final JWSObject jwsObject = JWSObject.parse(aadIssuedBearerToken); + final ConfigurableJWTProcessor validator = getValidator(jwsObject.getHeader().getAlgorithm()); + final JWTClaimsSet jwtClaimsSet = validator.process(aadIssuedBearerToken, null); + validator.getJWTClaimsSetVerifier().verify(jwtClaimsSet, null); + UserPrincipal userPrincipal = new UserPrincipal(aadIssuedBearerToken, jwsObject, jwtClaimsSet); + Set roles = Optional.of(userPrincipal) + .map(p -> p.getClaim(AADTokenClaim.ROLES)) + .map(r -> (JSONArray) r) + .map(Collection::stream) + .orElseGet(Stream::empty) + .map(Object::toString) + .collect(Collectors.toSet()); + userPrincipal.setRoles(roles); + return userPrincipal; + } + + public boolean isTokenIssuedByAAD(String token) { + try { + final JWT jwt = JWTParser.parse(token); + return isAADIssuer(jwt.getJWTClaimsSet().getIssuer()); + } catch (ParseException e) { + LOGGER.info("Fail to parse JWT {}, exception {}", token, e); + } + return false; + } + + private static boolean isAADIssuer(String issuer) { + if (issuer == null) { + return false; + } + return issuer.startsWith(LOGIN_MICROSOFT_ONLINE_ISSUER) + || issuer.startsWith(STS_WINDOWS_ISSUER) + || issuer.startsWith(STS_CHINA_CLOUD_API_ISSUER); + } + + private ConfigurableJWTProcessor getValidator(JWSAlgorithm jwsAlgorithm) { + final ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + final JWSKeySelector keySelector = new JWSVerificationKeySelector<>(jwsAlgorithm, keySource); + jwtProcessor.setJWSKeySelector(keySelector); + //TODO: would it make sense to inject it? and make it configurable or even allow to provide own implementation + jwtProcessor.setJWTClaimsSetVerifier(new DefaultJWTClaimsVerifier() { + @Override + public void verify(JWTClaimsSet claimsSet, SecurityContext ctx) throws BadJWTException { + super.verify(claimsSet, ctx); + final String issuer = claimsSet.getIssuer(); + if (!isAADIssuer(issuer)) { + throw new BadJWTException("Invalid token issuer"); + } + if (explicitAudienceCheck) { + Optional matchedAudience = claimsSet.getAudience() + .stream() + .filter(validAudiences::contains) + .findFirst(); + if (matchedAudience.isPresent()) { + LOGGER.debug("Matched audience: [{}]", matchedAudience.get()); + } else { + throw new BadJWTException("Invalid token audience. Provided value " + claimsSet.getAudience() + + "does not match neither client-id nor AppIdUri."); + } + } + } + }); + return jwtProcessor; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/package-info.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/package-info.java new file mode 100644 index 0000000000000..70d691f9c4d9a --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/package-info.java @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/** + * Package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad + */ +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CAuthorizationRequestResolver.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CAuthorizationRequestResolver.java new file mode 100644 index 0000000000000..56855f3354861 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CAuthorizationRequestResolver.java @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * This class handles the OAuth2 request procession for AAD B2C authorization. + *

+ * Userflow name is added in the request link and forgotten password redirection to password-reset page is added on the + * base of default OAuth2 authorization resolve. + */ +public class AADB2CAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { + + private static final String REQUEST_BASE_URI = + OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; + + private static final String REGISTRATION_ID_NAME = "registrationId"; + + private static final String PARAMETER_X_CLIENT_SKU = "x-client-SKU"; + + private static final String AAD_B2C_USER_AGENT = "spring-boot-starter"; + + private static final String MATCHER_PATTERN = String.format("%s/{%s}", REQUEST_BASE_URI, REGISTRATION_ID_NAME); + + private static final AntPathRequestMatcher REQUEST_MATCHER = new AntPathRequestMatcher(MATCHER_PATTERN); + + private final OAuth2AuthorizationRequestResolver defaultResolver; + + private final String passwordResetUserFlow; + + private final AADB2CProperties properties; + + public AADB2CAuthorizationRequestResolver(@NonNull ClientRegistrationRepository repository, + @NonNull AADB2CProperties properties) { + this.properties = properties; + this.passwordResetUserFlow = this.properties.getPasswordReset(); + this.defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(repository, REQUEST_BASE_URI); + } + + @Override + public OAuth2AuthorizationRequest resolve(@NonNull HttpServletRequest request) { + return resolve(request, getRegistrationId(request)); + } + + @Override + public OAuth2AuthorizationRequest resolve(@NonNull HttpServletRequest request, String registrationId) { + if (StringUtils.hasText(passwordResetUserFlow) && isForgotPasswordAuthorizationRequest(request)) { + final OAuth2AuthorizationRequest authRequest = defaultResolver.resolve(request, passwordResetUserFlow); + return getB2CAuthorizationRequest(authRequest, passwordResetUserFlow); + } + + if (StringUtils.hasText(registrationId) && REQUEST_MATCHER.matches(request)) { + return getB2CAuthorizationRequest(defaultResolver.resolve(request), registrationId); + } + + // Return null may not be the good practice, but we need to align with oauth2.client.web + // DefaultOAuth2AuthorizationRequestResolver. + return null; + } + + private OAuth2AuthorizationRequest getB2CAuthorizationRequest(@Nullable OAuth2AuthorizationRequest request, + String userFlow) { + Assert.hasText(userFlow, "User flow should contain text."); + + if (request == null) { + return null; + } + + final Map additionalParameters = new HashMap<>(); + Optional.ofNullable(this.properties) + .map(AADB2CProperties::getAuthenticateAdditionalParameters) + .ifPresent(additionalParameters::putAll); + additionalParameters.put("p", userFlow); + additionalParameters.put(PARAMETER_X_CLIENT_SKU, AAD_B2C_USER_AGENT); + + // OAuth2AuthorizationRequest.Builder.additionalParameters() in spring-security-oauth2-core 5.2.7.RELEASE + // and 5.3.5.RELEASE implementation way is different, so we to compatible with them. + additionalParameters.putAll(request.getAdditionalParameters()); + + return OAuth2AuthorizationRequest.from(request).additionalParameters(additionalParameters).build(); + } + + private String getRegistrationId(HttpServletRequest request) { + if (REQUEST_MATCHER.matches(request)) { + return REQUEST_MATCHER.matcher(request).getVariables().get(REGISTRATION_ID_NAME); + } + + return null; + } + + // Handle the forgot password of sign-up-or-in page cannot redirect user to password-reset page. + // The B2C service will enhance that, and then related code will be removed. + private boolean isForgotPasswordAuthorizationRequest(@NonNull HttpServletRequest request) { + final String error = request.getParameter("error"); + final String description = request.getParameter("error_description"); + + if ("access_denied".equals(error)) { + Assert.hasText(description, "description should contain text."); + return description.startsWith("AADB2C90118:"); + } + + return false; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CAutoConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CAutoConfiguration.java new file mode 100644 index 0000000000000..3579d7cd051c8 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CAutoConfiguration.java @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.lang.NonNull; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; + +/** + * When the configuration matches the {@link AADB2CConditions.CommonCondition.WebAppMode} condition, + * configure the necessary beans for AAD B2C authentication and authorization, + * and import {@link AADB2COAuth2ClientConfiguration} class for AAD B2C OAuth2 client support. + */ +@Configuration +@ConditionalOnResource(resources = "classpath:aadb2c.enable.config") +@Conditional({ AADB2CConditions.CommonCondition.class, AADB2CConditions.UserFlowCondition.class }) +@EnableConfigurationProperties(AADB2CProperties.class) +@Import(AADB2COAuth2ClientConfiguration.class) +public class AADB2CAutoConfiguration { + + private final ClientRegistrationRepository repository; + private final AADB2CProperties properties; + + public AADB2CAutoConfiguration(@NonNull ClientRegistrationRepository repository, + @NonNull AADB2CProperties properties) { + this.repository = repository; + this.properties = properties; + } + + @Bean + @ConditionalOnMissingBean + public AADB2CAuthorizationRequestResolver b2cOAuth2AuthorizationRequestResolver() { + return new AADB2CAuthorizationRequestResolver(repository, properties); + } + + @Bean + @ConditionalOnMissingBean + public AADB2CLogoutSuccessHandler b2cLogoutSuccessHandler() { + return new AADB2CLogoutSuccessHandler(properties); + } + + @Bean + @ConditionalOnMissingBean + public AADB2COidcLoginConfigurer b2cLoginConfigurer(AADB2CLogoutSuccessHandler handler, + AADB2CAuthorizationRequestResolver resolver) { + return new AADB2COidcLoginConfigurer(handler, resolver); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CClientRegistrationRepository.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CClientRegistrationRepository.java new file mode 100644 index 0000000000000..d9dc09c10d20f --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CClientRegistrationRepository.java @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; + +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +/** + *

+ * ClientRegistrationRepository for aad b2c + *

+ */ +public class AADB2CClientRegistrationRepository implements ClientRegistrationRepository, Iterable { + + private final InMemoryClientRegistrationRepository clientRegistrations; + private final List signUpOrSignInRegistrations; + + + AADB2CClientRegistrationRepository(String loginFlow, List clientRegistrations) { + this.signUpOrSignInRegistrations = clientRegistrations.stream() + .filter(client -> loginFlow.equals(client.getClientName())) + .collect(Collectors.toList()); + this.clientRegistrations = new InMemoryClientRegistrationRepository(clientRegistrations); + } + + @Override + public ClientRegistration findByRegistrationId(String registrationId) { + return this.clientRegistrations.findByRegistrationId(registrationId); + } + + @Override + public Iterator iterator() { + return this.signUpOrSignInRegistrations.iterator(); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CConditions.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CConditions.java new file mode 100644 index 0000000000000..e03121339f92a --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CConditions.java @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionMessage; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +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.CollectionUtils; + +import java.util.Map; + +/** + * Conditions for activating AAD B2C beans. + */ +public final class AADB2CConditions { + + /** + * Web application or web resource server scenario condition. + */ + static final class CommonCondition extends AnyNestedCondition { + CommonCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + /** + * Web application scenario condition. + */ + @ConditionalOnWebApplication + @ConditionalOnProperty( + prefix = AADB2CProperties.PREFIX, + value = { + "client-id", + "client-secret" + } + ) + static class WebAppMode { + + } + + /** + * Web resource server scenario condition. + */ + @ConditionalOnWebApplication + @ConditionalOnProperty(prefix = AADB2CProperties.PREFIX, value = { "tenant-id" }) + static class WebApiMode { + + } + } + + /** + * OAuth2 client beans condition. + */ + static final class ClientRegistrationCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(final ConditionContext context, + final AnnotatedTypeMetadata metadata) { + ConditionMessage.Builder message = ConditionMessage.forCondition( + "AAD B2C OAuth 2.0 Clients Configured Condition"); + AADB2CProperties aadb2CProperties = getAADB2CProperties(context); + if (aadb2CProperties == null) { + return ConditionOutcome.noMatch(message.notAvailable("aad b2c properties")); + } + + if (CollectionUtils.isEmpty(aadb2CProperties.getUserFlows()) + && CollectionUtils.isEmpty(aadb2CProperties.getAuthorizationClients())) { + return ConditionOutcome.noMatch(message.didNotFind("registered clients") + .items("user-flows", "authorization-clients")); + } + + StringBuilder details = new StringBuilder(); + if (!CollectionUtils.isEmpty(aadb2CProperties.getUserFlows())) { + details.append(getConditionResult("user-flows", aadb2CProperties.getUserFlows())); + } + if (!CollectionUtils.isEmpty(aadb2CProperties.getAuthorizationClients())) { + details.append(getConditionResult("authorization-clients", + aadb2CProperties.getAuthorizationClients())); + } + return ConditionOutcome.match(message.foundExactly(details.toString())); + } + } + + /** + * AAD B2C OAuth2 security configuration condition. + */ + static final class UserFlowCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(final ConditionContext context, + final AnnotatedTypeMetadata metadata) { + ConditionMessage.Builder message = ConditionMessage.forCondition( + "AAD B2C User Flow Clients Configured Condition"); + AADB2CProperties aadb2CProperties = getAADB2CProperties(context); + if (aadb2CProperties == null) { + return ConditionOutcome.noMatch(message.notAvailable("aad b2c properties")); + } + + if (CollectionUtils.isEmpty(aadb2CProperties.getUserFlows())) { + return ConditionOutcome.noMatch(message.didNotFind("user flows").atAll()); + } + + return ConditionOutcome.match(message.foundExactly( + getConditionResult("user-flows", aadb2CProperties.getUserFlows()))); + } + } + + /** + * Return the bound AADB2CProperties instance. + * @param context Condition context + * @return AADB2CProperties instance + */ + private static AADB2CProperties getAADB2CProperties(ConditionContext context) { + return Binder.get(context.getEnvironment()) + .bind("azure.activedirectory.b2c", AADB2CProperties.class) + .orElse(null); + } + + /** + * Return combined name and the string of the keys of the map which concatenated with ','. + * @param name name to concatenate + * @param map Map to concatenate + * @return the concatenated string. + */ + private static String getConditionResult(String name, Map map) { + return name + ": " + String.join(", ", map.keySet()) + " "; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CConfigurationException.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CConfigurationException.java new file mode 100644 index 0000000000000..25908e3d87826 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CConfigurationException.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +/** + * Throw runtime exception for configuration. + */ +public class AADB2CConfigurationException extends RuntimeException { + + public AADB2CConfigurationException(String message) { + super(message); + } + + public AADB2CConfigurationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CJwtBearerTokenAuthenticationConverter.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CJwtBearerTokenAuthenticationConverter.java new file mode 100644 index 0000000000000..f89fe110b06f2 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CJwtBearerTokenAuthenticationConverter.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi.AADJwtBearerTokenAuthenticationConverter; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication; + +import java.util.Map; + +/** + * A {@link Converter} that takes a {@link Jwt} and converts it into a {@link BearerTokenAuthentication}. + * + * @deprecated Use {@link AADJwtBearerTokenAuthenticationConverter} instead. + */ +@Deprecated +public class AADB2CJwtBearerTokenAuthenticationConverter extends AADJwtBearerTokenAuthenticationConverter { + + public AADB2CJwtBearerTokenAuthenticationConverter() { + super(); + } + + public AADB2CJwtBearerTokenAuthenticationConverter(String authoritiesClaimName) { + super(authoritiesClaimName); + } + + public AADB2CJwtBearerTokenAuthenticationConverter(String authoritiesClaimName, + String authorityPrefix) { + super(authoritiesClaimName, authorityPrefix); + } + + public AADB2CJwtBearerTokenAuthenticationConverter(String principalClaimName, + Map claimToAuthorityPrefixMap) { + super(principalClaimName, claimToAuthorityPrefixMap); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CLogoutSuccessHandler.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CLogoutSuccessHandler.java new file mode 100644 index 0000000000000..f61aa25625981 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CLogoutSuccessHandler.java @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +import org.springframework.lang.NonNull; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Get the url of successful logout and handle the navigation on logout. + */ +public class AADB2CLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { + + private final AADB2CProperties properties; + + public AADB2CLogoutSuccessHandler(@NonNull AADB2CProperties properties) { + this.properties = properties; + + super.setDefaultTargetUrl(getAADB2CEndSessionUrl()); + } + + private String getAADB2CEndSessionUrl() { + final String userFlow = properties.getUserFlows().get(properties.getLoginFlow()); + final String logoutSuccessUrl = properties.getLogoutSuccessUrl(); + + return AADB2CURL.getEndSessionUrl(properties.getBaseUri(), logoutSuccessUrl, userFlow); + } + + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + super.onLogoutSuccess(request, response, authentication); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2COAuth2AuthorizationCodeGrantRequestEntityConverter.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2COAuth2AuthorizationCodeGrantRequestEntityConverter.java new file mode 100644 index 0000000000000..8729ad2116aee --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2COAuth2AuthorizationCodeGrantRequestEntityConverter.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AbstractOAuth2AuthorizationCodeGrantRequestEntityConverter; +import com.azure.spring.core.AzureSpringIdentifier; + +/** + * Used to set azure service header tag when use "auth-code" to get "access_token". + */ +public class AADB2COAuth2AuthorizationCodeGrantRequestEntityConverter + extends AbstractOAuth2AuthorizationCodeGrantRequestEntityConverter { + + @Override + protected String getApplicationId() { + return AzureSpringIdentifier.AZURE_SPRING_B2C; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2COAuth2ClientConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2COAuth2ClientConfiguration.java new file mode 100644 index 0000000000000..4369905278e8e --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2COAuth2ClientConfiguration.java @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationGrantType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Configuration for AAD B2C OAuth2 client support, when depends on the Spring OAuth2 Client module. + */ +@Configuration +@ConditionalOnResource(resources = "classpath:aadb2c.enable.config") +@Conditional({ AADB2CConditions.CommonCondition.class, AADB2CConditions.ClientRegistrationCondition.class }) +@EnableConfigurationProperties(AADB2CProperties.class) +@ConditionalOnClass({ OAuth2LoginAuthenticationFilter.class }) +public class AADB2COAuth2ClientConfiguration { + + private static final Logger LOGGER = LoggerFactory.getLogger(AADB2COAuth2ClientConfiguration.class); + private final AADB2CProperties properties; + + public AADB2COAuth2ClientConfiguration(@NonNull AADB2CProperties properties) { + this.properties = properties; + } + + @Bean + @ConditionalOnMissingBean + public ClientRegistrationRepository clientRegistrationRepository() { + final List clientRegistrations = new ArrayList<>(); + clientRegistrations.addAll(properties.getUserFlows() + .entrySet() + .stream() + .map(this::buildUserFlowClientRegistration) + .collect(Collectors.toList())); + clientRegistrations.addAll(properties.getAuthorizationClients() + .entrySet() + .stream() + .map(this::buildClientRegistration) + .collect(Collectors.toList())); + return new AADB2CClientRegistrationRepository(properties.getLoginFlow(), clientRegistrations); + } + + /** + * Build user flow client registration. + * @param client user flow properties + * @return ClientRegistration + */ + private ClientRegistration buildUserFlowClientRegistration(Map.Entry client) { + return ClientRegistration.withRegistrationId(client.getValue()) // Use flow as registration Id. + .clientName(client.getKey()) + .clientId(properties.getClientId()) + .clientSecret(properties.getClientSecret()) + .clientAuthenticationMethod(ClientAuthenticationMethod.POST) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri(properties.getReplyUrl()) + .scope(properties.getClientId(), "openid", "offline_access") + .authorizationUri(AADB2CURL.getAuthorizationUrl(properties.getBaseUri())) + .tokenUri(AADB2CURL.getTokenUrl(properties.getBaseUri(), client.getValue())) + .jwkSetUri(AADB2CURL.getJwkSetUrl(properties.getBaseUri(), client.getValue())) + .userNameAttributeName(properties.getUserNameAttributeName()) + .build(); + } + + /** + * Create client registration, only support OAuth2 client credentials. + * + * @param client each client properties + * @return ClientRegistration + */ + private ClientRegistration buildClientRegistration(Map.Entry client) { + AuthorizationGrantType authGrantType = Optional.ofNullable(client.getValue().getAuthorizationGrantType()) + .map(AADAuthorizationGrantType::getValue) + .map(AuthorizationGrantType::new) + .orElse(null); + if (!AuthorizationGrantType.CLIENT_CREDENTIALS.equals(authGrantType)) { + LOGGER.warn("The authorization type of the {} client registration is not supported.", client.getKey()); + } + return ClientRegistration.withRegistrationId(client.getKey()) + .clientName(client.getKey()) + .clientId(properties.getClientId()) + .clientSecret(properties.getClientSecret()) + .clientAuthenticationMethod(ClientAuthenticationMethod.POST) + .authorizationGrantType(authGrantType) + .scope(client.getValue().getScopes()) + .tokenUri(AADB2CURL.getAADTokenUrl(properties.getTenantId())) + .jwkSetUri(AADB2CURL.getAADJwkSetUrl(properties.getTenantId())) + .build(); + } + + @Bean + @ConditionalOnMissingBean + public OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clients, + OAuth2AuthorizedClientRepository authorizedClients) { + return new DefaultOAuth2AuthorizedClientManager(clients, authorizedClients); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2COidcLoginConfigurer.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2COidcLoginConfigurer.java new file mode 100644 index 0000000000000..1ed6469843a10 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2COidcLoginConfigurer.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; + +/** + * Configure B2C OAUTH2 login properties. + */ +public class AADB2COidcLoginConfigurer extends AbstractHttpConfigurer { + + private final AADB2CLogoutSuccessHandler handler; + + private final AADB2CAuthorizationRequestResolver resolver; + + public AADB2COidcLoginConfigurer(AADB2CLogoutSuccessHandler handler, AADB2CAuthorizationRequestResolver resolver) { + this.handler = handler; + this.resolver = resolver; + } + + @Override + public void init(HttpSecurity http) throws Exception { + // @formatter:off + http.logout() + .logoutSuccessHandler(handler) + .and() + .oauth2Login() + .authorizationEndpoint() + .authorizationRequestResolver(resolver) + .and() + .tokenEndpoint() + .accessTokenResponseClient(accessTokenResponseClient()); + // @formatter:on + } + + protected OAuth2AccessTokenResponseClient accessTokenResponseClient() { + DefaultAuthorizationCodeTokenResponseClient result = new DefaultAuthorizationCodeTokenResponseClient(); + result.setRequestEntityConverter(new AADB2COAuth2AuthorizationCodeGrantRequestEntityConverter()); + return result; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CProperties.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CProperties.java new file mode 100644 index 0000000000000..a793905b67f90 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CProperties.java @@ -0,0 +1,326 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import org.hibernate.validator.constraints.URL; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotBlank; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationGrantType.CLIENT_CREDENTIALS; + +/** + * Configuration properties for Azure Active Directory B2C. + */ +@Validated +@ConfigurationProperties(prefix = AADB2CProperties.PREFIX) +public class AADB2CProperties implements InitializingBean { + + public static final String DEFAULT_LOGOUT_SUCCESS_URL = "http://localhost:8080/login"; + + public static final String PREFIX = "azure.activedirectory.b2c"; + + private static final String TENANT_NAME_PART_REGEX = "([A-Za-z0-9]+\\.)"; + + /** + * The default user flow key 'sign-up-or-sign-in'. + */ + protected static final String DEFAULT_KEY_SIGN_UP_OR_SIGN_IN = "sign-up-or-sign-in"; + + /** + * The default user flow key 'password-reset'. + */ + protected static final String DEFAULT_KEY_PASSWORD_RESET = "password-reset"; + + /** + * The name of the b2c tenant. + * @deprecated It's recommended to use 'baseUri' instead. + */ + @Deprecated + private String tenant; + + /** + * The name of the b2c tenant id. + */ + private String tenantId; + + /** + * App ID URI which might be used in the "aud" claim of an token. + */ + private String appIdUri; + + /** + * Connection Timeout for the JWKSet Remote URL call. + */ + private int jwtConnectTimeout = RemoteJWKSet.DEFAULT_HTTP_CONNECT_TIMEOUT; /* milliseconds */ + + /** + * Read Timeout for the JWKSet Remote URL call. + */ + private int jwtReadTimeout = RemoteJWKSet.DEFAULT_HTTP_READ_TIMEOUT; /* milliseconds */ + + /** + * Size limit in Bytes of the JWKSet Remote URL call. + */ + private int jwtSizeLimit = RemoteJWKSet.DEFAULT_HTTP_SIZE_LIMIT; /* bytes */ + + /** + * The application ID that registered under b2c tenant. + */ + @NotBlank(message = "client ID should not be blank") + private String clientId; + + /** + * The application secret that registered under b2c tenant. + */ + private String clientSecret; + + @URL(message = "logout success should be valid URL") + private String logoutSuccessUrl = DEFAULT_LOGOUT_SUCCESS_URL; + + private Map authenticateAdditionalParameters; + + /** + * User name attribute name + */ + private String userNameAttributeName; + + /** + * Telemetry data will be collected if true, or disable data collection. + */ + private boolean allowTelemetry = true; + + private String replyUrl = "{baseUrl}/login/oauth2/code/"; + + /** + * AAD B2C endpoint base uri. + */ + @URL(message = "baseUri should be valid URL") + private String baseUri; + + /** + * Specify the primary sign in flow key. + */ + private String loginFlow = DEFAULT_KEY_SIGN_UP_OR_SIGN_IN; + + private Map userFlows = new HashMap<>(); + + /** + * Specify client configuration + */ + private Map authorizationClients = new HashMap<>(); + + @Override + public void afterPropertiesSet() { + validateWebappProperties(); + validateCommonProperties(); + } + + /** + * Validate web app scenario properties configuration when using user flows. + */ + private void validateWebappProperties() { + if (!CollectionUtils.isEmpty(userFlows)) { + if (!StringUtils.hasText(tenant) && !StringUtils.hasText(baseUri)) { + throw new AADB2CConfigurationException("'tenant' and 'baseUri' at least configure one item."); + } + if (!userFlows.keySet().contains(loginFlow)) { + throw new AADB2CConfigurationException("Sign in user flow key '" + + loginFlow + "' is not in 'user-flows' map."); + } + } + } + + /** + * Validate common scenario properties configuration. + */ + private void validateCommonProperties() { + long credentialCount = authorizationClients.values() + .stream() + .map(authClient -> authClient.getAuthorizationGrantType()) + .filter(client -> CLIENT_CREDENTIALS == client) + .count(); + if (credentialCount > 0 && !StringUtils.hasText(tenantId)) { + throw new AADB2CConfigurationException("'tenant-id' must be configured " + + "when using client credential flow."); + } + } + + protected String getPasswordReset() { + Optional keyOptional = userFlows.keySet() + .stream() + .filter(key -> key.equalsIgnoreCase(DEFAULT_KEY_PASSWORD_RESET)) + .findAny(); + return keyOptional.isPresent() ? userFlows.get(keyOptional.get()) : null; + } + + public String getBaseUri() { + // baseUri is empty and points to Global env by default + if (StringUtils.hasText(tenant) && !StringUtils.hasText(baseUri)) { + return String.format("https://%s.b2clogin.com/%s.onmicrosoft.com/", tenant, tenant); + } + return baseUri; + } + + public void setBaseUri(String baseUri) { + this.baseUri = baseUri; + } + + public void setTenant(String tenant) { + this.tenant = tenant; + } + + /** + * Get tenant name for Telemetry + * @return tenant name + * @throws AADB2CConfigurationException resolve tenant name failed + */ + @DeprecatedConfigurationProperty( + reason = "Configuration updated to baseUri", + replacement = "azure.activedirectory.b2c.base-uri") + public String getTenant() { + if (StringUtils.hasText(baseUri)) { + Matcher matcher = Pattern.compile(TENANT_NAME_PART_REGEX).matcher(baseUri); + if (matcher.find()) { + String matched = matcher.group(); + return matched.substring(0, matched.length() - 1); + } + throw new AADB2CConfigurationException("Unable to resolve the 'tenant' name."); + } + return tenant; + } + + public Map getUserFlows() { + return userFlows; + } + + public void setUserFlows(Map userFlows) { + this.userFlows = userFlows; + } + + public String getLoginFlow() { + return loginFlow; + } + + public void setLoginFlow(String loginFlow) { + this.loginFlow = loginFlow; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClientSecret() { + return clientSecret; + } + + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + public String getLogoutSuccessUrl() { + return logoutSuccessUrl; + } + + public void setLogoutSuccessUrl(String logoutSuccessUrl) { + this.logoutSuccessUrl = logoutSuccessUrl; + } + + public Map getAuthenticateAdditionalParameters() { + return authenticateAdditionalParameters; + } + + public void setAuthenticateAdditionalParameters(Map authenticateAdditionalParameters) { + this.authenticateAdditionalParameters = authenticateAdditionalParameters; + } + + @Deprecated + @DeprecatedConfigurationProperty( + reason = "Deprecate the telemetry endpoint and use HTTP header User Agent instead.") + public boolean isAllowTelemetry() { + return allowTelemetry; + } + + public void setAllowTelemetry(boolean allowTelemetry) { + this.allowTelemetry = allowTelemetry; + } + + public String getUserNameAttributeName() { + return userNameAttributeName; + } + + public void setUserNameAttributeName(String userNameAttributeName) { + this.userNameAttributeName = userNameAttributeName; + } + + public String getReplyUrl() { + return replyUrl; + } + + public void setReplyUrl(String replyUrl) { + this.replyUrl = replyUrl; + } + + public String getAppIdUri() { + return appIdUri; + } + + public void setAppIdUri(String appIdUri) { + this.appIdUri = appIdUri; + } + + public int getJwtConnectTimeout() { + return jwtConnectTimeout; + } + + public void setJwtConnectTimeout(int jwtConnectTimeout) { + this.jwtConnectTimeout = jwtConnectTimeout; + } + + public int getJwtReadTimeout() { + return jwtReadTimeout; + } + + public void setJwtReadTimeout(int jwtReadTimeout) { + this.jwtReadTimeout = jwtReadTimeout; + } + + public int getJwtSizeLimit() { + return jwtSizeLimit; + } + + public void setJwtSizeLimit(int jwtSizeLimit) { + this.jwtSizeLimit = jwtSizeLimit; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public Map getAuthorizationClients() { + return authorizationClients; + } + + public void setAuthorizationClients(Map authorizationClients) { + this.authorizationClients = authorizationClients; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CResourceServerAutoConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CResourceServerAutoConfiguration.java new file mode 100644 index 0000000000000..4fdf0816c7782 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CResourceServerAutoConfiguration.java @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADIssuerJWSKeySelector; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADTrustedIssuerRepository; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi.validator.AADJwtAudienceValidator; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi.validator.AADJwtIssuerValidator; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import com.nimbusds.jwt.proc.JWTClaimsSetAwareJWSKeySelector; +import com.nimbusds.jwt.proc.JWTProcessor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.lang.NonNull; +import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtTimestampValidator; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * When the configuration matches the {@link AADB2CConditions.CommonCondition.WebApiMode} condition, configure the + * necessary beans for AAD B2C resource server beans, and import {@link AADB2COAuth2ClientConfiguration} class for AAD + * B2C OAuth2 client support. + */ +@Configuration +@ConditionalOnResource(resources = "classpath:aadb2c.enable.config") +@Conditional(AADB2CConditions.CommonCondition.class) +@ConditionalOnClass(BearerTokenAuthenticationToken.class) +@EnableConfigurationProperties(AADB2CProperties.class) +@Import(AADB2COAuth2ClientConfiguration.class) +public class AADB2CResourceServerAutoConfiguration { + + private final AADB2CProperties properties; + + public AADB2CResourceServerAutoConfiguration(@NonNull AADB2CProperties properties) { + this.properties = properties; + } + + @Bean + @ConditionalOnMissingBean + public AADTrustedIssuerRepository trustedIssuerRepository() { + return new AADB2CTrustedIssuerRepository(properties); + } + + @Bean + @ConditionalOnMissingBean + public JWTClaimsSetAwareJWSKeySelector aadIssuerJWSKeySelector( + AADTrustedIssuerRepository aadTrustedIssuerRepository) { + return new AADIssuerJWSKeySelector(aadTrustedIssuerRepository, properties.getJwtConnectTimeout(), + properties.getJwtReadTimeout(), properties.getJwtSizeLimit()); + } + + @Bean + @ConditionalOnMissingBean + public JWTProcessor jwtProcessor( + JWTClaimsSetAwareJWSKeySelector keySelector) { + ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + jwtProcessor.setJWTClaimsSetAwareJWSKeySelector(keySelector); + return jwtProcessor; + } + + @Bean + @ConditionalOnMissingBean + public JwtDecoder jwtDecoder(JWTProcessor jwtProcessor, + AADTrustedIssuerRepository trustedIssuerRepository) { + NimbusJwtDecoder decoder = new NimbusJwtDecoder(jwtProcessor); + List> validators = new ArrayList<>(); + List validAudiences = new ArrayList<>(); + if (StringUtils.hasText(properties.getAppIdUri())) { + validAudiences.add(properties.getAppIdUri()); + } + if (StringUtils.hasText(properties.getClientId())) { + validAudiences.add(properties.getClientId()); + } + if (!validAudiences.isEmpty()) { + validators.add(new AADJwtAudienceValidator(validAudiences)); + } + validators.add(new AADJwtIssuerValidator(trustedIssuerRepository)); + validators.add(new JwtTimestampValidator()); + decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators)); + return decoder; + } +} + diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CTrustedIssuerRepository.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CTrustedIssuerRepository.java new file mode 100644 index 0000000000000..c36abd3d7633c --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CTrustedIssuerRepository.java @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADTrustedIssuerRepository; +import org.springframework.util.Assert; + +import java.util.Map; + +import static java.util.Locale.ROOT; + +/** + * Construct a trusted aad b2c issuer repository. + */ +public class AADB2CTrustedIssuerRepository extends AADTrustedIssuerRepository { + + private final String resolvedBaseUri; + + private final Map userFlows; + + private final AADB2CProperties aadb2CProperties; + + public AADB2CTrustedIssuerRepository(AADB2CProperties aadb2CProperties) { + super(aadb2CProperties.getTenantId()); + this.aadb2CProperties = aadb2CProperties; + this.resolvedBaseUri = resolveBaseUri(aadb2CProperties.getBaseUri()); + this.userFlows = aadb2CProperties.getUserFlows(); + this.addB2CIssuer(); + this.addB2CUserFlowIssuers(); + } + + private void addB2CIssuer() { + Assert.notNull(aadb2CProperties, "aadb2CProperties cannot be null."); + Assert.notNull(resolvedBaseUri, "resolvedBaseUri cannot be null."); + String b2cIss = String.format("%s/%s/v2.0/", resolvedBaseUri, tenantId); + String oidcIssuerLocation = String.format("%s/%s/%s/v2.0/", resolvedBaseUri, tenantId, + userFlows.get(aadb2CProperties.getLoginFlow())); + // Adding oidc issuer location is not a consistent mapping with issuer contained in the access token. + addTrustedIssuer(b2cIss, oidcIssuerLocation); + } + + private void addB2CUserFlowIssuers() { + Assert.notNull(resolvedBaseUri, "resolvedBaseUri cannot be null."); + Assert.notNull(userFlows, "userFlows cannot be null."); + userFlows.values() + .stream() + .map(uf -> String.format("%s/tfp/%s/%s/v2.0/", resolvedBaseUri, tenantId, uf.toLowerCase(ROOT))) + .forEach(this::addTrustedIssuer); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CURL.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CURL.java new file mode 100644 index 0000000000000..d02fb5550a0d5 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CURL.java @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +import org.hibernate.validator.constraints.URL; +import org.springframework.util.Assert; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +/** + * To get AAD B2C URLs for configuration. + */ +public final class AADB2CURL { + + private AADB2CURL() { + + } + + private static final String AUTHORIZATION_URL_PATTERN = "oauth2/v2.0/authorize"; + + private static final String TOKEN_URL_PATTERN = "oauth2/v2.0/token?p="; + + private static final String JWKSET_URL_PATTERN = "discovery/v2.0/keys?p="; + + private static final String END_SESSION_URL_PATTERN = "oauth2/v2.0/logout?post_logout_redirect_uri=%s&p=%s"; + + private static final String AAD_TOKEN_URL_PATTERN = "https://login.microsoftonline.com/%s/oauth2/v2.0/token"; + private static final String AAD_JWKSET_URL_PATTERN = "https://login.microsoftonline.com/%s/discovery/v2.0/keys"; + + public static String getAuthorizationUrl(String baseUri) { + return addSlash(baseUri) + AUTHORIZATION_URL_PATTERN; + } + + public static String getTokenUrl(String baseUri, String userFlow) { + Assert.hasText(userFlow, "user flow should have text."); + + return addSlash(baseUri) + TOKEN_URL_PATTERN + userFlow; + } + + public static String getAADTokenUrl(String tenantId) { + Assert.hasText(tenantId, "tenantId should have text."); + return String.format(AAD_TOKEN_URL_PATTERN, tenantId); + } + + public static String getAADJwkSetUrl(String tenantId) { + Assert.hasText(tenantId, "tenantId should have text."); + return String.format(AAD_JWKSET_URL_PATTERN, tenantId); + } + + public static String getJwkSetUrl(String baseUri, String userFlow) { + Assert.hasText(userFlow, "user flow should have text."); + + return addSlash(baseUri) + JWKSET_URL_PATTERN + userFlow; + } + + public static String getEndSessionUrl(String baseUri, String logoutUrl, String userFlow) { + Assert.hasText(logoutUrl, "logoutUrl should have text."); + Assert.hasText(userFlow, "user flow should have text."); + + return addSlash(baseUri) + String.format(END_SESSION_URL_PATTERN, getEncodedURL(logoutUrl), userFlow); + } + + private static String getEncodedURL(String url) { + Assert.hasText(url, "url should have text."); + + try { + return URLEncoder.encode(url, "utf-8"); + } catch (UnsupportedEncodingException e) { + throw new AADB2CConfigurationException("failed to encode url: " + url, e); + } + } + + private static String addSlash(@URL String uri) { + return uri.endsWith("/") ? uri : uri + "/"; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AuthorizationClientProperties.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AuthorizationClientProperties.java new file mode 100644 index 0000000000000..5f5cf74b6270e --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AuthorizationClientProperties.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationGrantType; + +import java.util.List; + +/** + * Properties for an oauth2 client. + */ +public class AuthorizationClientProperties { + + private List scopes; + + private AADAuthorizationGrantType authorizationGrantType; + + public AADAuthorizationGrantType getAuthorizationGrantType() { + return authorizationGrantType; + } + + public void setAuthorizationGrantType(AADAuthorizationGrantType authorizationGrantType) { + this.authorizationGrantType = authorizationGrantType; + } + + public void setScopes(List scopes) { + this.scopes = scopes; + } + + public List getScopes() { + return scopes; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/package-info.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/package-info.java new file mode 100644 index 0000000000000..a43db843f5f48 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/package-info.java @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/** + * Package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c + */ +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/ClientRegistrationCondition.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/ClientRegistrationCondition.java new file mode 100644 index 0000000000000..7853f0f2ee58b --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/ClientRegistrationCondition.java @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.condition.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADApplicationType; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.AADAuthenticationProperties; +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; + +import java.util.Optional; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADApplicationType.RESOURCE_SERVER; + +/** + * Web application, web resource server or all in scenario condition. + */ +public final class ClientRegistrationCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + ConditionMessage.Builder message = ConditionMessage.forCondition("AAD Application Client Condition"); + AADAuthenticationProperties properties = + Binder.get(context.getEnvironment()) + .bind("azure.activedirectory", AADAuthenticationProperties.class) + .orElse(null); + if (properties == null) { + return ConditionOutcome.noMatch( + message.notAvailable("AAD authorization properties(azure.activedirectory" + ".xxx)")); + } + + if (!StringUtils.hasText(properties.getClientId())) { + return ConditionOutcome.noMatch(message.didNotFind("azure.activedirectory.client-id").atAll()); + } + + // Bind properties will not execute AADAuthenticationProperties#afterPropertiesSet() + AADApplicationType applicationType = Optional.ofNullable(properties.getApplicationType()) + .orElseGet(AADApplicationType::inferApplicationTypeByDependencies); + if (applicationType == null || applicationType == RESOURCE_SERVER) { + return ConditionOutcome.noMatch( + message.because("Resource server does not need client registration.")); + } + return ConditionOutcome.match( + message.foundExactly("azure.activedirectory.application-type=" + applicationType)); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/ResourceServerCondition.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/ResourceServerCondition.java new file mode 100644 index 0000000000000..deb77fcc8b39a --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/ResourceServerCondition.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.condition.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADApplicationType; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.AADAuthenticationProperties; +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 java.util.Optional; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADApplicationType.WEB_APPLICATION; + +/** + * Resource server or all in scenario condition. + */ +public final class ResourceServerCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + ConditionMessage.Builder message = ConditionMessage.forCondition("AAD Resource Server Condition"); + AADAuthenticationProperties properties = + Binder.get(context.getEnvironment()) + .bind("azure.activedirectory", AADAuthenticationProperties.class) + .orElse(null); + if (properties == null) { + return ConditionOutcome.noMatch(message.notAvailable("aad authorization properties")); + } + + // Bind properties will not execute AADAuthenticationProperties#afterPropertiesSet() + AADApplicationType applicationType = Optional.ofNullable(properties.getApplicationType()) + .orElseGet(AADApplicationType::inferApplicationTypeByDependencies); + if (applicationType == null || applicationType == WEB_APPLICATION) { + return ConditionOutcome.noMatch( + message.because("azure.activedirectory.application-type=" + applicationType)); + } + return ConditionOutcome.match( + message.foundExactly("azure.activedirectory.application-type=" + applicationType)); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/WebApplicationCondition.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/WebApplicationCondition.java new file mode 100644 index 0000000000000..4e24372807410 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/WebApplicationCondition.java @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.condition.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADApplicationType; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.AADAuthenticationProperties; +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; + +import java.util.Optional; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADApplicationType.RESOURCE_SERVER; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADApplicationType.RESOURCE_SERVER_WITH_OBO; + +/** + * Web application or all in scenario condition. + */ +public final class WebApplicationCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + ConditionMessage.Builder message = ConditionMessage.forCondition("AAD Web Application Condition"); + AADAuthenticationProperties properties = + Binder.get(context.getEnvironment()) + .bind("azure.activedirectory", AADAuthenticationProperties.class) + .orElse(null); + if (properties == null) { + return ConditionOutcome.noMatch(message.notAvailable("aad authorization properties")); + } + + if (!StringUtils.hasText(properties.getClientId())) { + return ConditionOutcome.noMatch(message.didNotFind("client-id").atAll()); + } + + // Bind properties will not execute AADAuthenticationProperties#afterPropertiesSet() + AADApplicationType applicationType = Optional.ofNullable(properties.getApplicationType()) + .orElseGet(AADApplicationType::inferApplicationTypeByDependencies); + if (applicationType == null + || applicationType == RESOURCE_SERVER + || applicationType == RESOURCE_SERVER_WITH_OBO) { + return ConditionOutcome.noMatch( + message.because("azure.activedirectory.application-type=" + applicationType)); + } + return ConditionOutcome.match( + message.foundExactly("azure.activedirectory.application-type=" + applicationType)); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/package-info.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/package-info.java new file mode 100644 index 0000000000000..d43643070a005 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/package-info.java @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/** + * Package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.condition.aad + */ +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.condition.aad; diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADApplicationTypeTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADApplicationTypeTest.java new file mode 100644 index 0000000000000..f94e5d1fe2599 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADApplicationTypeTest.java @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.util.ClassUtils; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADApplicationType.RESOURCE_SERVER; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADApplicationType.RESOURCE_SERVER_WITH_OBO; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADApplicationType.SPRING_SECURITY_OAUTH2_CLIENT_CLASS_NAME; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADApplicationType.SPRING_SECURITY_OAUTH2_RESOURCE_SERVER_CLASS_NAME; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADApplicationType.WEB_APPLICATION; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADApplicationType.inferApplicationTypeByDependencies; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mockStatic; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class AADApplicationTypeTest { + + @Test + public void noneApplicationType() { + try (MockedStatic classUtils = + mockStatic(ClassUtils.class, Mockito.CALLS_REAL_METHODS)) { + filterClassLoader(classUtils, false, false); + assertNull(inferApplicationTypeByDependencies()); + } + } + + @Test + public void webApplication() { + try (MockedStatic classUtils = + mockStatic(ClassUtils.class, Mockito.CALLS_REAL_METHODS)) { + filterClassLoader(classUtils, false, true); + assertSame(WEB_APPLICATION, inferApplicationTypeByDependencies()); + } + } + + @Test + public void resourceServer() { + try (MockedStatic classUtils = + mockStatic(ClassUtils.class, Mockito.CALLS_REAL_METHODS)) { + filterClassLoader(classUtils, true, false); + assertSame(RESOURCE_SERVER, inferApplicationTypeByDependencies()); + } + } + + @Test + public void resourceServerWithObo() { + try (MockedStatic classUtils = + mockStatic(ClassUtils.class, Mockito.CALLS_REAL_METHODS)) { + filterClassLoader(classUtils, true, true); + assertSame(RESOURCE_SERVER_WITH_OBO, inferApplicationTypeByDependencies()); + } + } + + private void filterClassLoader(MockedStatic classUtils, + boolean expectedTokenClassPresent, + boolean expectedRegistrationClassPresent) { + ClassLoader classLoader = this.getClass().getClassLoader(); + classUtils.when(ClassUtils::getDefaultClassLoader).thenReturn(classLoader); + classUtils.when(() -> ClassUtils.isPresent(SPRING_SECURITY_OAUTH2_RESOURCE_SERVER_CLASS_NAME, classLoader)) + .thenReturn(expectedTokenClassPresent); + classUtils.when(() -> ClassUtils.isPresent(SPRING_SECURITY_OAUTH2_CLIENT_CLASS_NAME, classLoader)) + .thenReturn(expectedRegistrationClassPresent); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADClientRegistrationRepositoryTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADClientRegistrationRepositoryTest.java new file mode 100644 index 0000000000000..1622a288b3ec0 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADClientRegistrationRepositoryTest.java @@ -0,0 +1,311 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.AADAuthenticationProperties; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationGrantType.AUTHORIZATION_CODE; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationGrantType.AZURE_DELEGATED; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADClientRegistrationRepository.AZURE_CLIENT_REGISTRATION_ID; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADClientRegistrationRepository.resourceServerCount; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.WebApplicationContextRunnerUtils.oauthClientRunner; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.WebApplicationContextRunnerUtils.webApplicationContextRunner; +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; + +public class AADClientRegistrationRepositoryTest { + + @Test + public void noClientsConfiguredTest() { + webApplicationContextRunner() + .run(context -> { + AADClientRegistrationRepository repository = + (AADClientRegistrationRepository) context.getBean(ClientRegistrationRepository.class); + assertEquals(new HashSet<>(Arrays.asList("openid", "profile", "offline_access")), + repository.getAzureClientAccessTokenScopes()); + + ClientRegistration azure = repository.findByRegistrationId(AZURE_CLIENT_REGISTRATION_ID); + assertEquals(AUTHORIZATION_CODE.getValue(), azure.getAuthorizationGrantType().getValue()); + assertEquals(new HashSet<>(Arrays.asList("openid", "profile", "offline_access")), azure.getScopes()); + List clients = collectClients(repository); + + assertEquals(1, clients.size()); + assertEquals(azure, clients.get(0)); + }); + } + + @Test + public void azureClientConfiguredTest() { + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.authorization-clients.azure.scopes = Azure.Scope" + ) + .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(new HashSet<>(Arrays.asList("Azure.Scope", "openid", "profile", "offline_access")), + azure.getScopes()); + + List clients = collectClients(repository); + assertEquals(Collections.singletonList(azure), clients); + }); + } + + @Test + public void graphClientConfiguredTest() { + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.authorization-clients.graph.scopes = Graph.Scope" + ) + .run(context -> { + AADClientRegistrationRepository repository = + (AADClientRegistrationRepository) context.getBean(ClientRegistrationRepository.class); + assertEquals(new HashSet<>(Arrays.asList("openid", "profile", "offline_access")), + repository.getAzureClientAccessTokenScopes()); + + ClientRegistration azure = repository.findByRegistrationId(AZURE_CLIENT_REGISTRATION_ID); + assertEquals(AUTHORIZATION_CODE.getValue(), azure.getAuthorizationGrantType().getValue()); + 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(new HashSet<>(Collections.singletonList("Graph.Scope")), graph.getScopes()); + + List clients = collectClients(repository); + assertEquals(Collections.singletonList(azure), clients); + }); + } + + @Test + public void onDemandGraphClientConfiguredTest() { + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.authorization-clients.graph.scopes = Graph.Scope", + "azure.activedirectory.authorization-clients.graph.on-demand = true" + ) + .run(context -> { + AADClientRegistrationRepository repository = + (AADClientRegistrationRepository) context.getBean(ClientRegistrationRepository.class); + assertEquals(new HashSet<>(Arrays.asList("openid", "profile", "offline_access")), + repository.getAzureClientAccessTokenScopes()); + + ClientRegistration azure = repository.findByRegistrationId(AZURE_CLIENT_REGISTRATION_ID); + assertEquals(AUTHORIZATION_CODE.getValue(), azure.getAuthorizationGrantType().getValue()); + assertEquals(new HashSet<>(Arrays.asList("openid", "profile", "offline_access")), + azure.getScopes()); + + ClientRegistration graph = repository.findByRegistrationId("graph"); + assertEquals(AUTHORIZATION_CODE.getValue(), graph.getAuthorizationGrantType().getValue()); + assertEquals(new HashSet<>(Arrays.asList("Graph.Scope", "openid", "profile", "offline_access")), + graph.getScopes()); + + List clients = collectClients(repository); + assertEquals(Arrays.asList(graph, azure), clients); + }); + } + + @Test + public void clientWithClientCredentialsPermissions() { + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.authorization-clients.graph.scopes = fakeValue:/.default", + "azure.activedirectory.authorization-clients.graph.authorizationGrantType = client_credentials" + ) + .run(context -> { + ClientRegistrationRepository repository = context.getBean(ClientRegistrationRepository.class); + assertEquals(repository.findByRegistrationId(AZURE_CLIENT_REGISTRATION_ID).getAuthorizationGrantType(), + AuthorizationGrantType.AUTHORIZATION_CODE); + assertEquals(repository.findByRegistrationId("graph").getAuthorizationGrantType(), + AuthorizationGrantType.CLIENT_CREDENTIALS); + }); + } + + @Test + public void clientWhichIsNotAuthorizationCodeButOnDemandExceptionTest() { + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.authorization-clients.graph.authorizationGrantType = client_credentials", + "azure.activedirectory.authorization-clients.graph.on-demand = true" + ) + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AADAuthenticationProperties.class)) + ); + } + + @Test + public void azureClientEndpointTest() { + webApplicationContextRunner() + .run(context -> { + ClientRegistrationRepository repository = context.getBean(ClientRegistrationRepository.class); + ClientRegistration azure = repository.findByRegistrationId(AZURE_CLIENT_REGISTRATION_ID); + + assertNotNull(azure); + assertEquals("fake-client-id", azure.getClientId()); + assertEquals("fake-client-secret", azure.getClientSecret()); + + AADAuthorizationServerEndpoints endpoints = new AADAuthorizationServerEndpoints( + "https://login.microsoftonline.com/", "fake-tenant-id"); + assertEquals(endpoints.authorizationEndpoint(), azure.getProviderDetails().getAuthorizationUri()); + assertEquals(endpoints.tokenEndpoint(), azure.getProviderDetails().getTokenUri()); + assertEquals(endpoints.jwkSetEndpoint(), azure.getProviderDetails().getJwkSetUri()); + assertEquals("{baseUrl}/login/oauth2/code/", azure.getRedirectUri()); + }); + } + + @Test + public void customizeUriTest() { + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.base-uri = http://localhost/" + ) + .run(context -> { + ClientRegistrationRepository repository = context.getBean(ClientRegistrationRepository.class); + ClientRegistration azure = repository.findByRegistrationId(AZURE_CLIENT_REGISTRATION_ID); + AADAuthorizationServerEndpoints endpoints = new AADAuthorizationServerEndpoints( + "http://localhost/", "fake-tenant-id"); + assertEquals(endpoints.authorizationEndpoint(), azure.getProviderDetails().getAuthorizationUri()); + assertEquals(endpoints.tokenEndpoint(), azure.getProviderDetails().getTokenUri()); + assertEquals(endpoints.jwkSetEndpoint(), azure.getProviderDetails().getJwkSetUri()); + }); + } + + @Test + public void testNoGroupIdAndGroupNameConfigured() { + webApplicationContextRunner() + .run(context -> { + ClientRegistrationRepository repository = context.getBean(ClientRegistrationRepository.class); + ClientRegistration azure = repository.findByRegistrationId(AZURE_CLIENT_REGISTRATION_ID); + assertEquals(new HashSet<>(Arrays.asList("openid", "profile", "offline_access")), azure.getScopes()); + }); + } + + @Test + public void testGroupNameConfigured() { + webApplicationContextRunner() + .withPropertyValues("azure.activedirectory.user-group.allowed-group-names = group1, group2") + .run(context -> { + ClientRegistrationRepository repository = context.getBean(ClientRegistrationRepository.class); + ClientRegistration azure = repository.findByRegistrationId(AZURE_CLIENT_REGISTRATION_ID); + assertEquals( + new HashSet<>(Arrays.asList( + "openid", "profile", "offline_access", "https://graph.microsoft.com/Directory.Read.All")), + azure.getScopes()); + }); + } + + @Test + public void testGroupIdConfigured() { + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.user-group.allowed-group-ids = 7c3a5d22-9093-42d7-b2eb-e72d06bf3718") + .run(context -> { + ClientRegistrationRepository repository = context.getBean(ClientRegistrationRepository.class); + ClientRegistration azure = repository.findByRegistrationId(AZURE_CLIENT_REGISTRATION_ID); + assertEquals( + new HashSet<>(Arrays.asList( + "openid", "profile", "offline_access", "https://graph.microsoft.com/User.Read")), + azure.getScopes()); + }); + } + + @Test + public void testGroupNameAndGroupIdConfigured() { + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.user-group.allowed-group-names = group1, group2", + "azure.activedirectory.user-group.allowed-group-ids = 7c3a5d22-9093-42d7-b2eb-e72d06bf3718") + .run(context -> { + ClientRegistrationRepository repository = context.getBean(ClientRegistrationRepository.class); + ClientRegistration azure = repository.findByRegistrationId(AZURE_CLIENT_REGISTRATION_ID); + assertEquals( + new HashSet<>(Arrays.asList( + "openid", "profile", "offline_access", "https://graph.microsoft.com/Directory.Read.All")), + azure.getScopes()); + }); + } + + @Test + public void haveResourceServerScopeInAccessTokenWhenThereAreMultiResourceServerScopesInAuthCode() { + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.authorization-clients.office.scopes = " + + "https://manage.office.com/ActivityFeed.Read", + "azure.activedirectory.authorization-clients.arm.scopes = " + + "https://management.core.windows.net/user_impersonation" + ) + .run(context -> { + AADClientRegistrationRepository repository = + (AADClientRegistrationRepository) context.getBean(ClientRegistrationRepository.class); + ClientRegistration azure = repository.findByRegistrationId(AZURE_CLIENT_REGISTRATION_ID); + assertNotNull(azure); + int resourceServerCountInAuthCode = resourceServerCount(azure.getScopes()); + assertTrue(resourceServerCountInAuthCode > 1); + int resourceServerCountInAccessToken = + resourceServerCount(repository.getAzureClientAccessTokenScopes()); + assertTrue(resourceServerCountInAccessToken != 0); + }); + } + + // TODO (moary) Enable this test. + // Related issue: https://github.com/Azure/azure-sdk-for-java/issues/23154 + @Disabled + @Test + public void noConfigurationOnMissingRequiredProperties() { + oauthClientRunner() + .run(context -> { + assertThat(context).doesNotHaveBean(ClientRegistrationRepository.class); + assertThat(context).doesNotHaveBean(OAuth2AuthorizedClientRepository.class); + assertThat(context).doesNotHaveBean(OAuth2UserService.class); + }); + } + + @Test + public void resourceServerCountTest() { + Set scopes = new HashSet<>(); + assertEquals(resourceServerCount(scopes), 0); + scopes.add("openid"); + scopes.add("profile"); + scopes.add("offline_access"); + assertEquals(resourceServerCount(scopes), 0); + scopes.add("https://graph.microsoft.com/User.Read"); + assertEquals(resourceServerCount(scopes), 1); + scopes.add("https://graph.microsoft.com/Directory.Read.All"); + assertEquals(resourceServerCount(scopes), 1); + scopes.add("https://manage.office.com/ActivityFeed.Read"); + assertEquals(resourceServerCount(scopes), 2); + scopes.add("https://manage.office.com/ActivityFeed.ReadDlp"); + assertEquals(resourceServerCount(scopes), 2); + scopes.add("https://manage.office.com/ServiceHealth.Read"); + assertEquals(resourceServerCount(scopes), 2); + } + + private List collectClients(Iterable itr) { + List result = new ArrayList<>(); + itr.forEach(result::add); + return result; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADOAuth2ClientConfigurationTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADOAuth2ClientConfigurationTest.java new file mode 100644 index 0000000000000..f953d5db24047 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/AADOAuth2ClientConfigurationTest.java @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.AADAuthenticationProperties; +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.web.OAuth2AuthorizedClientRepository; + +import java.util.Set; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.WebApplicationContextRunnerUtils.oauthClientAndResourceServerRunner; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.WebApplicationContextRunnerUtils.resourceServerContextRunner; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.WebApplicationContextRunnerUtils.resourceServerWithOboContextRunner; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.WebApplicationContextRunnerUtils.webApplicationContextRunner; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class AADOAuth2ClientConfigurationTest { + + @Test + public void testWithoutAnyPropertiesSet() { + new WebApplicationContextRunner() + .withUserConfiguration(AADOAuth2ClientConfiguration.class) + .run(context -> { + assertThat(context).doesNotHaveBean(AADAuthenticationProperties.class); + assertThat(context).doesNotHaveBean(ClientRegistrationRepository.class); + assertThat(context).doesNotHaveBean(OAuth2AuthorizedClientRepository.class); + }); + } + + @Test + public void testWithRequiredPropertiesSet() { + oauthClientAndResourceServerRunner() + .withPropertyValues("azure.activedirectory.client-id=fake-client-id") + .run(context -> { + assertThat(context).hasSingleBean(AADAuthenticationProperties.class); + assertThat(context).hasSingleBean(ClientRegistrationRepository.class); + assertThat(context).hasSingleBean(OAuth2AuthorizedClientRepository.class); + }); + } + + @Test + public void testWebApplication() { + webApplicationContextRunner() + .run(context -> { + assertThat(context).hasSingleBean(AADAuthenticationProperties.class); + assertThat(context).hasSingleBean(ClientRegistrationRepository.class); + assertThat(context).hasSingleBean(OAuth2AuthorizedClientRepository.class); + }); + } + + @Test + public void testResourceServer() { + resourceServerContextRunner() + .run(context -> assertThat(context).doesNotHaveBean(OAuth2AuthorizedClientRepository.class)); + } + + @Test + public void testResourceServerWithOboOnlyGraphClient() { + resourceServerWithOboContextRunner() + .withPropertyValues("azure.activedirectory.authorization-clients.graph.scopes=" + + "https://graph.microsoft.com/User.Read") + .run(context -> { + final AADClientRegistrationRepository oboRepo = context.getBean( + AADClientRegistrationRepository.class); + final OAuth2AuthorizedClientRepository aadOboRepo = context.getBean( + OAuth2AuthorizedClientRepository.class); + + ClientRegistration graph = oboRepo.findByRegistrationId("graph"); + Set graphScopes = graph.getScopes(); + + assertThat(aadOboRepo).isNotNull(); + assertThat(oboRepo).isExactlyInstanceOf(AADClientRegistrationRepository.class); + assertThat(graph).isNotNull(); + assertThat(graphScopes).containsOnly("https://graph.microsoft.com/User.Read"); + }); + } + + @Test + public void testResourceServerWithOboInvalidGrantType1() { + resourceServerWithOboContextRunner() + .withPropertyValues("azure.activedirectory.authorization-clients.graph.authorization-grant-type=" + + "authorization_code") + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AADAuthenticationProperties.class)) + ); + } + + @Test + public void testResourceServerWithOboInvalidGrantType2() { + resourceServerWithOboContextRunner() + .withPropertyValues("azure.activedirectory.authorization-clients.graph.authorization-grant-type=" + + "on_behalf_of") + .withPropertyValues("azure.activedirectory.authorization-clients.graph.on-demand = true") + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AADAuthenticationProperties.class)) + ); + } + + @Test + public void testResourceServerWithOboExistCustomAndGraphClient() { + resourceServerWithOboContextRunner() + .withPropertyValues("azure.activedirectory.authorization-clients.graph.scopes=" + + "https://graph.microsoft.com/User.Read") + .withPropertyValues("azure.activedirectory.authorization-clients.custom.scopes=" + + "api://52261059-e515-488e-84fd-a09a3f372814/File.Read") + .run(context -> { + final AADClientRegistrationRepository oboRepo = context.getBean( + AADClientRegistrationRepository.class); + final OAuth2AuthorizedClientRepository aadOboRepo = context.getBean( + OAuth2AuthorizedClientRepository.class); + + ClientRegistration graph = oboRepo.findByRegistrationId("graph"); + ClientRegistration custom = oboRepo.findByRegistrationId("custom"); + Set graphScopes = graph.getScopes(); + Set customScopes = custom.getScopes(); + + assertThat(aadOboRepo).isNotNull(); + assertThat(oboRepo).isExactlyInstanceOf(AADClientRegistrationRepository.class); + assertThat(graph).isNotNull(); + assertThat(customScopes).isNotNull(); + assertThat(graphScopes).containsOnly("https://graph.microsoft.com/User.Read"); + assertThat(customScopes).containsOnly("api://52261059-e515-488e-84fd-a09a3f372814/File.Read"); + }); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/WebApplicationContextRunnerUtils.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/WebApplicationContextRunnerUtils.java new file mode 100644 index 0000000000000..b075ddda6d07c --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/WebApplicationContextRunnerUtils.java @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.AADAutoConfiguration; +import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; +import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.http.HttpEntity; +import org.springframework.http.RequestEntity; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.util.MultiValueMap; + +import java.util.Optional; + +public class WebApplicationContextRunnerUtils { + + public static WebApplicationContextRunner oauthClientAndResourceServerRunner() { + return new WebApplicationContextRunner() + .withUserConfiguration(AADAutoConfiguration.class) + .withInitializer(new ConditionEvaluationReportLoggingListener(LogLevel.INFO)); + } + + public static WebApplicationContextRunner oauthClientRunner() { + return oauthClientAndResourceServerRunner() + .withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class)); + } + + public static WebApplicationContextRunner resourceServerRunner() { + return oauthClientAndResourceServerRunner() + .withClassLoader(new FilteredClassLoader(ClientRegistration.class)); + } + + public static WebApplicationContextRunner webApplicationContextRunner() { + return oauthClientRunner() + .withPropertyValues(withWebApplicationOrResourceServerWithOboPropertyValues()); + } + + public static WebApplicationContextRunner resourceServerContextRunner() { + return resourceServerRunner() + .withPropertyValues(withResourceServerPropertyValues()); + } + + public static WebApplicationContextRunner resourceServerWithOboContextRunner() { + return oauthClientAndResourceServerRunner() + .withPropertyValues(withWebApplicationOrResourceServerWithOboPropertyValues()) + .withPropertyValues(withResourceServerPropertyValues()); + } + + @SuppressWarnings("unchecked") + public static MultiValueMap toMultiValueMap(RequestEntity entity) { + return (MultiValueMap) Optional.ofNullable(entity) + .map(HttpEntity::getBody) + .orElse(null); + } + + public static String[] withWebApplicationOrResourceServerWithOboPropertyValues() { + return new String[] { + "azure.activedirectory.client-id = fake-client-id", + "azure.activedirectory.client-secret = fake-client-secret", + "azure.activedirectory.tenant-id = fake-tenant-id"}; + } + + public static String[] withResourceServerPropertyValues() { + return new String[] { + "azure.activedirectory.tenant-id=fake-tenant-id", + "azure.activedirectory.app-id-uri=fake-app-id-uri"}; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/SerializerUtilsTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/SerializerUtilsTest.java new file mode 100644 index 0000000000000..1d005224c6c94 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/implementation/jackson/SerializerUtilsTest.java @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.jackson; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationGrantType; +import org.junit.jupiter.api.Test; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AccessToken; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADClientRegistrationRepository.AZURE_CLIENT_REGISTRATION_ID; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.jackson.SerializerUtils.deserializeOAuth2AuthorizedClientMap; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.jackson.SerializerUtils.serializeOAuth2AuthorizedClientMap; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SerializerUtilsTest { + + private static final Logger LOGGER = Logger.getLogger(SerializerUtilsTest.class.getName()); + + @Test + public void serializeAndDeserializeTest() { + Map authorizedClients = new HashMap<>(); + authorizedClients.put(AZURE_CLIENT_REGISTRATION_ID, + createOAuth2AuthorizedClient(AZURE_CLIENT_REGISTRATION_ID, + AADAuthorizationGrantType.AUTHORIZATION_CODE.getValue())); + authorizedClients.put("graph", + createOAuth2AuthorizedClient("graph", + AADAuthorizationGrantType.AZURE_DELEGATED.getValue())); + authorizedClients.put("arm", + createOAuth2AuthorizedClient("arm", + AADAuthorizationGrantType.CLIENT_CREDENTIALS.getValue())); + authorizedClients.put("office", + createOAuth2AuthorizedClient("office", + AADAuthorizationGrantType.ON_BEHALF_OF.getValue())); + String serializedOAuth2AuthorizedClients = serializeOAuth2AuthorizedClientMap(authorizedClients); + LOGGER.info(serializedOAuth2AuthorizedClients); + Map deserializedOAuth2AuthorizedClients = + deserializeOAuth2AuthorizedClientMap(serializedOAuth2AuthorizedClients); + String serializedOAuth2AuthorizedClients1 = serializeOAuth2AuthorizedClientMap(deserializedOAuth2AuthorizedClients); + assertEquals(serializedOAuth2AuthorizedClients, serializedOAuth2AuthorizedClients1); + } + + private OAuth2AuthorizedClient createOAuth2AuthorizedClient(String registrationId, String authorizationGrantType) { + ClientRegistration clientRegistration = createClientRegistration(registrationId, authorizationGrantType); + OAuth2AccessToken oAuth2AccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + "tokenValue " + registrationId, Instant.now(), Instant.now().plusSeconds(60 * 60)); + return new OAuth2AuthorizedClient(clientRegistration, "principalName " + registrationId, oAuth2AccessToken); + } + + private ClientRegistration createClientRegistration(String registrationId, String authorizationGrantType) { + return ClientRegistration.withRegistrationId(registrationId) + .clientName(registrationId) + .authorizationGrantType(new AuthorizationGrantType(authorizationGrantType)) + .scope("scope" + registrationId) + .redirectUri("redirectUri " + registrationId) + .userNameAttributeName("userNameAttributeName " + registrationId) + .clientId("clientId " + registrationId) + .clientSecret("clientSecret " + registrationId) + .authorizationUri("authorizationUri " + registrationId) + .tokenUri("tokenUri " + registrationId) + .jwkSetUri("jwkSetUri " + registrationId) + .providerConfigurationMetadata(new HashMap<>()) + .build(); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADJwtBearerTokenAuthenticationConverterTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADJwtBearerTokenAuthenticationConverterTest.java new file mode 100644 index 0000000000000..5545a94c2c519 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADJwtBearerTokenAuthenticationConverterTest.java @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADOAuth2AuthenticatedPrincipal; +import net.minidev.json.JSONArray; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class AADJwtBearerTokenAuthenticationConverterTest { + + private Jwt jwt = mock(Jwt.class); + private Map claims = new HashMap<>(); + private Map headers = new HashMap<>(); + private JSONArray jsonArray = new JSONArray().appendElement("User.read").appendElement("User.write"); + + @BeforeAll + public void init() { + claims.put("iss", "fake-issuer"); + claims.put("tid", "fake-tid"); + headers.put("kid", "kg2LYs2T0CTjIfj4rt6JIynen38"); + when(jwt.getClaim("scp")).thenReturn("Order.read Order.write"); + when(jwt.getClaim("roles")).thenReturn(jsonArray); + when(jwt.getTokenValue()).thenReturn("fake-token-value"); + when(jwt.getIssuedAt()).thenReturn(Instant.now()); + when(jwt.getHeaders()).thenReturn(headers); + when(jwt.getExpiresAt()).thenReturn(Instant.MAX); + when(jwt.getClaims()).thenReturn(claims); + } + + @Test + public void testCreateUserPrincipal() { + AADJwtBearerTokenAuthenticationConverter converter = new AADJwtBearerTokenAuthenticationConverter(); + AbstractAuthenticationToken authenticationToken = converter.convert(jwt); + assertThat(authenticationToken.getPrincipal()).isExactlyInstanceOf(AADOAuth2AuthenticatedPrincipal.class); + AADOAuth2AuthenticatedPrincipal principal = (AADOAuth2AuthenticatedPrincipal) authenticationToken + .getPrincipal(); + assertThat(principal.getClaims()).isNotEmpty(); + assertThat(principal.getIssuer()).isEqualTo(claims.get("iss")); + assertThat(principal.getTenantId()).isEqualTo(claims.get("tid")); + } + + @Test + public void testNoArgumentsConstructorDefaultScopeAndRoleAuthorities() { + AADJwtBearerTokenAuthenticationConverter converter = new AADJwtBearerTokenAuthenticationConverter(); + AbstractAuthenticationToken authenticationToken = converter.convert(jwt); + assertThat(authenticationToken.getPrincipal()).isExactlyInstanceOf(AADOAuth2AuthenticatedPrincipal.class); + AADOAuth2AuthenticatedPrincipal principal = (AADOAuth2AuthenticatedPrincipal) authenticationToken + .getPrincipal(); + assertThat(principal.getAttributes()).isNotEmpty(); + assertThat(principal.getAttributes()).hasSize(2); + assertThat(principal.getAuthorities()).hasSize(4); + } + + @Test + public void testNoArgumentsConstructorExtractScopeAuthorities() { + AADJwtBearerTokenAuthenticationConverter converter = new AADJwtBearerTokenAuthenticationConverter(); + AbstractAuthenticationToken authenticationToken = converter.convert(jwt); + assertThat(authenticationToken.getPrincipal()).isExactlyInstanceOf(AADOAuth2AuthenticatedPrincipal.class); + AADOAuth2AuthenticatedPrincipal principal = (AADOAuth2AuthenticatedPrincipal) authenticationToken + .getPrincipal(); + assertThat(principal.getAttributes()).isNotEmpty(); + assertThat(principal.getAttributes()).hasSize(2); + assertThat(principal.getAuthorities()).hasSize(4); + } + + @Test + public void testParameterConstructorExtractScopeAuthorities() { + AADJwtBearerTokenAuthenticationConverter converter = new AADJwtBearerTokenAuthenticationConverter("scp"); + AbstractAuthenticationToken authenticationToken = converter.convert(jwt); + assertThat(authenticationToken.getPrincipal()).isExactlyInstanceOf(AADOAuth2AuthenticatedPrincipal.class); + AADOAuth2AuthenticatedPrincipal principal = (AADOAuth2AuthenticatedPrincipal) authenticationToken + .getPrincipal(); + assertThat(principal.getAttributes()).isNotEmpty(); + assertThat(principal.getAttributes()).hasSize(2); + assertThat(principal.getAuthorities()).hasSize(2); + } + + @Test + public void testParameterConstructorExtractRoleAuthorities() { + AADJwtBearerTokenAuthenticationConverter converter = new AADJwtBearerTokenAuthenticationConverter("roles", + "APPROLE_"); + AbstractAuthenticationToken authenticationToken = converter.convert(jwt); + assertThat(authenticationToken.getPrincipal()).isExactlyInstanceOf(AADOAuth2AuthenticatedPrincipal.class); + AADOAuth2AuthenticatedPrincipal principal = (AADOAuth2AuthenticatedPrincipal) authenticationToken + .getPrincipal(); + assertThat(principal.getAttributes()).isNotEmpty(); + assertThat(principal.getAttributes()).hasSize(2); + assertThat(principal.getAuthorities()).hasSize(2); + } + + @Test + public void testConstructorExtractRoleAuthoritiesWithAuthorityPrefixMapParameter() { + Map claimToAuthorityPrefixMap = new HashMap<>(); + claimToAuthorityPrefixMap.put("roles", "APPROLE_"); + AADJwtBearerTokenAuthenticationConverter converter = new AADJwtBearerTokenAuthenticationConverter("scp", claimToAuthorityPrefixMap); + AbstractAuthenticationToken authenticationToken = converter.convert(jwt); + assertThat(authenticationToken.getPrincipal()).isExactlyInstanceOf(AADOAuth2AuthenticatedPrincipal.class); + AADOAuth2AuthenticatedPrincipal principal = (AADOAuth2AuthenticatedPrincipal) authenticationToken + .getPrincipal(); + assertThat(principal.getAttributes()).isNotEmpty(); + assertThat(principal.getAttributes()).hasSize(2); + assertThat(principal.getAuthorities()).hasSize(2); + Assertions.assertTrue(principal.getAuthorities().contains(new SimpleGrantedAuthority("APPROLE_User.read"))); + Assertions.assertTrue(principal.getAuthorities().contains(new SimpleGrantedAuthority("APPROLE_User.write"))); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADResourceServerConfigurationTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADResourceServerConfigurationTest.java new file mode 100644 index 0000000000000..12ac0c406159c --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADResourceServerConfigurationTest.java @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi; + +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; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; + +import java.util.List; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.WebApplicationContextRunnerUtils.resourceServerContextRunner; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class AADResourceServerConfigurationTest { + + @Test + public void testNotExistBearerTokenAuthenticationToken() { + resourceServerContextRunner() + .withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class)) + .run(context -> assertThrows(NoSuchBeanDefinitionException.class, + () -> context.getBean(JWTClaimsSetAwareJWSKeySelector.class))); + } + + @Test + public void testCreateJwtDecoderByJwkKeySetUri() { + resourceServerContextRunner() + .run(context -> { + final JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class); + assertThat(jwtDecoder).isNotNull(); + assertThat(jwtDecoder).isExactlyInstanceOf(NimbusJwtDecoder.class); + }); + } + + @Test + public void testNotAudienceDefaultValidator() { + resourceServerContextRunner() + .run(context -> { + AADResourceServerConfiguration bean = context + .getBean(AADResourceServerConfiguration.class); + List> defaultValidator = bean.createDefaultValidator(); + assertThat(defaultValidator).isNotNull(); + assertThat(defaultValidator).hasSize(3); + }); + } + + @Test + public void testExistAudienceDefaultValidator() { + resourceServerContextRunner() + .run(context -> { + AADResourceServerConfiguration bean = context + .getBean(AADResourceServerConfiguration.class); + List> defaultValidator = bean.createDefaultValidator(); + assertThat(defaultValidator).isNotNull(); + assertThat(defaultValidator).hasSize(3); + }); + } + + @Test + public void testCreateWebSecurityConfigurerAdapter() { + resourceServerContextRunner() + .run(context -> { + WebSecurityConfigurerAdapter webSecurityConfigurerAdapter = context + .getBean(AADResourceServerConfiguration.DefaultAADResourceServerWebSecurityConfigurerAdapter.class); + assertThat(webSecurityConfigurerAdapter).isNotNull(); + }); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADResourceServerPropertiesTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADResourceServerPropertiesTest.java new file mode 100644 index 0000000000000..ef93d33111270 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/AADResourceServerPropertiesTest.java @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.constants.AADTokenClaim; +import org.junit.jupiter.api.Test; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.WebApplicationContextRunnerUtils.resourceServerContextRunner; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi.AADResourceServerProperties.DEFAULT_CLAIM_TO_AUTHORITY_PREFIX_MAP; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class AADResourceServerPropertiesTest { + + @Test + public void testNoPropertiesConfigured() { + resourceServerContextRunner() + .run(context -> { + AADResourceServerProperties properties = context.getBean(AADResourceServerProperties.class); + assertEquals(AADTokenClaim.SUB, properties.getPrincipalClaimName()); + assertEquals(DEFAULT_CLAIM_TO_AUTHORITY_PREFIX_MAP, properties.getClaimToAuthorityPrefixMap()); + }); + } + + @Test + public void testPropertiesConfigured() { + resourceServerContextRunner() + .withPropertyValues( + "azure.activedirectory.resource-server.principal-claim-name=fake-claim-name", + "azure.activedirectory.resource-server.claim-to-authority-prefix-map.fake-key-1=fake-value-1", + "azure.activedirectory.resource-server.claim-to-authority-prefix-map.fake-key-2=fake-value-2") + .run(context -> { + AADResourceServerProperties properties = context.getBean(AADResourceServerProperties.class); + assertEquals(properties.getPrincipalClaimName(), "fake-claim-name"); + assertEquals(2, properties.getClaimToAuthorityPrefixMap().size()); + assertEquals("fake-value-1", properties.getClaimToAuthorityPrefixMap().get("fake-key-1")); + assertEquals("fake-value-2", properties.getClaimToAuthorityPrefixMap().get("fake-key-2")); + }); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/AADJwtAudienceValidatorTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/AADJwtAudienceValidatorTest.java new file mode 100644 index 0000000000000..52a3197c47177 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/AADJwtAudienceValidatorTest.java @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi.validator; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.constants.AADTokenClaim; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.AADAuthenticationProperties; +import org.junit.jupiter.api.Test; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AADJwtAudienceValidatorTest { + + final AADAuthenticationProperties aadAuthenticationProperties = mock(AADAuthenticationProperties.class); + final Jwt jwt = mock(Jwt.class); + final List audiences = new ArrayList<>(); + final List claimAudience = new ArrayList<>(); + + @Test + public void testClientIdExistAndSuccessVerify() { + when(aadAuthenticationProperties.getClientId()).thenReturn("fake-client-id"); + when(jwt.getClaim(AADTokenClaim.AUD)).thenReturn(claimAudience); + claimAudience.add("fake-client-id"); + audiences.add(aadAuthenticationProperties.getClientId()); + AADJwtAudienceValidator validator = new AADJwtAudienceValidator(audiences); + OAuth2TokenValidatorResult result = validator.validate(jwt); + + assertThat(result).isNotNull(); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void testAppIdUriExistAndSuccessVerify() { + when(aadAuthenticationProperties.getClientId()).thenReturn("fake-app-id-uri"); + when(jwt.getClaim(AADTokenClaim.AUD)).thenReturn(claimAudience); + claimAudience.add("fake-app-id-uri"); + audiences.add(aadAuthenticationProperties.getClientId()); + AADJwtAudienceValidator validator = new AADJwtAudienceValidator(audiences); + OAuth2TokenValidatorResult result = validator.validate(jwt); + + assertThat(result).isNotNull(); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void testAppIdUriExistAndClientIdAndSuccessVerify() { + when(aadAuthenticationProperties.getClientId()).thenReturn("fake-app-id-uri"); + when(aadAuthenticationProperties.getAppIdUri()).thenReturn("fake-client-id"); + when(jwt.getClaim(AADTokenClaim.AUD)).thenReturn(claimAudience); + //claimAudience.add("fake-client-id"); + claimAudience.add("fake-app-id-uri"); + audiences.add(aadAuthenticationProperties.getClientId()); + audiences.add(aadAuthenticationProperties.getAppIdUri()); + AADJwtAudienceValidator validator = new AADJwtAudienceValidator(audiences); + OAuth2TokenValidatorResult result = validator.validate(jwt); + + assertThat(result).isNotNull(); + assertThat(result.getErrors()).isEmpty(); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/AADJwtIssuerValidatorTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/AADJwtIssuerValidatorTest.java new file mode 100644 index 0000000000000..7e3d8b4d7c8b5 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/AADJwtIssuerValidatorTest.java @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi.validator; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADTrustedIssuerRepository; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.constants.AADTokenClaim; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.AADAuthenticationProperties; +import org.junit.jupiter.api.Test; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jwt.Jwt; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AADJwtIssuerValidatorTest { + + private final AADAuthenticationProperties aadAuthenticationProperties = mock(AADAuthenticationProperties.class); + private final Jwt jwt = mock(Jwt.class); + private final AADTrustedIssuerRepository aadTrustedIssuerRepository = new AADTrustedIssuerRepository("fake-tenant" + + "-id"); + + @Test + public void testNoStructureIssuerSuccessVerify() { + when(aadAuthenticationProperties.getTenantId()).thenReturn("fake-tenant-id"); + when(jwt.getClaim(AADTokenClaim.ISS)).thenReturn("https://sts.windows.net/fake-tenant-id/v2.0"); + + AADJwtIssuerValidator validator = new AADJwtIssuerValidator(); + OAuth2TokenValidatorResult result = validator.validate(jwt); + assertThat(result).isNotNull(); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void testNoStructureIssuerFailureVerify() { + when(aadAuthenticationProperties.getTenantId()).thenReturn("common"); + when(jwt.getClaim(AADTokenClaim.ISS)).thenReturn("https://sts.failure.net/fake-tenant-id/v2.0"); + + AADJwtIssuerValidator validator = new AADJwtIssuerValidator(); + OAuth2TokenValidatorResult result = validator.validate(jwt); + assertThat(result).isNotNull(); + assertThat(result.getErrors()).isNotEmpty(); + } + + @Test + public void testIssuerSuccessVerify() { + when(aadAuthenticationProperties.getTenantId()).thenReturn("fake-tenant-id"); + when(jwt.getClaim(AADTokenClaim.ISS)).thenReturn("https://sts.windows.net/fake-tenant-id/v2.0"); + + AADJwtIssuerValidator validator = new AADJwtIssuerValidator(aadTrustedIssuerRepository); + OAuth2TokenValidatorResult result = validator.validate(jwt); + assertThat(result).isNotNull(); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void testIssuerFailureVerify() { + when(aadAuthenticationProperties.getTenantId()).thenReturn("common"); + when(jwt.getClaim(AADTokenClaim.ISS)).thenReturn("https://sts.failure.net/fake-tenant-id/v2.0"); + + AADJwtIssuerValidator validator = new AADJwtIssuerValidator(aadTrustedIssuerRepository); + OAuth2TokenValidatorResult result = validator.validate(jwt); + assertThat(result).isNotNull(); + assertThat(result.getErrors()).isNotEmpty(); + } + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADAccessTokenGroupRolesExtractionTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADAccessTokenGroupRolesExtractionTest.java new file mode 100644 index 0000000000000..44cfbfaefd073 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADAccessTokenGroupRolesExtractionTest.java @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.WebApplicationContextRunnerUtils; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.AADAuthenticationProperties; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.security.oauth2.core.OAuth2AccessToken; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class AADAccessTokenGroupRolesExtractionTest { + + private static final String GROUP_ID_1 = "d07c0bd6-4aab-45ac-b87c-23e8d00194ab"; + private static final String GROUP_ID_2 = "6eddcc22-a24a-4459-b036-b9d9fc0f0bc7"; + + private AutoCloseable autoCloseable; + + @Mock + private OAuth2AccessToken accessToken; + + @Mock + private GraphClient graphClient; + + @BeforeAll + public void setup() { + this.autoCloseable = MockitoAnnotations.openMocks(this); + GroupInformation groupInformationFromGraph = new GroupInformation(); + Set groupNamesFromGraph = new HashSet<>(); + Set groupIdsFromGraph = new HashSet<>(); + groupNamesFromGraph.add("group1"); + groupNamesFromGraph.add("group2"); + groupIdsFromGraph.add(GROUP_ID_1); + groupIdsFromGraph.add(GROUP_ID_2); + groupInformationFromGraph.setGroupsIds(groupIdsFromGraph); + groupInformationFromGraph.setGroupsNames(groupNamesFromGraph); + Mockito.lenient().when(accessToken.getTokenValue()) + .thenReturn("fake-access-token"); + Mockito.lenient().when(graphClient.getGroupInformation(accessToken.getTokenValue())) + .thenReturn(groupInformationFromGraph); + } + + @AfterAll + public void close() throws Exception { + this.autoCloseable.close(); + } + + private AADAuthenticationProperties getProperties() { + AADAuthenticationProperties properties = new AADAuthenticationProperties(); + AADAuthenticationProperties.UserGroupProperties userGroup = + new AADAuthenticationProperties.UserGroupProperties(); + properties.setUserGroup(userGroup); + properties.setGraphMembershipUri("https://graph.microsoft.com/v1.0/me/memberOf"); + return properties; + } + + @Test + public void testAllowedGroupsNames() { + List allowedGroupNames = new ArrayList<>(); + allowedGroupNames.add("group1"); + + AADAuthenticationProperties properties = getProperties(); + properties.getUserGroup().setAllowedGroupNames(allowedGroupNames); + + AADOAuth2UserService userService = new AADOAuth2UserService(properties, graphClient); + Set groupRoles = userService.extractGroupRolesFromAccessToken(accessToken); + assertThat(groupRoles).hasSize(1); + assertThat(groupRoles).contains("ROLE_group1"); + assertThat(groupRoles).doesNotContain("ROLE_group2"); + } + + @Test + public void testAllowedGroupsIds() { + Set allowedGroupIds = new HashSet<>(); + allowedGroupIds.add(GROUP_ID_1); + + AADAuthenticationProperties properties = getProperties(); + properties.getUserGroup().setAllowedGroupIds(allowedGroupIds); + + AADOAuth2UserService userService = new AADOAuth2UserService(properties, graphClient); + Set groupRoles = userService.extractGroupRolesFromAccessToken(accessToken); + assertThat(groupRoles).hasSize(1); + assertThat(groupRoles).contains("ROLE_" + GROUP_ID_1); + assertThat(groupRoles).doesNotContain("ROLE_" + GROUP_ID_2); + } + + @Test + public void testAllowedGroupsNamesAndAllowedGroupsIds() { + Set allowedGroupIds = new HashSet<>(); + allowedGroupIds.add(GROUP_ID_1); + List allowedGroupNames = new ArrayList<>(); + allowedGroupNames.add("group1"); + + + AADAuthenticationProperties properties = getProperties(); + properties.getUserGroup().setAllowedGroupIds(allowedGroupIds); + properties.getUserGroup().setAllowedGroupNames(allowedGroupNames); + + AADOAuth2UserService userService = new AADOAuth2UserService(properties, graphClient); + Set groupRoles = userService.extractGroupRolesFromAccessToken(accessToken); + assertThat(groupRoles).hasSize(2); + assertThat(groupRoles).contains("ROLE_group1"); + assertThat(groupRoles).doesNotContain("ROLE_group2"); + assertThat(groupRoles).contains("ROLE_" + GROUP_ID_1); + assertThat(groupRoles).doesNotContain("ROLE_" + GROUP_ID_2); + } + + @Test + public void testWithEnableFullList() { + Set allowedGroupIds = new HashSet<>(); + allowedGroupIds.add(GROUP_ID_1); + List allowedGroupNames = new ArrayList<>(); + allowedGroupNames.add("group1"); + + AADAuthenticationProperties properties = getProperties(); + properties.getUserGroup().setAllowedGroupIds(allowedGroupIds); + properties.getUserGroup().setAllowedGroupNames(allowedGroupNames); + properties.getUserGroup().setEnableFullList(true); + + AADOAuth2UserService userService = new AADOAuth2UserService(properties, graphClient); + Set groupRoles = userService.extractGroupRolesFromAccessToken(accessToken); + assertThat(groupRoles).hasSize(3); + assertThat(groupRoles).contains("ROLE_group1"); + assertThat(groupRoles).contains("ROLE_" + GROUP_ID_1); + assertThat(groupRoles).contains("ROLE_" + GROUP_ID_2); + } + + @Test + public void testWithoutEnableFullList() { + List allowedGroupNames = new ArrayList<>(); + Set allowedGroupIds = new HashSet<>(); + allowedGroupIds.add(GROUP_ID_1); + allowedGroupNames.add("group1"); + + AADAuthenticationProperties properties = getProperties(); + properties.getUserGroup().setEnableFullList(false); + properties.getUserGroup().setAllowedGroupIds(allowedGroupIds); + properties.getUserGroup().setAllowedGroupNames(allowedGroupNames); + + AADOAuth2UserService userService = new AADOAuth2UserService(properties, graphClient); + Set groupRoles = userService.extractGroupRolesFromAccessToken(accessToken); + assertThat(groupRoles).hasSize(2); + assertThat(groupRoles).contains("ROLE_group1"); + assertThat(groupRoles).doesNotContain("ROLE_group2"); + assertThat(groupRoles).contains("ROLE_" + GROUP_ID_1); + assertThat(groupRoles).doesNotContain("ROLE_" + GROUP_ID_2); + } + + @Test + public void testAllowedGroupIdsAllWithoutEnableFullList() { + Set allowedGroupIds = new HashSet<>(); + allowedGroupIds.add("all"); + List allowedGroupNames = new ArrayList<>(); + allowedGroupNames.add("group1"); + + AADAuthenticationProperties properties = getProperties(); + properties.getUserGroup().setAllowedGroupIds(allowedGroupIds); + properties.getUserGroup().setAllowedGroupNames(allowedGroupNames); + properties.getUserGroup().setEnableFullList(false); + + AADOAuth2UserService userService = new AADOAuth2UserService(properties, graphClient); + Set groupRoles = userService.extractGroupRolesFromAccessToken(accessToken); + assertThat(groupRoles).hasSize(3); + assertThat(groupRoles).contains("ROLE_group1"); + assertThat(groupRoles).doesNotContain("ROLE_group2"); + assertThat(groupRoles).contains("ROLE_" + GROUP_ID_1); + assertThat(groupRoles).contains("ROLE_" + GROUP_ID_2); + } + + @Test + public void testIllegalGroupIdParam() { + WebApplicationContextRunnerUtils + .webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.user-group.allowed-group-ids = all," + GROUP_ID_1 + ) + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AADAuthenticationProperties.class))); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADAzureDelegatedOAuth2AuthorizedClientProviderTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADAzureDelegatedOAuth2AuthorizedClientProviderTest.java new file mode 100644 index 0000000000000..05bffb8afd0b5 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADAzureDelegatedOAuth2AuthorizedClientProviderTest.java @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationGrantType; +import org.junit.jupiter.api.Test; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; +import org.springframework.security.oauth2.client.OAuth2AuthorizationContext; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +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.active.directory.implementation.aad.AADClientRegistrationRepository.AZURE_CLIENT_REGISTRATION_ID; +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; + +public class AADAzureDelegatedOAuth2AuthorizedClientProviderTest { + + private static final ClientRegistration AZURE_CLIENT_REGISTRATION = + toClientRegistrationBuilder(AZURE_CLIENT_REGISTRATION_ID) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .build(); + + private static final ClientRegistration DELEGATED_CLIENT_REGISTRATION = + toClientRegistrationBuilder("delegated") + .authorizationGrantType(new AuthorizationGrantType(AADAuthorizationGrantType.AZURE_DELEGATED.getValue())) + .scope("testScope") + .build(); + + private static final ClientRegistration CLIENT_CREDENTIALS_CLIENT_REGISTRATION = + toClientRegistrationBuilder("clientCredentials") + .authorizationGrantType(new AuthorizationGrantType(AADAuthorizationGrantType.CLIENT_CREDENTIALS.getValue())) + .build(); + + private static ClientRegistration.Builder toClientRegistrationBuilder(String registrationId) { + return ClientRegistration.withRegistrationId(registrationId) + .clientId("clientId") + .clientSecret("clientSecret") + .redirectUri("redirectUri") + .authorizationUri("authorizationUri") + .tokenUri("tokenUri"); + } + + @Test + public void testGrantTypeIsNotAzureDelegated() { + AADAzureDelegatedOAuth2AuthorizedClientProvider provider = + new AADAzureDelegatedOAuth2AuthorizedClientProvider(null, null); + Authentication principal = mock(Authentication.class); + OAuth2AuthorizationContext context = + OAuth2AuthorizationContext.withClientRegistration(AZURE_CLIENT_REGISTRATION) + .principal(principal) + .build(); + assertNull(provider.authorize(context)); + context = OAuth2AuthorizationContext.withClientRegistration(CLIENT_CREDENTIALS_CLIENT_REGISTRATION) + .principal(principal) + .build(); + assertNull(provider.authorize(context)); + } + + @Test + public void testDelegatedClientNotExpired() { + OAuth2AuthorizedClientRepository authorizedClientRepository = mock(OAuth2AuthorizedClientRepository.class); + OAuth2AccessToken oAuth2AccessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, + "tokenValue", + Instant.now(), + Instant.now().plusSeconds(60 * 60)); + OAuth2AuthorizedClient delegatedAuthorizedClient = new OAuth2AuthorizedClient( + DELEGATED_CLIENT_REGISTRATION, + "principalName", + oAuth2AccessToken); + Authentication principal = mock(Authentication.class); + OAuth2AuthorizationContext context = + OAuth2AuthorizationContext.withAuthorizedClient(delegatedAuthorizedClient) + .principal(principal) + .build(); + AADAzureDelegatedOAuth2AuthorizedClientProvider provider = + new AADAzureDelegatedOAuth2AuthorizedClientProvider(null, authorizedClientRepository); + assertNull(provider.authorize(context)); + } + + @Test + public void testAzureClientIsNull() { + OAuth2AuthorizedClientRepository authorizedClientRepository = mock(OAuth2AuthorizedClientRepository.class); + when(authorizedClientRepository.loadAuthorizedClient(any(), any(), any())).thenReturn(null); + Authentication principal = mock(Authentication.class); + AADAzureDelegatedOAuth2AuthorizedClientProvider provider = + new AADAzureDelegatedOAuth2AuthorizedClientProvider(null, authorizedClientRepository); + OAuth2AuthorizationContext context = + OAuth2AuthorizationContext.withClientRegistration(DELEGATED_CLIENT_REGISTRATION) + .principal(principal) + .build(); + assertThrows(ClientAuthorizationRequiredException.class, () -> provider.authorize(context)); + } + + @Test + public void testGetAccessTokenByRefreshToken() { + OAuth2AuthorizedClientRepository authorizedClientRepository = mock(OAuth2AuthorizedClientRepository.class); + OAuth2AccessToken oAuth2AccessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, + "tokenValue", + Instant.now().minusSeconds(60 * 60), + Instant.now()); + OAuth2RefreshToken oAuth2RefreshToken = new OAuth2RefreshToken( + "fakeTokenValue", + Instant.now(), + Instant.now().plusSeconds(60 * 60)); + OAuth2AuthorizedClient azureAuthorizedClient = new OAuth2AuthorizedClient( + AZURE_CLIENT_REGISTRATION, + "principalName", + oAuth2AccessToken, + oAuth2RefreshToken); + OAuth2AuthorizedClient delegatedAuthorizedClient = new OAuth2AuthorizedClient( + DELEGATED_CLIENT_REGISTRATION, + "principalName", + oAuth2AccessToken); + Authentication principal = mock(Authentication.class); + when(principal.getName()).thenReturn("principalName"); + OAuth2AuthorizationContext context = + OAuth2AuthorizationContext.withAuthorizedClient(delegatedAuthorizedClient) + .principal(principal) + .build(); + when(authorizedClientRepository.loadAuthorizedClient(any(), any(), any())).thenReturn(azureAuthorizedClient); + RefreshTokenOAuth2AuthorizedClientProvider refreshTokenProvider = + mock(RefreshTokenOAuth2AuthorizedClientProvider.class); + OAuth2AuthorizedClient clientGetByRefreshToken = new OAuth2AuthorizedClient( + ClientRegistration.withClientRegistration(DELEGATED_CLIENT_REGISTRATION) + .scope("testScope") + .build(), + "principalName1", + oAuth2AccessToken); + when(refreshTokenProvider.authorize(any())).thenReturn(clientGetByRefreshToken); + AADAzureDelegatedOAuth2AuthorizedClientProvider provider = + new AADAzureDelegatedOAuth2AuthorizedClientProvider(refreshTokenProvider, authorizedClientRepository); + assertEquals(clientGetByRefreshToken, provider.authorize(context)); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADIdTokenRolesExtractionTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADIdTokenRolesExtractionTest.java new file mode 100644 index 0000000000000..322d69267d99e --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADIdTokenRolesExtractionTest.java @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad.AADAuthenticationProperties; +import net.minidev.json.JSONArray; +import org.junit.jupiter.api.Test; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.implementation.constants.AADTokenClaim.ROLES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AADIdTokenRolesExtractionTest { + + private AADOAuth2UserService getUserService() { + AADAuthenticationProperties properties = mock(AADAuthenticationProperties.class); + return new AADOAuth2UserService(properties); + } + + @Test + public void testNoRolesClaim() { + OidcIdToken idToken = mock(OidcIdToken.class); + when(idToken.getClaim(ROLES)).thenReturn(null); + Set authorityStrings = getUserService().extractRolesFromIdToken(idToken); + assertThat(authorityStrings).hasSize(0); + } + + @Test + public void testRolesClaimAsList() { + OidcIdToken idToken = mock(OidcIdToken.class); + JSONArray rolesClaim = new JSONArray().appendElement("Admin"); + when(idToken.getClaim(ROLES)).thenReturn(rolesClaim); + Set authorityStrings = getUserService().extractRolesFromIdToken(idToken); + assertThat(authorityStrings).hasSize(1); + } + + @Test + public void testRolesClaimIllegal() { + OidcIdToken idToken = mock(OidcIdToken.class); + Set rolesClaim = new HashSet<>(Collections.singletonList("Admin")); + when(idToken.getClaim(ROLES)).thenReturn(rolesClaim); + Set authorityStrings = getUserService().extractRolesFromIdToken(idToken); + assertThat(authorityStrings).hasSize(0); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADOAuth2AuthorizationCodeGrantRequestEntityConverterTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADOAuth2AuthorizationCodeGrantRequestEntityConverterTest.java new file mode 100644 index 0000000000000..bcf4ac83f8bf4 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/AADOAuth2AuthorizationCodeGrantRequestEntityConverterTest.java @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADClientRegistrationRepository; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.WebApplicationContextRunnerUtils; +import org.hamcrest.Matcher; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.RequestEntity; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; +import org.springframework.util.MultiValueMap; + +import java.util.Optional; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADClientRegistrationRepository.AZURE_CLIENT_REGISTRATION_ID; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class AADOAuth2AuthorizationCodeGrantRequestEntityConverterTest { + + private WebApplicationContextRunner getContextRunner() { + return WebApplicationContextRunnerUtils + .webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.base-uri = fake-uri", + "azure.activedirectory.authorization-clients.graph.scopes = Graph.Scope", + "azure.activedirectory.authorization-clients.arm.scopes = Arm.Scope", + "azure.activedirectory.authorization-clients.arm.on-demand = true"); + } + + @Test + public void addScopeForAzureClient() { + getContextRunner().run(context -> { + AADClientRegistrationRepository repository = + (AADClientRegistrationRepository) context.getBean(ClientRegistrationRepository.class); + ClientRegistration azure = repository.findByRegistrationId(AZURE_CLIENT_REGISTRATION_ID); + MultiValueMap body = convertedBodyOf(repository, createCodeGrantRequest(azure)); + assertEquals("openid profile offline_access", body.getFirst("scope")); + }); + } + + @Test + public void addScopeForOnDemandClient() { + getContextRunner().run(context -> { + AADClientRegistrationRepository repository = + (AADClientRegistrationRepository) context.getBean(ClientRegistrationRepository.class); + ClientRegistration arm = repository.findByRegistrationId("arm"); + MultiValueMap body = convertedBodyOf(repository, createCodeGrantRequest(arm)); + assertEquals("Arm.Scope openid profile offline_access", body.getFirst("scope")); + }); + } + + @Test + @SuppressWarnings("unchecked") + public void addHeadersForAzureClient() { + getContextRunner().run(context -> { + AADClientRegistrationRepository repository = + (AADClientRegistrationRepository) context.getBean(ClientRegistrationRepository.class); + ClientRegistration azure = repository.findByRegistrationId(AZURE_CLIENT_REGISTRATION_ID); + HttpHeaders httpHeaders = convertedHeaderOf(repository, createCodeGrantRequest(azure)); + assertThat(httpHeaders.entrySet(), (Matcher) hasItems(expectedHeaders(repository))); + }); + } + + @Test + @SuppressWarnings("unchecked") + public void addHeadersForOnDemandClient() { + getContextRunner().run(context -> { + AADClientRegistrationRepository repository = + (AADClientRegistrationRepository) context.getBean(ClientRegistrationRepository.class); + ClientRegistration arm = repository.findByRegistrationId("arm"); + HttpHeaders httpHeaders = convertedHeaderOf(repository, createCodeGrantRequest(arm)); + assertThat(httpHeaders.entrySet(), (Matcher) hasItems(expectedHeaders(repository))); + }); + } + + private HttpHeaders convertedHeaderOf(AADClientRegistrationRepository repository, + OAuth2AuthorizationCodeGrantRequest request) { + AADOAuth2AuthorizationCodeGrantRequestEntityConverter converter = + new AADOAuth2AuthorizationCodeGrantRequestEntityConverter(repository.getAzureClientAccessTokenScopes()); + RequestEntity entity = converter.convert(request); + return Optional.ofNullable(entity) + .map(HttpEntity::getHeaders) + .orElse(null); + } + + private Object[] expectedHeaders(AADClientRegistrationRepository repository) { + return new AADOAuth2AuthorizationCodeGrantRequestEntityConverter(repository.getAzureClientAccessTokenScopes()) + .getHttpHeaders() + .entrySet() + .stream() + .filter(entry -> !entry.getKey().equals("client-request-id")) + .toArray(); + } + + private MultiValueMap convertedBodyOf(AADClientRegistrationRepository repository, + OAuth2AuthorizationCodeGrantRequest request) { + AADOAuth2AuthorizationCodeGrantRequestEntityConverter converter = + new AADOAuth2AuthorizationCodeGrantRequestEntityConverter(repository.getAzureClientAccessTokenScopes()); + RequestEntity entity = converter.convert(request); + return WebApplicationContextRunnerUtils.toMultiValueMap(entity); + } + + private OAuth2AuthorizationCodeGrantRequest createCodeGrantRequest(ClientRegistration client) { + return new OAuth2AuthorizationCodeGrantRequest(client, createExchange(client)); + } + + private OAuth2AuthorizationExchange createExchange(ClientRegistration client) { + return new OAuth2AuthorizationExchange( + createAuthorizationRequest(client), + createAuthorizationResponse()); + } + + private OAuth2AuthorizationRequest createAuthorizationRequest(ClientRegistration client) { + OAuth2AuthorizationRequest.Builder builder = OAuth2AuthorizationRequest.authorizationCode(); + builder.authorizationUri(client.getProviderDetails().getAuthorizationUri()); + builder.clientId(client.getClientId()); + builder.scopes(client.getScopes()); + builder.state("fake-state"); + builder.redirectUri("http://localhost"); + return builder.build(); + } + + private OAuth2AuthorizationResponse createAuthorizationResponse() { + OAuth2AuthorizationResponse.Builder builder = OAuth2AuthorizationResponse.success("fake-code"); + builder.redirectUri("http://localhost"); + builder.state("fake-state"); + return builder.build(); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAppRoleAuthenticationFilterTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAppRoleAuthenticationFilterTest.java new file mode 100644 index 0000000000000..53b00d405e751 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAppRoleAuthenticationFilterTest.java @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader.Builder; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.Payload; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.proc.BadJWTException; +import net.minidev.json.JSONArray; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.text.ParseException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class AADAppRoleAuthenticationFilterTest { + + private static final String TOKEN = "dummy-token"; + + private final UserPrincipalManager userPrincipalManager; + private final HttpServletRequest request; + private final HttpServletResponse response; + private final SimpleGrantedAuthority roleAdmin; + private final SimpleGrantedAuthority roleUser; + private final AADAppRoleStatelessAuthenticationFilter filter; + + private UserPrincipal createUserPrincipal(Set roles) { + final JSONArray claims = new JSONArray(); + claims.addAll(roles); + final JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder() + .subject("john doe") + .claim("roles", claims) + .build(); + final JWSObject jwsObject = new JWSObject( + new Builder(JWSAlgorithm.RS256).build(), + new Payload(jwtClaimsSet.toString()) + ); + UserPrincipal userPrincipal = new UserPrincipal("", jwsObject, jwtClaimsSet); + userPrincipal.setRoles(roles); + return userPrincipal; + } + + public AADAppRoleAuthenticationFilterTest() { + userPrincipalManager = mock(UserPrincipalManager.class); + request = mock(HttpServletRequest.class); + response = mock(HttpServletResponse.class); + roleAdmin = new SimpleGrantedAuthority("ROLE_admin"); + roleUser = new SimpleGrantedAuthority("ROLE_user"); + filter = new AADAppRoleStatelessAuthenticationFilter(userPrincipalManager); + } + + @Test + public void testDoFilterGoodCase() + throws ParseException, JOSEException, BadJOSEException, ServletException, IOException { + Set dummyValues = new HashSet<>(2); + dummyValues.add("user"); + dummyValues.add("admin"); + final UserPrincipal dummyPrincipal = createUserPrincipal(Collections.unmodifiableSet(dummyValues)); + + when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer " + TOKEN); + when(userPrincipalManager.buildUserPrincipal(TOKEN)).thenReturn(dummyPrincipal); + when(userPrincipalManager.isTokenIssuedByAAD(TOKEN)).thenReturn(true); + + // Check in subsequent filter that authentication is available! + final FilterChain filterChain = (request, response) -> { + final SecurityContext context = SecurityContextHolder.getContext(); + assertNotNull(context); + final Authentication authentication = context.getAuthentication(); + assertNotNull(authentication); + assertTrue(authentication.isAuthenticated(), "User should be authenticated!"); + assertEquals(dummyPrincipal, authentication.getPrincipal()); + + @SuppressWarnings("unchecked") final Collection authorities = + (Collection) authentication + .getAuthorities(); + Assertions.assertThat(authorities).containsExactlyInAnyOrder(roleAdmin, roleUser); + }; + + filter.doFilterInternal(request, response, filterChain); + + verify(userPrincipalManager).buildUserPrincipal(TOKEN); + assertNull(SecurityContextHolder.getContext().getAuthentication(), "Authentication has not been cleaned up!"); + } + + @Test + public void testBadJWTExceptionReturn401() + throws ParseException, JOSEException, BadJOSEException, ServletException, IOException { + + when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer " + TOKEN); + when(userPrincipalManager.buildUserPrincipal(any())).thenThrow(new BadJWTException("bad token")); + when(userPrincipalManager.isTokenIssuedByAAD(any())).thenReturn(true); + MockHttpServletResponse mockHttpServletResponse = new MockHttpServletResponse(); + filter.doFilterInternal(request, mockHttpServletResponse, mock(FilterChain.class)); + assertEquals(HttpStatus.UNAUTHORIZED.value(), mockHttpServletResponse.getStatus()); + } + + @Test + public void testDoFilterAddsDefaultRole() + throws ParseException, JOSEException, BadJOSEException, ServletException, IOException { + + final UserPrincipal dummyPrincipal = createUserPrincipal(Collections.emptySet()); + + when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer " + TOKEN); + when(userPrincipalManager.buildUserPrincipal(TOKEN)).thenReturn(dummyPrincipal); + when(userPrincipalManager.isTokenIssuedByAAD(TOKEN)).thenReturn(true); + + // Check in subsequent filter that authentication is available and default roles are filled. + final FilterChain filterChain = (request, response) -> { + final SecurityContext context = SecurityContextHolder.getContext(); + assertNotNull(context); + final Authentication authentication = context.getAuthentication(); + assertNotNull(authentication); + assertTrue(authentication.isAuthenticated(), "User should be authenticated!"); + final SimpleGrantedAuthority expectedDefaultRole = new SimpleGrantedAuthority("ROLE_USER"); + + @SuppressWarnings("unchecked") final Collection authorities = + (Collection) authentication + .getAuthorities(); + Assertions.assertThat(authorities).containsExactlyInAnyOrder(expectedDefaultRole); + }; + + filter.doFilterInternal(request, response, filterChain); + + verify(userPrincipalManager).buildUserPrincipal(TOKEN); + assertNull(SecurityContextHolder.getContext().getAuthentication(), "Authentication has not been cleaned up!"); + } + + @Test + public void testToSimpleGrantedAuthoritySetWithWhitespaceRole() { + AADAppRoleStatelessAuthenticationFilter filter = new AADAppRoleStatelessAuthenticationFilter(null); + UserPrincipal userPrincipal = new UserPrincipal(null, null, null); + Set roles = new HashSet<>(3); + roles.add("user"); + roles.add(""); + roles.add("ADMIN"); + userPrincipal.setRoles(Collections.unmodifiableSet(roles)); + Set result = filter.toSimpleGrantedAuthoritySet(userPrincipal); + assertThat( + "Set should contain the two granted authority 'ROLE_user' and 'ROLE_ADMIN'.", + result, + containsInAnyOrder( + new SimpleGrantedAuthority("ROLE_user"), + new SimpleGrantedAuthority("ROLE_ADMIN") + ) + ); + } + + @Test + public void testToSimpleGrantedAuthoritySetWithNoRole() { + AADAppRoleStatelessAuthenticationFilter filter = new AADAppRoleStatelessAuthenticationFilter(null); + UserPrincipal userPrincipal = new UserPrincipal(null, null, null); + Set roles = Collections.unmodifiableSet(new HashSet<>()); + userPrincipal.setRoles(roles); + Set result = filter.toSimpleGrantedAuthoritySet(userPrincipal); + assertThat( + "Set should contain the default authority 'ROLE_USER'.", + result, + containsInAnyOrder( + new SimpleGrantedAuthority("ROLE_USER") + ) + ); + } + + @Test + public void testTokenNotIssuedByAAD() throws ServletException, IOException { + when(userPrincipalManager.isTokenIssuedByAAD(TOKEN)).thenReturn(false); + + final FilterChain filterChain = (request, response) -> { + final SecurityContext context = SecurityContextHolder.getContext(); + assertNotNull(context); + final Authentication authentication = context.getAuthentication(); + assertNull(authentication); + }; + + filter.doFilterInternal(request, response, filterChain); + } + + @Test + public void testAlreadyAuthenticated() throws ServletException, IOException, ParseException, JOSEException, + BadJOSEException { + final Authentication authentication = mock(Authentication.class); + when(authentication.isAuthenticated()).thenReturn(true); + when(userPrincipalManager.isTokenIssuedByAAD(TOKEN)).thenReturn(true); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + final FilterChain filterChain = (request, response) -> { + final SecurityContext context = SecurityContextHolder.getContext(); + assertNotNull(context); + assertNotNull(context.getAuthentication()); + SecurityContextHolder.clearContext(); + }; + + filter.doFilterInternal(request, response, filterChain); + verify(userPrincipalManager, times(0)).buildUserPrincipal(TOKEN); + + } + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationFilterPropertiesTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationFilterPropertiesTest.java new file mode 100644 index 0000000000000..9ab6c49f5ecae --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationFilterPropertiesTest.java @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.boot.context.properties.ConfigurationPropertiesBindException; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.bind.validation.BindValidationException; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.ObjectError; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.context.support.TestPropertySourceUtils.addInlinedPropertiesToEnvironment; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class AADAuthenticationFilterPropertiesTest { + + private static final String AAD_PROPERTY_PREFIX = "azure.activedirectory."; + + @Test + public void canSetProperties() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + configureAllRequiredProperties(context); + context.register(Config.class); + context.refresh(); + + final AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class); + + assertThat(properties.getClientId()).isEqualTo(TestConstants.CLIENT_ID); + assertThat(properties.getClientSecret()).isEqualTo(TestConstants.CLIENT_SECRET); + assertThat(properties.getActiveDirectoryGroups() + .toString()).isEqualTo(TestConstants.TARGETED_GROUPS.toString()); + } + } + + private void configureAllRequiredProperties(AnnotationConfigApplicationContext context) { + addInlinedPropertiesToEnvironment( + context, + AAD_PROPERTY_PREFIX + "tenant-id=demo-tenant-id", + AAD_PROPERTY_PREFIX + "client-id=" + TestConstants.CLIENT_ID, + AAD_PROPERTY_PREFIX + "client-secret=" + TestConstants.CLIENT_SECRET, + AAD_PROPERTY_PREFIX + "user-group.allowed-groups=" + + TestConstants.TARGETED_GROUPS.toString().replace("[", "").replace("]", "") + ); + } + + @Disabled + @Test + //TODO (wepa) clientId and clientSecret can also be configured in oauth2 config, test to be refactored + public void emptySettingsNotAllowed() { + System.setProperty("azure.activedirectory.client-id", ""); + System.setProperty("azure.activedirectory.client-secret", ""); + + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + Exception exception = null; + + context.register(Config.class); + + try { + context.refresh(); + } catch (Exception e) { + exception = e; + } + + assertThat(exception).isNotNull(); + assertThat(exception).isExactlyInstanceOf(ConfigurationPropertiesBindException.class); + + final BindValidationException bindException = (BindValidationException) exception.getCause().getCause(); + final List errors = bindException.getValidationErrors().getAllErrors(); + + final List errorStrings = errors.stream().map(ObjectError::toString).collect(Collectors.toList()); + + final List errorStringsExpected = Arrays.asList( + "Field error in object 'azure.activedirectory' on field 'activeDirectoryGroups': " + + "rejected value [null];", + "Field error in object 'azure.activedirectory' on field 'clientId': rejected value [];", + "Field error in object 'azure.activedirectory' on field 'clientSecret': rejected value [];" + ); + + Collections.sort(errorStrings); + + assertThat(errors.size()).isEqualTo(errorStringsExpected.size()); + + for (int i = 0; i < errorStrings.size(); i++) { + assertThat(errorStrings.get(i)).contains(errorStringsExpected.get(i)); + } + } + } + + @Configuration + @EnableConfigurationProperties(AADAuthenticationProperties.class) + static class Config { + } +} + diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationFilterTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationFilterTest.java new file mode 100644 index 0000000000000..7ea0bcfa29488 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationFilterTest.java @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationServerEndpoints; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.proc.BadJOSEException; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.text.ParseException; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class AADAuthenticationFilterTest { + private static final String TOKEN = "dummy-token"; + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AADAuthenticationFilterAutoConfiguration.class)); + private final UserPrincipalManager userPrincipalManager; + private final HttpServletRequest request; + private final HttpServletResponse response; + private final AADAuthenticationFilter filter; + + public AADAuthenticationFilterTest() { + userPrincipalManager = mock(UserPrincipalManager.class); + request = mock(HttpServletRequest.class); + response = mock(HttpServletResponse.class); + filter = new AADAuthenticationFilter( + mock(AADAuthenticationProperties.class), + mock(AADAuthorizationServerEndpoints.class), + userPrincipalManager + ); + } + + //TODO (Zhou Liu): current test case is out of date, a new test case need to cover here, do it later. + @Test + @Disabled + public void doFilterInternal() { + this.contextRunner.withPropertyValues("azure.activedirectory.client-id", TestConstants.CLIENT_ID) + .withPropertyValues("azure.activedirectory.client-secret", TestConstants.CLIENT_SECRET) + .withPropertyValues("azure.activedirectory.client-secret", + TestConstants.TARGETED_GROUPS.toString() + .replace("[", "").replace("]", "")); + + this.contextRunner.run(context -> { + final HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader(TestConstants.TOKEN_HEADER)).thenReturn(TestConstants.BEARER_TOKEN); + + final HttpServletResponse response = mock(HttpServletResponse.class); + final FilterChain filterChain = mock(FilterChain.class); + + + final AADAuthenticationFilter azureADJwtTokenFilter = context.getBean(AADAuthenticationFilter.class); + assertThat(azureADJwtTokenFilter).isNotNull(); + assertThat(azureADJwtTokenFilter).isExactlyInstanceOf(AADAuthenticationFilter.class); + + azureADJwtTokenFilter.doFilterInternal(request, response, filterChain); + + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + assertThat(authentication.getPrincipal()).isNotNull(); + assertThat(authentication.getPrincipal()).isExactlyInstanceOf(UserPrincipal.class); + assertThat(authentication.getAuthorities()).isNotNull(); + assertThat(authentication.getAuthorities().size()).isEqualTo(2); + assertThat(authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_group1")) + && authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_group2")) + ).isTrue(); + + final UserPrincipal principal = (UserPrincipal) authentication.getPrincipal(); + assertThat(principal.getIssuer()).isNotNull().isNotEmpty(); + assertThat(principal.getKid()).isNotNull().isNotEmpty(); + assertThat(principal.getSubject()).isNotNull().isNotEmpty(); + + assertThat(principal.getClaims()).isNotNull().isNotEmpty(); + final Map claims = principal.getClaims(); + assertThat(claims.get("iss")).isEqualTo(principal.getIssuer()); + assertThat(claims.get("sub")).isEqualTo(principal.getSubject()); + }); + } + + @Test + public void testTokenNotIssuedByAAD() throws ServletException, IOException { + when(userPrincipalManager.isTokenIssuedByAAD(TOKEN)).thenReturn(false); + + final FilterChain filterChain = (request, response) -> { + final SecurityContext context = SecurityContextHolder.getContext(); + assertNotNull(context); + final Authentication authentication = context.getAuthentication(); + assertNull(authentication); + }; + + filter.doFilterInternal(request, response, filterChain); + } + + @Test + public void testAlreadyAuthenticated() throws ServletException, IOException, ParseException, JOSEException, + BadJOSEException { + final Authentication authentication = mock(Authentication.class); + when(authentication.isAuthenticated()).thenReturn(true); + when(userPrincipalManager.isTokenIssuedByAAD(TOKEN)).thenReturn(true); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + final FilterChain filterChain = (request, response) -> { + final SecurityContext context = SecurityContextHolder.getContext(); + assertNotNull(context); + assertNotNull(context.getAuthentication()); + SecurityContextHolder.clearContext(); + }; + + filter.doFilterInternal(request, response, filterChain); + verify(userPrincipalManager, times(0)).buildUserPrincipal(TOKEN); + } + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationPropertiesTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationPropertiesTest.java new file mode 100644 index 0000000000000..29281be0584d1 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AADAuthenticationPropertiesTest.java @@ -0,0 +1,290 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADApplicationType; +import org.junit.jupiter.api.Test; + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.WebApplicationContextRunnerUtils.resourceServerContextRunner; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.WebApplicationContextRunnerUtils.resourceServerWithOboContextRunner; +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.WebApplicationContextRunnerUtils.webApplicationContextRunner; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class AADAuthenticationPropertiesTest { + + @Test + public void webAppWithOboWithExceptionTest() { + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.authorization-clients.graph.authorizationGrantType = on_behalf_of") + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AADAuthenticationProperties.class))); + } + + @Test + public void graphUriConfigurationTest() { + webApplicationContextRunner() + .run(context -> { + AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class); + assertEquals(properties.getGraphBaseUri(), "https://graph.microsoft.com/"); + assertEquals(properties.getGraphMembershipUri(), "https://graph.microsoft.com/v1.0/me/memberOf"); + }); + + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.graph-base-uri=https://microsoftgraph.chinacloudapi.cn" + ) + .run(context -> { + AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class); + assertEquals(properties.getGraphBaseUri(), "https://microsoftgraph.chinacloudapi.cn/"); + assertEquals(properties.getGraphMembershipUri(), + "https://microsoftgraph.chinacloudapi.cn/v1.0/me/memberOf"); + }); + + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.graph-base-uri=https://microsoftgraph.chinacloudapi.cn/" + ) + .run(context -> { + AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class); + assertEquals(properties.getGraphBaseUri(), "https://microsoftgraph.chinacloudapi.cn/"); + assertEquals(properties.getGraphMembershipUri(), + "https://microsoftgraph.chinacloudapi.cn/v1.0/me/memberOf"); + }); + + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.graph-membership-uri=https://graph.microsoft.com/v1.0/me/memberOf" + ) + .run(context -> { + AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class); + assertEquals(properties.getGraphBaseUri(), "https://graph.microsoft.com/"); + assertEquals(properties.getGraphMembershipUri(), "https://graph.microsoft.com/v1.0/me/memberOf"); + }); + + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.graph-base-uri=https://microsoftgraph.chinacloudapi.cn/", + "azure.activedirectory.graph-membership-uri=https://microsoftgraph.chinacloudapi.cn/v1.0/me/memberOf" + ) + .run(context -> { + AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class); + assertEquals(properties.getGraphBaseUri(), "https://microsoftgraph.chinacloudapi.cn/"); + assertEquals(properties.getGraphMembershipUri(), + "https://microsoftgraph.chinacloudapi.cn/v1.0/me/memberOf"); + }); + } + + @Test + public void graphUriConfigurationWithExceptionTest() { + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.graph-membership-uri=https://microsoftgraph.chinacloudapi.cn/v1.0/me/memberOf" + ) + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AADAuthenticationProperties.class)) + ); + } + + @Test + public void multiTenantWithAllowedGroupsConfiguredTest1() { + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.tenant-id=", + "azure.activedirectory.user-group.allowed-groups=group1,group2" + ) + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AADAuthenticationProperties.class)) + ); + } + + @Test + public void multiTenantWithAllowedGroupsConfiguredTest2() { + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.tenant-id=common", + "azure.activedirectory.user-group.allowed-groups=group1,group2" + ) + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AADAuthenticationProperties.class)) + ); + } + + @Test + public void multiTenantWithAllowedGroupsConfiguredTest3() { + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.tenant-id=organizations", + "azure.activedirectory.user-group.allowed-groups=group1,group2" + ) + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AADAuthenticationProperties.class)) + ); + } + + @Test + public void multiTenantWithAllowedGroupsIdConfiguredTest1() { + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.tenant-id=", + "azure.activedirectory.user-group.allowed-group-ids = 7c3a5d22-9093-42d7-b2eb-e72d06bf3718," + + "39087533-2593-4b5b-ad05-4a73a01ea6a9" + ) + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AADAuthenticationProperties.class)) + ); + } + + @Test + public void multiTenantWithAllowedGroupsIdConfiguredTest2() { + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.tenant-id=common", + "azure.activedirectory.user-group.allowed-group-ids = 7c3a5d22-9093-42d7-b2eb-e72d06bf3718," + + "39087533-2593-4b5b-ad05-4a73a01ea6a9" + ) + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AADAuthenticationProperties.class)) + ); + } + + @Test + public void multiTenantWithAllowedGroupsIdConfiguredTest3() { + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.tenant-id=organizations", + "azure.activedirectory.user-group.allowed-group-ids = 7c3a5d22-9093-42d7-b2eb-e72d06bf3718," + + "39087533-2593-4b5b-ad05-4a73a01ea6a9" + ) + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AADAuthenticationProperties.class)) + ); + } + + @Test + public void multiTenantWithAllowedGroupsIdConfiguredTest4() { + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.tenant-id=consumers", + "azure.activedirectory.user-group.allowed-group-ids = 7c3a5d22-9093-42d7-b2eb-e72d06bf3718," + + "39087533-2593-4b5b-ad05-4a73a01ea6a9" + ) + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AADAuthenticationProperties.class)) + ); + } + + @Test + public void multiTenantWithAllowedGroupsConfiguredTest4() { + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.tenant-id=consumers", + "azure.activedirectory.user-group.allowed-groups=group1,group2" + ) + .run(context -> + assertThrows(IllegalStateException.class, () -> context.getBean(AADAuthenticationProperties.class)) + ); + } + + @Test + public void applicationTypeOfWebApplication() { + webApplicationContextRunner() + .run(context -> { + AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class); + assertEquals(properties.getApplicationType(), AADApplicationType.WEB_APPLICATION); + }); + + webApplicationContextRunner() + .withPropertyValues("azure.activedirectory.application-type=web_application") + .run(context -> { + AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class); + assertEquals(properties.getApplicationType(), AADApplicationType.WEB_APPLICATION); + }); + + resourceServerWithOboContextRunner() + .withPropertyValues("azure.activedirectory.application-type=web_application") + .run(context -> { + AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class); + assertEquals(properties.getApplicationType(), AADApplicationType.WEB_APPLICATION); + }); + } + + @Test + public void applicationTypeWithResourceServer() { + resourceServerContextRunner() + .run(context -> { + AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class); + assertEquals(properties.getApplicationType(), AADApplicationType.RESOURCE_SERVER); + }); + + resourceServerContextRunner() + .withPropertyValues("azure.activedirectory.application-type=resource_server") + .run(context -> { + AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class); + assertEquals(properties.getApplicationType(), AADApplicationType.RESOURCE_SERVER); + }); + + resourceServerWithOboContextRunner() + .withPropertyValues("azure.activedirectory.application-type=resource_server") + .run(context -> { + AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class); + assertEquals(properties.getApplicationType(), AADApplicationType.RESOURCE_SERVER); + }); + } + + @Test + public void applicationTypeOfResourceServerWithOBO() { + resourceServerWithOboContextRunner() + .run(context -> { + AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class); + assertEquals(properties.getApplicationType(), AADApplicationType.RESOURCE_SERVER_WITH_OBO); + }); + + resourceServerWithOboContextRunner() + .withPropertyValues("azure.activedirectory.application-type=resource_server_with_obo") + .run(context -> { + AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class); + assertEquals(properties.getApplicationType(), AADApplicationType.RESOURCE_SERVER_WITH_OBO); + }); + } + + @Test + public void applicationTypeWithWebApplicationAndResourceServer() { + resourceServerWithOboContextRunner() + .withPropertyValues("azure.activedirectory.application-type=web_application_and_resource_server") + .run(context -> { + AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class); + assertEquals(properties.getApplicationType(), AADApplicationType.WEB_APPLICATION_AND_RESOURCE_SERVER); + }); + } + + @Test + public void testInvalidApplicationType() { + resourceServerContextRunner() + .withPropertyValues("azure.activedirectory.application-type=web_application") + .run(context -> { + assertThrows(IllegalStateException.class, () -> context.getBean(AADAuthenticationProperties.class)); + }); + + webApplicationContextRunner() + .withPropertyValues("azure.activedirectory.application-type=resource_server") + .run(context -> { + assertThrows(IllegalStateException.class, () -> context.getBean(AADAuthenticationProperties.class)); + }); + } + + @Test + public void invalidAuthorizationCodeWhenOnDemandIsFalse() { + webApplicationContextRunner() + .withPropertyValues( + "azure.activedirectory.authorization-clients.graph.scopes = Graph.Scope", + "azure.activedirectory.authorization-clients.graph.on-demand = true", + "azure.activedirectory.authorization-clients.graph.authorizationGrantType = azure_delegated" + ) + .run(context -> { + assertThrows(IllegalStateException.class, () -> context.getBean(AADAuthenticationProperties.class)); + }); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AzureADGraphClientTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AzureADGraphClientTest.java new file mode 100644 index 0000000000000..668fe6d0769e4 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/AzureADGraphClientTest.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationServerEndpoints; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AzureADGraphClientTest { + + private AzureADGraphClient client; + + @Mock + private AADAuthorizationServerEndpoints endpoints; + + @BeforeEach + public void setup() { + final List activeDirectoryGroups = new ArrayList<>(); + activeDirectoryGroups.add("Test_Group"); + AADAuthenticationProperties aadAuthenticationProperties = new AADAuthenticationProperties(); + aadAuthenticationProperties.getUserGroup().setAllowedGroups(activeDirectoryGroups); + client = new AzureADGraphClient("client", "pass", aadAuthenticationProperties, endpoints); + } + + @Test + public void testConvertGroupToGrantedAuthorities() { + final Set groups = new HashSet<>(1); + groups.add("Test_Group"); + final Set authorities = client.toGrantedAuthoritySet( + Collections.unmodifiableSet(groups)); + assertThat(authorities) + .hasSize(1) + .extracting(GrantedAuthority::getAuthority) + .containsExactly("ROLE_Test_Group"); + } + + @Test + public void testConvertGroupToGrantedAuthoritiesUsingAllowedGroups() { + final Set groups = new HashSet<>(2); + groups.add("Test_Group"); + groups.add("Another_Group"); + final Set authorities = client.toGrantedAuthoritySet( + Collections.unmodifiableSet(groups)); + assertThat(authorities) + .hasSize(1) + .extracting(GrantedAuthority::getAuthority) + .containsExactly("ROLE_Test_Group"); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/MembershipTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/MembershipTest.java new file mode 100644 index 0000000000000..e6d61c2533f97 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/MembershipTest.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class MembershipTest { + private static final Membership GROUP_1 = new Membership("12345", Membership.OBJECT_TYPE_GROUP, "test"); + + @Test + public void getDisplayName() { + Assertions.assertEquals("test", GROUP_1.getDisplayName()); + } + + @Test + public void getObjectType() { + Assertions.assertEquals(Membership.OBJECT_TYPE_GROUP, GROUP_1.getObjectType()); + } + + @Test + public void getObjectID() { + Assertions.assertEquals("12345", GROUP_1.getObjectID()); + } + + @Test + public void equals() { + final Membership group2 = new Membership("12345", Membership.OBJECT_TYPE_GROUP, "test"); + Assertions.assertEquals(GROUP_1, group2); + } + + @Test + public void hashCodeTest() { + final Membership group2 = new Membership("12345", Membership.OBJECT_TYPE_GROUP, "test"); + Assertions.assertEquals(GROUP_1.hashCode(), group2.hashCode()); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/MicrosoftGraphConstants.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/MicrosoftGraphConstants.java new file mode 100644 index 0000000000000..a2c2b268a4226 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/MicrosoftGraphConstants.java @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +public class MicrosoftGraphConstants { + + public static final String BEARER_TOKEN = "Bearer real_jtw_bearer_token"; + public static final String CLIENT_ID = "real_client_id"; + /** + * Token from https://docs.microsoft.com/azure/active-directory/develop/v2-id-and-access-tokens + */ + public static final String JWT_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1uQ19WWmNBVGZNNXBPWWlKSE1" + + "iYTlnb0VLWSJ9.eyJhdWQiOiI2NzMxZGU3Ni0xNGE2LTQ5YWUtOTdiYy02ZWJhNjkxNDM5MWUiLCJpc3MiOiJodHRwczovL2xvZ2lu" + + "Lm1pY3Jvc29mdG9ubGluZS5jb20vYjk0MTk4MTgtMDlhZi00OWMyLWIwYzMtNjUzYWRjMWYzNzZlL3YyLjAiLCJpYXQiOjE0NTIyOD" + + "UzMzEsIm5iZiI6MTQ1MjI4NTMzMSwiZXhwIjoxNDUyMjg5MjMxLCJuYW1lIjoiQmFiZSBSdXRoIiwibm9uY2UiOiIxMjM0NSIsIm9p" + + "ZCI6ImExZGJkZGU4LWU0ZjktNDU3MS1hZDkzLTMwNTllMzc1MGQyMyIsInByZWZlcnJlZF91c2VybmFtZSI6InRoZWdyZWF0YmFtYm" + + "lub0BueXkub25taWNyb3NvZnQuY29tIiwic3ViIjoiTUY0Zi1nZ1dNRWppMTJLeW5KVU5RWnBoYVVUdkxjUXVnNWpkRjJubDAxUSIs" + + "InRpZCI6ImI5NDE5ODE4LTA5YWYtNDljMi1iMGMzLTY1M2FkYzFmMzc2ZSIsInZlciI6IjIuMCJ9.p_rYdrtJ1oCmgDBggNHB9O38K" + + "TnLCMGbMDODdirdmZbmJcTHiZDdtTc-hguu3krhbtOsoYM2HJeZM3Wsbp_YcfSKDY--X_NobMNsxbT7bqZHxDnA2jTMyrmt5v2EKUn" + + "EeVtSiJXyO3JWUq9R0dO-m4o9_8jGP6zHtR62zLaotTBYHmgeKpZgTFB9WtUq8DVdyMn_HSvQEfz-LWqckbcTwM_9RNKoGRVk38KCh" + + "VJo4z5LkksYRarDo8QgQ7xEKmYmPvRr_I7gvM2bmlZQds2OeqWLB1NSNbFZqyFOCgYn3bAQ-nEQSKwBaA36jYGPOVG2r2Qv1uKcpSO" + + "xzxaQybzYpQ"; +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/ResourceRetrieverTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/ResourceRetrieverTest.java new file mode 100644 index 0000000000000..70e80be20e4b7 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/ResourceRetrieverTest.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import com.nimbusds.jose.util.DefaultResourceRetriever; +import com.nimbusds.jose.util.ResourceRetriever; +import org.junit.jupiter.api.Test; +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.server.resource.BearerTokenAuthenticationToken; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ResourceRetrieverTest { + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AADAuthenticationFilterAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class)) + .withPropertyValues( + "azure.activedirectory.client-id=fake-client-id", + "azure.activedirectory.client-secret=fake-client-secret"); + + @Test + public void resourceRetrieverDefaultConfig() { + this.contextRunner.run(context -> { + assertThat(context).hasSingleBean(ResourceRetriever.class); + final ResourceRetriever retriever = context.getBean(ResourceRetriever.class); + assertThat(retriever).isInstanceOf(DefaultResourceRetriever.class); + + final DefaultResourceRetriever defaultRetriever = (DefaultResourceRetriever) retriever; + assertThat(defaultRetriever.getConnectTimeout()).isEqualTo(RemoteJWKSet.DEFAULT_HTTP_CONNECT_TIMEOUT); + assertThat(defaultRetriever.getReadTimeout()).isEqualTo(RemoteJWKSet.DEFAULT_HTTP_READ_TIMEOUT); + assertThat(defaultRetriever.getSizeLimit()).isEqualTo(RemoteJWKSet.DEFAULT_HTTP_SIZE_LIMIT); + }); + } + + @Test + public void resourceRetriverIsConfigurable() { + this.contextRunner + .withPropertyValues( + "azure.activedirectory.jwt-connect-timeout=1234", + "azure.activedirectory.jwt-read-timeout=1234", + "azure.activedirectory.jwt-size-limit=123400") + .run(context -> { + assertThat(context).hasSingleBean(ResourceRetriever.class); + final ResourceRetriever retriever = context.getBean(ResourceRetriever.class); + assertThat(retriever).isInstanceOf(DefaultResourceRetriever.class); + + final DefaultResourceRetriever defaultRetriever = (DefaultResourceRetriever) retriever; + assertThat(defaultRetriever.getConnectTimeout()).isEqualTo(1234); + assertThat(defaultRetriever.getReadTimeout()).isEqualTo(1234); + assertThat(defaultRetriever.getSizeLimit()).isEqualTo(123400); + }); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/TestConstants.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/TestConstants.java new file mode 100644 index 0000000000000..0a4ef348b6b17 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/TestConstants.java @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import java.util.Arrays; +import java.util.List; + +public class TestConstants { + + public static final String CLIENT_ID = "real_client_id"; + public static final String CLIENT_SECRET = "real_client_secret"; + public static final List TARGETED_GROUPS = Arrays.asList("group1", "group2", "group3"); + + public static final String TOKEN_HEADER = "Authorization"; + public static final String BEARER_TOKEN = "Bearer real_jwt_bearer_token"; + + /** Token from https://docs.microsoft.com/azure/active-directory/develop/v2-id-and-access-tokens */ + public static final String JWT_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1uQ19WWmNBVGZNNXBPWWlKSE1" + + "iYTlnb0VLWSJ9.eyJhdWQiOiI2NzMxZGU3Ni0xNGE2LTQ5YWUtOTdiYy02ZWJhNjkxNDM5MWUiLCJpc3MiOiJodHRwczovL2xvZ2lu" + + "Lm1pY3Jvc29mdG9ubGluZS5jb20vYjk0MTk4MTgtMDlhZi00OWMyLWIwYzMtNjUzYWRjMWYzNzZlL3YyLjAiLCJpYXQiOjE0NTIyOD" + + "UzMzEsIm5iZiI6MTQ1MjI4NTMzMSwiZXhwIjoxNDUyMjg5MjMxLCJuYW1lIjoiQmFiZSBSdXRoIiwibm9uY2UiOiIxMjM0NSIsIm9p" + + "ZCI6ImExZGJkZGU4LWU0ZjktNDU3MS1hZDkzLTMwNTllMzc1MGQyMyIsInByZWZlcnJlZF91c2VybmFtZSI6InRoZWdyZWF0YmFtYm" + + "lub0BueXkub25taWNyb3NvZnQuY29tIiwic3ViIjoiTUY0Zi1nZ1dNRWppMTJLeW5KVU5RWnBoYVVUdkxjUXVnNWpkRjJubDAxUSIs" + + "InRpZCI6ImI5NDE5ODE4LTA5YWYtNDljMi1iMGMzLTY1M2FkYzFmMzc2ZSIsInZlciI6IjIuMCJ9.p_rYdrtJ1oCmgDBggNHB9O38K" + + "TnLCMGbMDODdirdmZbmJcTHiZDdtTc-hguu3krhbtOsoYM2HJeZM3Wsbp_YcfSKDY--X_NobMNsxbT7bqZHxDnA2jTMyrmt5v2EKUn" + + "EeVtSiJXyO3JWUq9R0dO-m4o9_8jGP6zHtR62zLaotTBYHmgeKpZgTFB9WtUq8DVdyMn_HSvQEfz-LWqckbcTwM_9RNKoGRVk38KCh" + + "VJo4z5LkksYRarDo8QgQ7xEKmYmPvRr_I7gvM2bmlZQds2OeqWLB1NSNbFZqyFOCgYn3bAQ-nEQSKwBaA36jYGPOVG2r2Qv1uKcpSO" + + "xzxaQybzYpQ"; +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipalAzureADGraphTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipalAzureADGraphTest.java new file mode 100644 index 0000000000000..f8596c2f5ce81 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipalAzureADGraphTest.java @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jwt.JWTClaimsSet; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.util.StringUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.nio.file.Files; +import java.text.ParseException; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +public class UserPrincipalAzureADGraphTest { + + private WireMockRule wireMockRule; + + @BeforeEach + void setup() { + wireMockRule = new WireMockRule(9519); + wireMockRule.start(); + } + + @AfterEach + void close() { + if (wireMockRule.isRunning()) { + wireMockRule.stop(); + } + } + + @Test + public void userPrincipalIsSerializable() throws ParseException, IOException, ClassNotFoundException { + final File tmpOutputFile = File.createTempFile("test-user-principal", "txt"); + + try (FileOutputStream fileOutputStream = new FileOutputStream(tmpOutputFile); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); + FileInputStream fileInputStream = new FileInputStream(tmpOutputFile); + ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) { + + final JWSObject jwsObject = JWSObject.parse(TestConstants.JWT_TOKEN); + final JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().subject("fake-subject").build(); + final UserPrincipal principal = new UserPrincipal("", jwsObject, jwtClaimsSet); + + objectOutputStream.writeObject(principal); + + final UserPrincipal serializedPrincipal = (UserPrincipal) objectInputStream.readObject(); + + assertNotNull(serializedPrincipal, "Serialized UserPrincipal not null"); + assertTrue(StringUtils.hasText(serializedPrincipal.getKid()), "Serialized UserPrincipal kid not empty"); + assertNotNull(serializedPrincipal.getClaims(), "Serialized UserPrincipal claims not null."); + assertTrue(serializedPrincipal.getClaims().size() > 0, "Serialized UserPrincipal claims not empty."); + } finally { + Files.deleteIfExists(tmpOutputFile.toPath()); + } + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipalManagerAudienceTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipalManagerAudienceTest.java new file mode 100644 index 0000000000000..086d24913751e --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipalManagerAudienceTest.java @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationServerEndpoints; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.util.Resource; +import com.nimbusds.jose.util.ResourceRetriever; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.time.Instant; +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class UserPrincipalManagerAudienceTest { + + private static final String FAKE_CLIENT_ID = "dsflkjsdflkjsdf"; + private static final String FAKE_APPLICATION_URI = "https://oihiugjuzfvbhg"; + + private JWSSigner signer; + private String jwkString; + private ResourceRetriever resourceRetriever; + + private AADAuthorizationServerEndpoints endpoints; + private AADAuthenticationProperties properties; + private UserPrincipalManager userPrincipalManager; + + @BeforeEach + public void setupKeys() throws NoSuchAlgorithmException { + final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + + final KeyPair kp = kpg.genKeyPair(); + final RSAPrivateKey privateKey = (RSAPrivateKey) kp.getPrivate(); + + signer = new RSASSASigner(privateKey); + + final RSAKey rsaJWK = new RSAKey.Builder((RSAPublicKey) kp.getPublic()) + .privateKey((RSAPrivateKey) kp.getPrivate()) + .keyID("1") + .build(); + final JWKSet jwkSet = new JWKSet(rsaJWK); + jwkString = jwkSet.toString(); + + resourceRetriever = url -> new Resource(jwkString, "application/json"); + + endpoints = mock(AADAuthorizationServerEndpoints.class); + properties = new AADAuthenticationProperties(); + properties.setClientId(FAKE_CLIENT_ID); + properties.setAppIdUri(FAKE_APPLICATION_URI); + when(endpoints.jwkSetEndpoint()).thenReturn("file://dummy"); + } + + @Test + public void allowApplicationUriAsAudience() throws JOSEException { + final JWTClaimsSet claimsSetOne = new JWTClaimsSet.Builder() + .subject("foo") + .issueTime(Date.from(Instant.now().minusSeconds(60))) + .issuer("https://sts.windows.net/") + .audience(FAKE_CLIENT_ID) + .build(); + final SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSetOne); + signedJWT.sign(signer); + + final String orderTwo = signedJWT.serialize(); + userPrincipalManager = new UserPrincipalManager(endpoints, properties, + resourceRetriever, true); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal(orderTwo)) + .doesNotThrowAnyException(); + } + + @Test + public void allowClientIdAsAudience() throws JOSEException { + final JWTClaimsSet claimsSetOne = new JWTClaimsSet.Builder() + .subject("foo") + .issueTime(Date.from(Instant.now().minusSeconds(60))) + .issuer("https://sts.windows.net/") + .audience(FAKE_APPLICATION_URI) + .build(); + final SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSetOne); + signedJWT.sign(signer); + + final String orderTwo = signedJWT.serialize(); + userPrincipalManager = new UserPrincipalManager(endpoints, properties, + resourceRetriever, true); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal(orderTwo)) + .doesNotThrowAnyException(); + } + + @Test + public void failWithUnkownAudience() throws JOSEException { + final JWTClaimsSet claimsSetOne = new JWTClaimsSet.Builder() + .subject("foo") + .issueTime(Date.from(Instant.now().minusSeconds(60))) + .issuer("https://sts.windows.net/") + .audience("unknown audience") + .build(); + final SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSetOne); + signedJWT.sign(signer); + + final String orderTwo = signedJWT.serialize(); + userPrincipalManager = new UserPrincipalManager(endpoints, properties, + resourceRetriever, true); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal(orderTwo)) + .hasMessageContaining("Invalid token audience."); + } + + @Test + public void failOnInvalidSiganture() throws JOSEException { + final JWTClaimsSet claimsSetOne = new JWTClaimsSet.Builder() + .subject("foo") + .issueTime(Date.from(Instant.now().minusSeconds(60))) + .issuer("https://sts.windows.net/") + .audience(FAKE_APPLICATION_URI) + .build(); + final SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSetOne); + signedJWT.sign(signer); + + final String orderTwo = signedJWT.serialize(); + final String invalidToken = orderTwo.substring(0, orderTwo.length() - 5); + + userPrincipalManager = new UserPrincipalManager(endpoints, properties, + resourceRetriever, true); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal(invalidToken)) + .hasMessageContaining("JWT rejected: Invalid signature"); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipalManagerTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipalManagerTest.java new file mode 100644 index 0000000000000..9295cc364d01c --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipalManagerTest.java @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.proc.BadJWTException; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + + +public class UserPrincipalManagerTest { + + private static ImmutableJWKSet immutableJWKSet; + + @BeforeAll + public static void setupClass() throws Exception { + final X509Certificate cert = (X509Certificate) CertificateFactory.getInstance("X.509") + .generateCertificate(Files.newInputStream(Paths.get("src/test/resources/test-public-key.txt"))); + immutableJWKSet = new ImmutableJWKSet<>(new JWKSet(JWK.parse( + cert))); + } + + private UserPrincipalManager userPrincipalManager; + + + @Test + public void testAlgIsTakenFromJWT() throws Exception { + userPrincipalManager = new UserPrincipalManager(immutableJWKSet); + final UserPrincipal userPrincipal = userPrincipalManager.buildUserPrincipal( + new String(Files.readAllBytes( + Paths.get("src/test/resources/jwt-signed.txt")), StandardCharsets.UTF_8)); + assertThat(userPrincipal).isNotNull().extracting(UserPrincipal::getIssuer, UserPrincipal::getSubject) + .containsExactly("https://sts.windows.net/test", "test@example.com"); + } + + @Test + public void invalidIssuer() { + userPrincipalManager = new UserPrincipalManager(immutableJWKSet); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal(readJwtValidIssuerTxt())) + .isInstanceOf(BadJWTException.class); + } + + //TODO: add more generated tokens with other valid issuers to this file. Didn't manage to generate them + @ParameterizedTest + @MethodSource("readJwtValidIssuerTxtStream") + public void validIssuer(final String token) { + userPrincipalManager = new UserPrincipalManager(immutableJWKSet); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal(token)) + .doesNotThrowAnyException(); + } + + @Test + public void nullIssuer() { + userPrincipalManager = new UserPrincipalManager(immutableJWKSet); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal(readJwtValidIssuerTxt())) + .isInstanceOf(BadJWTException.class); + } + + private String readJwtValidIssuerTxt() throws IOException { + return new String(Files.readAllBytes( + Paths.get("src/test/resources/jwt-null-issuer.txt")), StandardCharsets.UTF_8); + } + + private static Stream readJwtValidIssuerTxtStream() throws IOException { + return Stream.of(new String(Files.readAllBytes( + Paths.get("src/test/resources/jwt-valid-issuer.txt")), StandardCharsets.UTF_8)); + } + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java new file mode 100644 index 0000000000000..abb69fe086674 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationServerEndpoints; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jwt.JWTClaimsSet; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.util.StringUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.nio.file.Files; +import java.text.ParseException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.http.HttpHeaders.ACCEPT; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class UserPrincipalMicrosoftGraphTest { + + private WireMockRule wireMockRule; + + private String clientId; + private String clientSecret; + private AADAuthenticationProperties properties; + private AADAuthorizationServerEndpoints endpoints; + private String accessToken; + private static String userGroupsJson; + + static { + try { + final ObjectMapper objectMapper = new ObjectMapper(); + final Map json = objectMapper.readValue(UserPrincipalMicrosoftGraphTest.class + .getClassLoader().getResourceAsStream("aad/microsoft-graph-user-groups.json"), + new TypeReference>() { + }); + userGroupsJson = objectMapper.writeValueAsString(json); + } catch (IOException e) { + e.printStackTrace(); + userGroupsJson = null; + } + assertNotNull(userGroupsJson); + } + + @BeforeAll + public void setup() { + accessToken = MicrosoftGraphConstants.BEARER_TOKEN; + properties = new AADAuthenticationProperties(); + properties.setGraphMembershipUri("http://localhost:8080/memberOf"); + endpoints = new AADAuthorizationServerEndpoints(properties.getBaseUri(), properties.getTenantId()); + clientId = "client"; + clientSecret = "pass"; + wireMockRule = new WireMockRule(8080); + wireMockRule.start(); + } + + @AfterAll + public void close() { + if (wireMockRule.isRunning()) { + wireMockRule.shutdown(); + } + } + + @Test + public void getGroups() throws Exception { + properties.getUserGroup().setAllowedGroups(Arrays.asList("group1", "group2", "group3")); + AzureADGraphClient graphClientMock = new AzureADGraphClient(clientId, clientSecret, properties, + endpoints); + stubFor(get(urlEqualTo("/memberOf")) + .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE)) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .withBody(userGroupsJson))); + + Set groups = graphClientMock.getGroups(MicrosoftGraphConstants.BEARER_TOKEN); + assertThat(groups) + .isNotEmpty() + .containsExactlyInAnyOrder("group1", "group2", "group3"); + + verify(getRequestedFor(urlMatching("/memberOf")) + .withHeader(AUTHORIZATION, equalTo(String.format("Bearer %s", accessToken))) + .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE))); + } + + @Test + public void userPrincipalIsSerializable() throws ParseException, IOException, ClassNotFoundException { + final File tmpOutputFile = File.createTempFile("test-user-principal", "txt"); + + try (FileOutputStream fileOutputStream = new FileOutputStream(tmpOutputFile); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); + FileInputStream fileInputStream = new FileInputStream(tmpOutputFile); + ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) { + + final JWSObject jwsObject = JWSObject.parse(MicrosoftGraphConstants.JWT_TOKEN); + final JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().subject("fake-subject").build(); + final UserPrincipal principal = new UserPrincipal("", jwsObject, jwtClaimsSet); + + objectOutputStream.writeObject(principal); + + final UserPrincipal serializedPrincipal = (UserPrincipal) objectInputStream.readObject(); + + assertNotNull(serializedPrincipal, "Serialized UserPrincipal not null"); + assertTrue(StringUtils.hasText(serializedPrincipal.getKid()), "Serialized UserPrincipal kid not empty"); + assertNotNull(serializedPrincipal.getClaims(), "Serialized UserPrincipal claims not null."); + assertTrue(serializedPrincipal.getClaims().size() > 0, "Serialized UserPrincipal claims not empty."); + } finally { + Files.deleteIfExists(tmpOutputFile.toPath()); + } + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CAuthorizationRequestResolverTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CAuthorizationRequestResolverTest.java new file mode 100644 index 0000000000000..54b400e5e9236 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CAuthorizationRequestResolverTest.java @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.http.HttpMethod; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.util.Assert; + +import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; + +public class AADB2CAuthorizationRequestResolverTest { + + private WebApplicationContextRunner getContextRunner() { + return new WebApplicationContextRunner() + .withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class)) + .withConfiguration(AutoConfigurations.of(AbstractAADB2COAuth2ClientTestConfiguration.WebOAuth2ClientApp.class, + AADB2CAutoConfiguration.class)) + .withPropertyValues( + String.format("%s=%s", AADB2CConstants.TENANT_ID, AADB2CConstants.TEST_TENANT_ID), + String.format("%s=%s", AADB2CConstants.BASE_URI, AADB2CConstants.TEST_BASE_URI), + String.format("%s=%s", AADB2CConstants.CLIENT_ID, AADB2CConstants.TEST_CLIENT_ID), + String.format("%s=%s", AADB2CConstants.CLIENT_SECRET, AADB2CConstants.TEST_CLIENT_SECRET), + String.format("%s=%s", AADB2CConstants.LOGOUT_SUCCESS_URL, AADB2CConstants.TEST_LOGOUT_SUCCESS_URL), + String.format("%s=%s", AADB2CConstants.LOGIN_FLOW, AADB2CConstants.TEST_KEY_SIGN_UP_OR_IN), + String.format("%s.%s=%s", AADB2CConstants.USER_FLOWS, + AADB2CConstants.TEST_KEY_SIGN_UP_OR_IN, AADB2CConstants.TEST_SIGN_UP_OR_IN_NAME), + String.format("%s.%s=%s", AADB2CConstants.USER_FLOWS, + AADB2CConstants.TEST_KEY_PROFILE_EDIT, AADB2CConstants.TEST_PROFILE_EDIT_NAME), + String.format("%s=%s", AADB2CConstants.CONFIG_PROMPT, AADB2CConstants.TEST_PROMPT), + String.format("%s=%s", AADB2CConstants.CONFIG_LOGIN_HINT, AADB2CConstants.TEST_LOGIN_HINT) + ); + } + + private HttpServletRequest getHttpServletRequest(String uri) { + Assert.hasText(uri, "uri must contain text."); + + final MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.GET.toString(), uri); + + request.setServletPath(uri); + + return request; + } + + @Test + public void testAutoConfigurationBean() { + getContextRunner().run(c -> { + String requestUri = "/fake-url"; + HttpServletRequest request = getHttpServletRequest(requestUri); + final String registrationId = AADB2CConstants.TEST_SIGN_UP_OR_IN_NAME; + final AADB2CAuthorizationRequestResolver resolver = c.getBean(AADB2CAuthorizationRequestResolver.class); + + Assertions.assertNotNull(resolver); + Assertions.assertNull(resolver.resolve(request)); + Assertions.assertNull(resolver.resolve(request, registrationId)); + + requestUri = "/oauth2/authorization/" + AADB2CConstants.TEST_SIGN_UP_OR_IN_NAME; + request = getHttpServletRequest(requestUri); + + Assertions.assertNotNull(resolver.resolve(request)); + Assertions.assertNotNull(resolver.resolve(request, registrationId)); + + Assertions.assertEquals(resolver.resolve(request).getAdditionalParameters().get("p"), AADB2CConstants.TEST_SIGN_UP_OR_IN_NAME); + Assertions.assertEquals(resolver.resolve(request).getAdditionalParameters().get(AADB2CConstants.PROMPT), AADB2CConstants.TEST_PROMPT); + Assertions.assertEquals(resolver.resolve(request).getAdditionalParameters().get(AADB2CConstants.LOGIN_HINT), AADB2CConstants.TEST_LOGIN_HINT); + Assertions.assertEquals((resolver.resolve(request).getClientId()), AADB2CConstants.TEST_CLIENT_ID); + Assertions.assertEquals((resolver.resolve(request).getGrantType()), AuthorizationGrantType.AUTHORIZATION_CODE); + Assertions.assertTrue(resolver.resolve(request).getScopes().containsAll(Arrays.asList("openid", AADB2CConstants.TEST_CLIENT_ID))); + }); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CAutoConfigurationTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CAutoConfigurationTest.java new file mode 100644 index 0000000000000..a896c502f8fcf --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CAutoConfigurationTest.java @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.beans.BeanUtils; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.core.io.ClassPathResource; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +public class AADB2CAutoConfigurationTest extends AbstractAADB2COAuth2ClientTestConfiguration { + + @Override + public WebApplicationContextRunner getDefaultContextRunner() { + return new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(WebOAuth2ClientApp.class, AADB2CAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class)) + .withPropertyValues(getWebappCommonPropertyValues()); + } + + private String[] getWebappCommonPropertyValues() { + return new String[] { String.format("%s=%s", AADB2CConstants.BASE_URI, AADB2CConstants.TEST_BASE_URI), + String.format("%s=%s", AADB2CConstants.TENANT_ID, AADB2CConstants.TEST_TENANT_ID), + String.format("%s=%s", AADB2CConstants.CLIENT_ID, AADB2CConstants.TEST_CLIENT_ID), + String.format("%s=%s", AADB2CConstants.CLIENT_SECRET, AADB2CConstants.TEST_CLIENT_SECRET), + String.format("%s=%s", AADB2CConstants.LOGOUT_SUCCESS_URL, AADB2CConstants.TEST_LOGOUT_SUCCESS_URL), + String.format("%s=%s", AADB2CConstants.LOGIN_FLOW, AADB2CConstants.TEST_KEY_SIGN_UP_OR_IN), + String.format("%s.%s=%s", AADB2CConstants.USER_FLOWS, + AADB2CConstants.TEST_KEY_SIGN_UP_OR_IN, AADB2CConstants.TEST_SIGN_UP_OR_IN_NAME), + String.format("%s.%s=%s", AADB2CConstants.USER_FLOWS, + AADB2CConstants.TEST_KEY_SIGN_IN, AADB2CConstants.TEST_SIGN_IN_NAME), + String.format("%s.%s=%s", AADB2CConstants.USER_FLOWS, + AADB2CConstants.TEST_KEY_SIGN_UP, AADB2CConstants.TEST_SIGN_UP_NAME), + String.format("%s=%s", AADB2CConstants.CONFIG_PROMPT, AADB2CConstants.TEST_PROMPT), + String.format("%s=%s", AADB2CConstants.CONFIG_LOGIN_HINT, AADB2CConstants.TEST_LOGIN_HINT), + String.format("%s=%s", AADB2CConstants.USER_NAME_ATTRIBUTE_NAME, AADB2CConstants.TEST_ATTRIBUTE_NAME) }; + } + + @Test + public void testAutoConfigurationBean() { + getDefaultContextRunner().run(c -> { + final AADB2CAutoConfiguration autoConfig = c.getBean(AADB2CAutoConfiguration.class); + Assertions.assertNotNull(autoConfig); + }); + } + + @Test + public void testPropertiesBean() { + getDefaultContextRunner().run(c -> { + final AADB2CProperties properties = c.getBean(AADB2CProperties.class); + + Assertions.assertNotNull(properties); + Assertions.assertEquals(properties.getClientId(), AADB2CConstants.TEST_CLIENT_ID); + Assertions.assertEquals(properties.getClientSecret(), AADB2CConstants.TEST_CLIENT_SECRET); + Assertions.assertEquals(properties.getUserNameAttributeName(), AADB2CConstants.TEST_ATTRIBUTE_NAME); + + Map userFlows = properties.getUserFlows(); + Assertions.assertTrue(userFlows.size() > 0); + final Object prompt = properties.getAuthenticateAdditionalParameters().get(AADB2CConstants.PROMPT); + final String loginHint = + String.valueOf(properties.getAuthenticateAdditionalParameters().get(AADB2CConstants.LOGIN_HINT)); + Set clientNames = new HashSet<>(Arrays.asList(AADB2CConstants.TEST_SIGN_IN_NAME, + AADB2CConstants.TEST_SIGN_UP_NAME, AADB2CConstants.TEST_SIGN_UP_OR_IN_NAME)); + for (String clientName: userFlows.keySet()) { + Assertions.assertTrue(clientNames.contains(userFlows.get(clientName))); + } + Assertions.assertEquals(prompt, AADB2CConstants.TEST_PROMPT); + Assertions.assertEquals(loginHint, AADB2CConstants.TEST_LOGIN_HINT); + }); + } + + @Test + public void testAADB2CAuthorizationRequestResolverBean() { + getDefaultContextRunner().run(c -> { + final AADB2CAuthorizationRequestResolver resolver = c.getBean(AADB2CAuthorizationRequestResolver.class); + Assertions.assertNotNull(resolver); + }); + } + + @Test + public void testLogoutSuccessHandlerBean() { + getDefaultContextRunner().run(c -> { + final AADB2CLogoutSuccessHandler handler = c.getBean(AADB2CLogoutSuccessHandler.class); + Assertions.assertNotNull(handler); + }); + } + + @Test + public void testWebappConditionsIsInvokedWhenAADB2CEnableFileExists() { + try (MockedStatic beanUtils = mockStatic(BeanUtils.class, Mockito.CALLS_REAL_METHODS)) { + AADB2CConditions.UserFlowCondition userFlowCondition = spy(AADB2CConditions.UserFlowCondition.class); + AADB2CConditions.ClientRegistrationCondition clientRegistrationCondition = + spy(AADB2CConditions.ClientRegistrationCondition.class); + beanUtils.when(() -> BeanUtils.instantiateClass(AADB2CConditions.UserFlowCondition.class)) + .thenReturn(userFlowCondition); + beanUtils.when(() -> BeanUtils.instantiateClass(AADB2CConditions.ClientRegistrationCondition.class)) + .thenReturn(clientRegistrationCondition); + getDefaultContextRunner() + .run(c -> { + Assertions.assertTrue(c.getResource(AAD_B2C_ENABLE_CONFIG_FILE_NAME).exists()); + verify(userFlowCondition, atLeastOnce()).getMatchOutcome(any(), any()); + verify(clientRegistrationCondition, atLeastOnce()).getMatchOutcome(any(), any()); + }); + } + } + + @Test + public void testWebappConditionsIsNotInvokedWhenAADB2CEnableFileDoesNotExists() { + try (MockedStatic beanUtils = mockStatic(BeanUtils.class, Mockito.CALLS_REAL_METHODS)) { + AADB2CConditions.UserFlowCondition userFlowCondition = mock(AADB2CConditions.UserFlowCondition.class); + AADB2CConditions.ClientRegistrationCondition clientRegistrationCondition = + spy(AADB2CConditions.ClientRegistrationCondition.class); + beanUtils.when(() -> BeanUtils.instantiateClass(AADB2CConditions.UserFlowCondition.class)) + .thenReturn(userFlowCondition); + beanUtils.when(() -> BeanUtils.instantiateClass(AADB2CConditions.ClientRegistrationCondition.class)) + .thenReturn(clientRegistrationCondition); + new WebApplicationContextRunner() + .withClassLoader(new FilteredClassLoader(new ClassPathResource(AAD_B2C_ENABLE_CONFIG_FILE_NAME))) + .withConfiguration(AutoConfigurations.of(WebResourceServerApp.class, + AADB2CResourceServerAutoConfiguration.class)) + .withPropertyValues(getWebappCommonPropertyValues()) + .run(c -> { + Assertions.assertFalse(c.getResource(AAD_B2C_ENABLE_CONFIG_FILE_NAME).exists()); + verify(userFlowCondition, never()).getMatchOutcome(any(), any()); + verify(clientRegistrationCondition, never()).getMatchOutcome(any(), any()); + }); + } + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CConstants.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CConstants.java new file mode 100644 index 0000000000000..3c9da39523d00 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CConstants.java @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + + +import static com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c.AADB2CProperties.PREFIX; + +public class AADB2CConstants { + public static final String AUTHENTICATE_ADDITIONAL_PARAMETERS_LOGIN_HINT = ".authenticate-additional-parameters" + + ".login-hint"; + + public static final String AUTHENTICATE_ADDITIONAL_PARAMETERS_PROMPT = ".authenticate-additional-parameters.prompt"; + + public static final String PROMPT = "prompt"; + + public static final String LOGIN_HINT = "login-hint"; + + public static final Object TEST_PROMPT = "fake-prompt"; + + public static final String TEST_LOGIN_HINT = "fake-login-hint"; + + public static final String TEST_BASE_URI = "https://faketenant.b2clogin.com/faketenant.onmicrosoft.com/"; + + public static final String TEST_CLIENT_ID = "fake-client-id"; + + public static final String TEST_TENANT_ID = "fake-tenant-id"; + + public static final String TEST_CLIENT_SECRET = "fake-client-secret"; + public static final String TEST_APP_ID_URI = "https://fake-tenant.onmicrosoft.com/custom"; + + public static final String TEST_KEY_SIGN_UP_OR_IN = "sign-up-or-sign-in"; + public static final String TEST_SIGN_UP_OR_IN_NAME = "fake-sign-in-or-up"; + + public static final Object TEST_KEY_SIGN_IN = "sign-in"; + public static final Object TEST_SIGN_IN_NAME = "fake-sign-in"; + + public static final Object TEST_KEY_SIGN_UP = "sign-up"; + public static final Object TEST_SIGN_UP_NAME = "fake-sign-up"; + + public static final String TEST_KEY_PROFILE_EDIT = "profile-edit"; + public static final String TEST_PROFILE_EDIT_NAME = "profile_edit"; + + public static final String TEST_LOGOUT_SUCCESS_URL = "https://fake-logout-success-url"; + + public static final String TEST_CLIENT_CREDENTIAL_SCOPES = "https://fake-tenant.onmicrosoft.com/other/.default"; + public static final String TEST_CLIENT_CREDENTIAL_GRANT_TYPE = "client_credentials"; + + public static final String CLIENT_CREDENTIAL_NAME = "webApiA"; + + public static final String BASE_URI = String.format("%s.%s", PREFIX, "base-uri"); + + public static final String TEST_ATTRIBUTE_NAME = String.format("%s.%s", PREFIX, "name"); + + public static final String USER_NAME_ATTRIBUTE_NAME = String.format("%s.%s", PREFIX, "user-name-attribute-name"); + + public static final String CLIENT_ID = String.format("%s.%s", PREFIX, "client-id"); + + public static final String TENANT_ID = String.format("%s.%s", PREFIX, "tenant-id"); + + public static final String CLIENT_SECRET = String.format("%s.%s", PREFIX, "client-secret"); + public static final String APP_ID_URI = String.format("%s.%s", PREFIX, "app-id-uri"); + + public static final String LOGOUT_SUCCESS_URL = String.format("%s.%s", PREFIX, "logout-success-url"); + + public static final String LOGIN_FLOW = String.format("%s.%s", PREFIX, "login-flow"); + + public static final String USER_FLOWS = String.format("%s.%s", PREFIX, "user-flows"); + + public static final Object CONFIG_PROMPT = String.format("%s.%s", PREFIX, + AUTHENTICATE_ADDITIONAL_PARAMETERS_PROMPT); + + public static final String CONFIG_LOGIN_HINT = String.format("%s.%s", PREFIX, + AUTHENTICATE_ADDITIONAL_PARAMETERS_LOGIN_HINT); + + public static final String AUTHORIZATION_CLIENTS = String.format("%s.%s", PREFIX, "authorization-clients"); +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CLogoutSuccessHandlerTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CLogoutSuccessHandlerTest.java new file mode 100644 index 0000000000000..5c9f418f08672 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CLogoutSuccessHandlerTest.java @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import static org.assertj.core.api.Assertions.assertThat; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class AADB2CLogoutSuccessHandlerTest { + + private static final String BASE_URI = "https://faketenant.b2clogin.com/faketenant.onmicrosoft.com"; + + private static final String TEST_LOGOUT_SUCCESS_URL = "http://localhost:8080/login"; + + private static final String TEST_USER_FLOW_SIGN_UP_OR_IN = "my-sign-up-or-in"; + + private AADB2CProperties properties; + + @BeforeAll + public void setUp() { + properties = new AADB2CProperties(); + + properties.setBaseUri(BASE_URI); + properties.setLogoutSuccessUrl(TEST_LOGOUT_SUCCESS_URL); + properties.getUserFlows().put(AADB2CProperties.DEFAULT_KEY_SIGN_UP_OR_SIGN_IN, TEST_USER_FLOW_SIGN_UP_OR_IN); + } + + @Test + public void testDefaultTargetUrl() { + final MyLogoutSuccessHandler handler = new MyLogoutSuccessHandler(properties); + final String baseUri = properties.getBaseUri(); + final String url = properties.getLogoutSuccessUrl(); + final String userFlow = properties.getUserFlows().get(properties.getLoginFlow()); + + assertThat(handler.getTargetUrl()).isEqualTo(AADB2CURL.getEndSessionUrl(baseUri, url, userFlow)); + } + + private static class MyLogoutSuccessHandler extends AADB2CLogoutSuccessHandler { + + MyLogoutSuccessHandler(AADB2CProperties properties) { + super(properties); + } + + public String getTargetUrl() { + return super.getDefaultTargetUrl(); + } + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CResourceServerAutoConfigurationTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CResourceServerAutoConfigurationTest.java new file mode 100644 index 0000000000000..3ac607e1e775f --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CResourceServerAutoConfigurationTest.java @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADIssuerJWSKeySelector; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADTrustedIssuerRepository; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import com.nimbusds.jwt.proc.JWTClaimsSetAwareJWSKeySelector; +import com.nimbusds.jwt.proc.JWTProcessor; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.beans.BeanUtils; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.ClassPathResource; +import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +public class AADB2CResourceServerAutoConfigurationTest extends AbstractAADB2COAuth2ClientTestConfiguration { + + private WebApplicationContextRunner getResourceServerContextRunner() { + return new WebApplicationContextRunner() + .withClassLoader(new FilteredClassLoader(OAuth2LoginAuthenticationFilter.class)) + .withConfiguration(AutoConfigurations.of(WebResourceServerApp.class, + AADB2CResourceServerAutoConfiguration.class)) + .withPropertyValues(getB2CResourceServerProperties()); + } + + @Override + public WebApplicationContextRunner getDefaultContextRunner() { + return new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(WebOAuth2ClientApp.class, + AADB2CResourceServerAutoConfiguration.class)) + .withPropertyValues(getB2CResourceServerProperties()); + } + + private String[] getB2CResourceServerProperties() { + return new String[] { + String.format("%s=%s", AADB2CConstants.BASE_URI, AADB2CConstants.TEST_BASE_URI), + String.format("%s=%s", AADB2CConstants.TENANT_ID, AADB2CConstants.TEST_TENANT_ID), + String.format("%s=%s", AADB2CConstants.CLIENT_ID, AADB2CConstants.TEST_CLIENT_ID), + String.format("%s=%s", AADB2CConstants.APP_ID_URI, AADB2CConstants.TEST_APP_ID_URI), + String.format("%s.%s=%s", AADB2CConstants.USER_FLOWS, AADB2CConstants.TEST_KEY_SIGN_UP_OR_IN, + AADB2CConstants.TEST_SIGN_UP_OR_IN_NAME), + }; + } + + private ContextConsumer b2CAutoConfigurationBean() { + return (c) -> { + final AADB2CResourceServerAutoConfiguration autoResourceConfig = + c.getBean(AADB2CResourceServerAutoConfiguration.class); + Assertions.assertNotNull(autoResourceConfig); + }; + } + + private ContextConsumer b2CResourceServerPropertiesBean() { + return (c) -> { + final AADB2CProperties properties = c.getBean(AADB2CProperties.class); + + Assertions.assertNotNull(properties); + Assertions.assertEquals(properties.getTenantId(), AADB2CConstants.TEST_TENANT_ID); + Assertions.assertEquals(properties.getClientId(), AADB2CConstants.TEST_CLIENT_ID); + Assertions.assertEquals(properties.getAppIdUri(), AADB2CConstants.TEST_APP_ID_URI); + }; + } + + private ContextConsumer b2CResourceServerBean() { + return (c) -> { + final JwtDecoder jwtDecoder = c.getBean(JwtDecoder.class); + final AADIssuerJWSKeySelector jwsKeySelector = c.getBean(AADIssuerJWSKeySelector.class); + final AADTrustedIssuerRepository issuerRepository = c.getBean(AADTrustedIssuerRepository.class); + Assertions.assertNotNull(jwtDecoder); + Assertions.assertNotNull(jwsKeySelector); + Assertions.assertNotNull(issuerRepository); + }; + } + + @Test + public void testB2COAuth2ClientAutoConfigurationBean() { + getDefaultContextRunner().withPropertyValues(getAuthorizationClientPropertyValues()) + .run(b2CAutoConfigurationBean()); + } + + @Test + public void testB2COnlyAutoConfigurationBean() { + getResourceServerContextRunner().run(b2CAutoConfigurationBean()); + } + + @Test + public void testB2COAuth2ClientResourceServerPropertiesBean() { + getDefaultContextRunner().withPropertyValues(getAuthorizationClientPropertyValues()) + .run(b2CResourceServerPropertiesBean()); + } + + @Test + public void testB2COnlyResourceServerPropertiesBean() { + getResourceServerContextRunner().run(b2CResourceServerPropertiesBean()); + } + + @Test + public void testB2COAuth2ClientResourceServerBean() { + getDefaultContextRunner().withPropertyValues(getAuthorizationClientPropertyValues()) + .run(b2CResourceServerBean()); + } + + @Test + public void testB2COnlyResourceServerBean() { + getResourceServerContextRunner().run(b2CResourceServerBean()); + } + + @Test + public void testResourceServerConditionsIsInvokedWhenAADB2CEnableFileExists() { + try (MockedStatic beanUtils = mockStatic(BeanUtils.class, Mockito.CALLS_REAL_METHODS)) { + AADB2CConditions.ClientRegistrationCondition clientRegistrationCondition = + spy(AADB2CConditions.ClientRegistrationCondition.class); + beanUtils.when(() -> BeanUtils.instantiateClass(AADB2CConditions.ClientRegistrationCondition.class)) + .thenReturn(clientRegistrationCondition); + getDefaultContextRunner() + .withPropertyValues(getAuthorizationClientPropertyValues()) + .run(c -> { + Assertions.assertTrue(c.getResource(AAD_B2C_ENABLE_CONFIG_FILE_NAME).exists()); + verify(clientRegistrationCondition, atLeastOnce()).getMatchOutcome(any(), any()); + }); + } + } + + @Test + public void testResourceServerConditionsIsNotInvokedWhenAADB2CEnableFileDoesNotExists() { + try (MockedStatic beanUtils = mockStatic(BeanUtils.class, Mockito.CALLS_REAL_METHODS)) { + AADB2CConditions.ClientRegistrationCondition clientRegistrationCondition = + mock(AADB2CConditions.ClientRegistrationCondition.class); + beanUtils.when(() -> BeanUtils.instantiateClass(AADB2CConditions.ClientRegistrationCondition.class)) + .thenReturn(clientRegistrationCondition); + new WebApplicationContextRunner() + .withClassLoader(new FilteredClassLoader(new ClassPathResource(AAD_B2C_ENABLE_CONFIG_FILE_NAME))) + .withConfiguration(AutoConfigurations.of(WebOAuth2ClientApp.class, + AADB2CResourceServerAutoConfiguration.class)) + .withPropertyValues(getB2CResourceServerProperties()) + .withPropertyValues(getAuthorizationClientPropertyValues()) + .run(c -> { + Assertions.assertFalse(c.getResource(AAD_B2C_ENABLE_CONFIG_FILE_NAME).exists()); + verify(clientRegistrationCondition, never()).getMatchOutcome(any(), any()); + }); + } + } + + @Test + public void testExistAADB2CTrustedIssuerRepositoryBean() { + getDefaultContextRunner() + .withPropertyValues(getB2CResourceServerProperties()) + .withUserConfiguration(AADB2CResourceServerAutoConfiguration.class) + .run(context -> { + final AADB2CTrustedIssuerRepository aadb2CTrustedIssuerRepository = + context.getBean(AADB2CTrustedIssuerRepository.class); + assertThat(aadb2CTrustedIssuerRepository).isNotNull(); + assertThat(aadb2CTrustedIssuerRepository).isExactlyInstanceOf(AADB2CTrustedIssuerRepository.class); + }); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void testExistjwtProcessorBean() { + getDefaultContextRunner() + .withPropertyValues(getB2CResourceServerProperties()) + .withUserConfiguration(AADB2CResourceServerAutoConfiguration.class) + .run(context -> { + JWTProcessor jwtProcessor = context.getBean(JWTProcessor.class); + assertThat(jwtProcessor).isNotNull(); + assertThat(jwtProcessor).isExactlyInstanceOf(DefaultJWTProcessor.class); + }); + } + + @Test + public void testExistJwtDecoderBean() { + getDefaultContextRunner() + .withPropertyValues(getB2CResourceServerProperties()) + .withUserConfiguration(AADB2CResourceServerAutoConfiguration.class) + .run(context -> { + final JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class); + assertThat(jwtDecoder).isNotNull(); + assertThat(jwtDecoder).isExactlyInstanceOf(NimbusJwtDecoder.class); + }); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void testExistJWTClaimsSetAwareJWSKeySelectorBean() { + getDefaultContextRunner() + .withPropertyValues(getB2CResourceServerProperties()) + .withUserConfiguration(AADB2CResourceServerAutoConfiguration.class) + .run(context -> { + final JWTClaimsSetAwareJWSKeySelector jwsKeySelector = + context.getBean(JWTClaimsSetAwareJWSKeySelector.class); + assertThat(jwsKeySelector).isNotNull(); + assertThat(jwsKeySelector).isExactlyInstanceOf(AADIssuerJWSKeySelector.class); + }); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CURLTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CURLTest.java new file mode 100644 index 0000000000000..a51eea0ff2c8c --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CURLTest.java @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class AADB2CURLTest { + + private static final String DEFAULT_BASE_URI = "https://faketenant.b2clogin.com/faketenant.onmicrosoft.com/"; + private static final String CHINA_BASE_URI = "https://faketenant.b2clogin.cn/faketenant.partner.onmschina.cn/"; + private static final String B2C_TENANT_ID = "fake-tenant-id"; + + /** + * Reference pattern see AUTHORIZATION_URL_PATTERN of ${@link AADB2CURL}. + */ + @Test + public void testGetGlobalAuthorizationUrl() { + final String expect = "https://faketenant.b2clogin.com/faketenant.onmicrosoft.com/oauth2/v2.0/authorize"; + Assertions.assertEquals(AADB2CURL.getAuthorizationUrl(DEFAULT_BASE_URI), expect); + } + + @Test + public void testGetChinaAuthorizationUrl() { + final String expect = "https://faketenant.b2clogin.cn/faketenant.partner.onmschina.cn/oauth2/v2.0/authorize"; + Assertions.assertEquals(AADB2CURL.getAuthorizationUrl(CHINA_BASE_URI), expect); + } + + /** + * Reference pattern see TOKEN_URL_PATTERN of ${@link AADB2CURL}. + */ + @Test + public void testGetGlobalTokenUrl() { + final String expect = "https://faketenant.b2clogin.com/faketenant.onmicrosoft.com/oauth2/v2.0/token?p=fake-p"; + Assertions.assertEquals(AADB2CURL.getTokenUrl(DEFAULT_BASE_URI, "fake-p"), expect); + } + + @Test + public void testGetChinaTokenUrl() { + final String expect = "https://faketenant.b2clogin.cn/faketenant.partner.onmschina.cn/oauth2/v2.0/token?p=fake-p"; + Assertions.assertEquals(AADB2CURL.getTokenUrl(CHINA_BASE_URI, "fake-p"), expect); + } + + @Test + public void testGetTokenUrlException() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> AADB2CURL.getTokenUrl("", "")); + } + + @Test + public void testGetAADTokenUrlException() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> AADB2CURL.getAADTokenUrl("")); + } + + @Test + public void testGetAADTokenUrl() { + final String expect = "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/token"; + Assertions.assertEquals(AADB2CURL.getAADTokenUrl(B2C_TENANT_ID), expect); + } + + /** + * Reference pattern see JWKSET_URL_PATTERN of ${@link AADB2CURL}. + */ + @Test + public void testGetGlobalJwkSetUrl() { + final String expect = "https://faketenant.b2clogin.com/faketenant.onmicrosoft.com/discovery/v2.0/keys?p=new-p"; + Assertions.assertEquals(AADB2CURL.getJwkSetUrl(DEFAULT_BASE_URI, "new-p"), expect); + } + + @Test + public void testGetChinaJwkSetUrl() { + final String expect = "https://faketenant.b2clogin.cn/faketenant.partner.onmschina.cn/discovery/v2.0/keys?p=new-p"; + Assertions.assertEquals(AADB2CURL.getJwkSetUrl(CHINA_BASE_URI, "new-p"), expect); + } + + @Test + public void testGetJwkSetUrlException() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> AADB2CURL.getJwkSetUrl(DEFAULT_BASE_URI, "")); + } + + @Test + public void testGetAADJwkSetUrlException() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> AADB2CURL.getAADJwkSetUrl("")); + } + + @Test + public void testGetAADJwkSetUrl() { + final String expect = "https://login.microsoftonline.com/fake-tenant-id/discovery/v2.0/keys"; + Assertions.assertEquals(AADB2CURL.getAADJwkSetUrl(B2C_TENANT_ID), expect); + } + + /** + * Reference pattern see END_SESSION_URL_PATTERN of ${@link AADB2CURL}. + */ + @Test + public void testGetGlobalEndSessionUrl() { + final String expect = "https://faketenant.b2clogin.com/faketenant.onmicrosoft.com/oauth2/v2.0/logout?" + + "post_logout_redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fhome&p=my-p"; + + Assertions.assertEquals(AADB2CURL.getEndSessionUrl(DEFAULT_BASE_URI, + "http://localhost:8080/home", "my-p"), expect); + } + + @Test + public void testGetChinaEndSessionUrl() { + final String expect = "https://faketenant.b2clogin.cn/faketenant.partner.onmschina.cn/oauth2/v2.0/logout?" + + "post_logout_redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fhome&p=my-p"; + + Assertions.assertEquals(AADB2CURL.getEndSessionUrl(CHINA_BASE_URI, + "http://localhost:8080/home", "my-p"), expect); + } + + @Test + public void testGetEndSessionUrlException() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> AADB2CURL.getJwkSetUrl(DEFAULT_BASE_URI, "")); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CUserPrincipalTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CUserPrincipalTest.java new file mode 100644 index 0000000000000..d61727ffbab63 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AADB2CUserPrincipalTest.java @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADOAuth2AuthenticatedPrincipal; +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi.AADJwtBearerTokenAuthenticationConverter; +import net.minidev.json.JSONArray; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class AADB2CUserPrincipalTest { + + private Jwt jwt; + private Map claims; + private Map headers; + private JSONArray jsonArray = new JSONArray().appendElement("User.read").appendElement("User.write"); + + @BeforeEach + public void init() { + claims = new HashMap<>(); + claims.put("iss", "fake-issuer"); + claims.put("tid", "fake-tid"); + + headers = new HashMap<>(); + headers.put("kid", "kg2LYs2T0CTjIfj4rt6JIynen38"); + + jwt = mock(Jwt.class); + when(jwt.getClaim("scp")).thenReturn("Order.read Order.write"); + when(jwt.getClaim("roles")).thenReturn(jsonArray); + when(jwt.getTokenValue()).thenReturn("fake-token-value"); + when(jwt.getIssuedAt()).thenReturn(Instant.now()); + when(jwt.getHeaders()).thenReturn(headers); + when(jwt.getExpiresAt()).thenReturn(Instant.MAX); + when(jwt.getClaims()).thenReturn(claims); + } + + @Test + public void testCreateUserPrincipal() { + AADJwtBearerTokenAuthenticationConverter converter = new AADJwtBearerTokenAuthenticationConverter(); + AbstractAuthenticationToken authenticationToken = converter.convert(jwt); + Assertions.assertTrue(authenticationToken.getPrincipal().getClass().isAssignableFrom(AADOAuth2AuthenticatedPrincipal.class)); + AADOAuth2AuthenticatedPrincipal principal = (AADOAuth2AuthenticatedPrincipal) authenticationToken + .getPrincipal(); + Assertions.assertFalse(principal.getClaims().isEmpty()); + Assertions.assertEquals(principal.getIssuer(), claims.get("iss")); + Assertions.assertEquals(principal.getTenantId(), claims.get("tid")); + } + + @Test + public void testNoArgumentsConstructorDefaultScopeAndRoleAuthorities() { + AADJwtBearerTokenAuthenticationConverter converter = new AADJwtBearerTokenAuthenticationConverter(); + AbstractAuthenticationToken authenticationToken = converter.convert(jwt); + Assertions.assertTrue(authenticationToken.getPrincipal().getClass().isAssignableFrom(AADOAuth2AuthenticatedPrincipal.class)); + AADOAuth2AuthenticatedPrincipal principal = (AADOAuth2AuthenticatedPrincipal) authenticationToken + .getPrincipal(); + Assertions.assertFalse(principal.getAttributes().isEmpty()); + Assertions.assertEquals(2, principal.getAttributes().size()); + Assertions.assertEquals(4, principal.getAuthorities().size()); + Assertions.assertTrue(principal.getAuthorities().contains(new SimpleGrantedAuthority("SCOPE_Order.read"))); + Assertions.assertTrue(principal.getAuthorities().contains(new SimpleGrantedAuthority("APPROLE_User.write"))); + } + + @Test + public void testNoArgumentsConstructorExtractScopeAuthorities() { + when(jwt.getClaim("roles")).thenReturn(null); + AADJwtBearerTokenAuthenticationConverter converter = new AADJwtBearerTokenAuthenticationConverter(); + AbstractAuthenticationToken authenticationToken = converter.convert(jwt); + Assertions.assertTrue(authenticationToken.getPrincipal().getClass().isAssignableFrom(AADOAuth2AuthenticatedPrincipal.class)); + AADOAuth2AuthenticatedPrincipal principal = (AADOAuth2AuthenticatedPrincipal) authenticationToken + .getPrincipal(); + Assertions.assertFalse(principal.getAttributes().isEmpty()); + Assertions.assertEquals(2, principal.getAttributes().size()); + Assertions.assertEquals(2, principal.getAuthorities().size()); + Assertions.assertTrue(principal.getAuthorities().contains(new SimpleGrantedAuthority("SCOPE_Order.read"))); + Assertions.assertTrue(principal.getAuthorities().contains(new SimpleGrantedAuthority("SCOPE_Order.write"))); + Assertions.assertFalse(principal.getAuthorities().contains(new SimpleGrantedAuthority("APPROLE_User.read"))); + Assertions.assertFalse(principal.getAuthorities().contains(new SimpleGrantedAuthority("APPROLE_User.write"))); + } + + @Test + public void testNoArgumentsConstructorExtractRoleAuthorities() { + when(jwt.getClaim("scp")).thenReturn(null); + AADJwtBearerTokenAuthenticationConverter converter = new AADJwtBearerTokenAuthenticationConverter(); + AbstractAuthenticationToken authenticationToken = converter.convert(jwt); + Assertions.assertTrue(authenticationToken.getPrincipal().getClass().isAssignableFrom(AADOAuth2AuthenticatedPrincipal.class)); + AADOAuth2AuthenticatedPrincipal principal = (AADOAuth2AuthenticatedPrincipal) authenticationToken + .getPrincipal(); + Assertions.assertFalse(principal.getAttributes().isEmpty()); + Assertions.assertEquals(2, principal.getAttributes().size()); + Assertions.assertEquals(2, principal.getAuthorities().size()); + Assertions.assertTrue(principal.getAuthorities().contains(new SimpleGrantedAuthority("APPROLE_User.read"))); + Assertions.assertTrue(principal.getAuthorities().contains(new SimpleGrantedAuthority("APPROLE_User.write"))); + Assertions.assertFalse(principal.getAuthorities().contains(new SimpleGrantedAuthority("SCOPE_Order.read"))); + Assertions.assertFalse(principal.getAuthorities().contains(new SimpleGrantedAuthority("SCOPE_Order.write"))); + } + + @Test + public void testConstructorExtractRoleAuthoritiesWithAuthorityPrefixMapParameter() { + when(jwt.getClaim("scp")).thenReturn(null); + Map claimToAuthorityPrefixMap = new HashMap<>(); + claimToAuthorityPrefixMap.put("roles", "APPROLE_"); + AADJwtBearerTokenAuthenticationConverter converter = new AADJwtBearerTokenAuthenticationConverter("sub", claimToAuthorityPrefixMap); + AbstractAuthenticationToken authenticationToken = converter.convert(jwt); + assertThat(authenticationToken.getPrincipal()).isExactlyInstanceOf(AADOAuth2AuthenticatedPrincipal.class); + AADOAuth2AuthenticatedPrincipal principal = (AADOAuth2AuthenticatedPrincipal) authenticationToken + .getPrincipal(); + assertThat(principal.getAttributes()).isNotEmpty(); + assertThat(principal.getAttributes()).hasSize(2); + assertThat(principal.getAuthorities()).hasSize(2); + Assertions.assertTrue(principal.getAuthorities().contains(new SimpleGrantedAuthority("APPROLE_User.read"))); + Assertions.assertTrue(principal.getAuthorities().contains(new SimpleGrantedAuthority("APPROLE_User.write"))); + } + + @Test + public void testParameterConstructorExtractScopeAuthorities() { + when(jwt.getClaim("roles")).thenReturn(null); + AADJwtBearerTokenAuthenticationConverter converter = new AADJwtBearerTokenAuthenticationConverter("scp"); + AbstractAuthenticationToken authenticationToken = converter.convert(jwt); + Assertions.assertTrue(authenticationToken.getPrincipal().getClass().isAssignableFrom(AADOAuth2AuthenticatedPrincipal.class)); + AADOAuth2AuthenticatedPrincipal principal = (AADOAuth2AuthenticatedPrincipal) authenticationToken + .getPrincipal(); + Assertions.assertFalse(principal.getAttributes().isEmpty()); + Assertions.assertEquals(2, principal.getAttributes().size()); + Assertions.assertEquals(2, principal.getAuthorities().size()); + Assertions.assertTrue(principal.getAuthorities().contains(new SimpleGrantedAuthority("SCOPE_Order.read"))); + Assertions.assertTrue(principal.getAuthorities().contains(new SimpleGrantedAuthority("SCOPE_Order.write"))); + Assertions.assertFalse(principal.getAuthorities().contains(new SimpleGrantedAuthority("APPROLE_User.read"))); + Assertions.assertFalse(principal.getAuthorities().contains(new SimpleGrantedAuthority("APPROLE_User.write"))); + } + + @Test + public void testParameterConstructorExtractRoleAuthorities() { + when(jwt.getClaim("scp")).thenReturn(null); + AADJwtBearerTokenAuthenticationConverter converter = new AADJwtBearerTokenAuthenticationConverter("roles", + "APPROLE_"); + AbstractAuthenticationToken authenticationToken = converter.convert(jwt); + Assertions.assertTrue(authenticationToken.getPrincipal().getClass().isAssignableFrom(AADOAuth2AuthenticatedPrincipal.class)); + AADOAuth2AuthenticatedPrincipal principal = (AADOAuth2AuthenticatedPrincipal) authenticationToken + .getPrincipal(); + Assertions.assertFalse(principal.getAttributes().isEmpty()); + Assertions.assertEquals(2, principal.getAttributes().size()); + Assertions.assertEquals(2, principal.getAuthorities().size()); + Assertions.assertTrue(principal.getAuthorities().contains(new SimpleGrantedAuthority("APPROLE_User.read"))); + Assertions.assertTrue(principal.getAuthorities().contains(new SimpleGrantedAuthority("APPROLE_User.write"))); + Assertions.assertFalse(principal.getAuthorities().contains(new SimpleGrantedAuthority("SCOPE_Order.read"))); + Assertions.assertFalse(principal.getAuthorities().contains(new SimpleGrantedAuthority("SCOPE_Order.write"))); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AbstractAADB2COAuth2ClientTestConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AbstractAADB2COAuth2ClientTestConfiguration.java new file mode 100644 index 0000000000000..a21f92155776b --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/AbstractAADB2COAuth2ClientTestConfiguration.java @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; + +import com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.AADAuthorizationGrantType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.context.annotation.Import; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; + +import java.util.Map; + +public abstract class AbstractAADB2COAuth2ClientTestConfiguration { + + @EnableWebSecurity + @Import(OAuth2ClientAutoConfiguration.class) + public static class WebOAuth2ClientApp { + + } + + @EnableWebSecurity + public static class WebResourceServerApp { + + } + + protected static final String AAD_B2C_ENABLE_CONFIG_FILE_NAME = "aadb2c.enable.config"; + + abstract WebApplicationContextRunner getDefaultContextRunner(); + + protected String[] getAuthorizationClientPropertyValues() { + return new String[]{ String.format("%s.%s.scopes=%s", AADB2CConstants.AUTHORIZATION_CLIENTS, + AADB2CConstants.CLIENT_CREDENTIAL_NAME, AADB2CConstants.TEST_CLIENT_CREDENTIAL_SCOPES), + String.format("%s.%s.authorization-grant-type=%s", AADB2CConstants.AUTHORIZATION_CLIENTS, + AADB2CConstants.CLIENT_CREDENTIAL_NAME, AADB2CConstants.TEST_CLIENT_CREDENTIAL_GRANT_TYPE), + }; + } + + @Test + public void testClientCredentialProperties() { + getDefaultContextRunner() + .withPropertyValues(getAuthorizationClientPropertyValues()) + .run(c -> { + final AADB2CProperties properties = c.getBean(AADB2CProperties.class); + Assertions.assertNotNull(properties); + Map authorizationClients = properties.getAuthorizationClients(); + Assertions.assertTrue(authorizationClients.size() > 0); + for (String clientName: authorizationClients.keySet()) { + Assertions.assertEquals(clientName, AADB2CConstants.CLIENT_CREDENTIAL_NAME); + Assertions.assertEquals(authorizationClients.get(clientName).getScopes().get(0), + AADB2CConstants.TEST_CLIENT_CREDENTIAL_SCOPES); + Assertions.assertEquals(authorizationClients.get(clientName).getAuthorizationGrantType(), + AADAuthorizationGrantType.CLIENT_CREDENTIALS); + } + }); + } + + @Test + public void testClientRelatedBeans() { + getDefaultContextRunner() + .withPropertyValues(getAuthorizationClientPropertyValues()) + .run(c -> { + final AADB2COAuth2ClientConfiguration config = c.getBean(AADB2COAuth2ClientConfiguration.class); + final ClientRegistrationRepository clientRepo = c.getBean(ClientRegistrationRepository.class); + final OAuth2AuthorizedClientService clientService = c.getBean(OAuth2AuthorizedClientService.class); + final OAuth2AuthorizedClientRepository authorizedClientRepo = + c.getBean(OAuth2AuthorizedClientRepository.class); + final OAuth2AuthorizedClientManager clientManager = c.getBean(OAuth2AuthorizedClientManager.class); + + Assertions.assertNotNull(config); + Assertions.assertNotNull(clientRepo); + Assertions.assertNotNull(clientService); + Assertions.assertNotNull(authorizedClientRepo); + Assertions.assertNotNull(clientManager); + }); + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/AbstractCondition.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/AbstractCondition.java new file mode 100644 index 0000000000000..2ddafb9196b1e --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/AbstractCondition.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.condition.aad; + +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.context.annotation.Bean; + +import static org.assertj.core.api.Assertions.assertThat; + +public abstract class AbstractCondition { + + final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); + + static class Config { + + @Bean + String myBean() { + return "myBean"; + } + } + + protected ContextConsumer assertConditionMatch(boolean mustHaveBean) { + return (context) -> { + if (mustHaveBean) { + assertThat(context).hasBean("myBean"); + } else { + assertThat(context).doesNotHaveBean("myBean"); + } + }; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/ClientRegistrationConditionTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/ClientRegistrationConditionTest.java new file mode 100644 index 0000000000000..01c75b779407f --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/ClientRegistrationConditionTest.java @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.condition.aad; + +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.client.registration.ClientRegistration; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; + +public class ClientRegistrationConditionTest extends AbstractCondition { + + @Test + void testClientConditionWhenApplicationTypeIsEmpty() { + this.contextRunner + .withPropertyValues( + "azure.activedirectory.client-id = fake-client-id") + .withUserConfiguration(ClientRegistrationConditionConfig.class) + .run(assertConditionMatch(true)); + } + + @Test + void testClientConditionWhenNoOAuth2ClientDependency() { + this.contextRunner + .withPropertyValues("azure.activedirectory.client-id = fake-client-id") + .withClassLoader(new FilteredClassLoader(ClientRegistration.class)) + .withUserConfiguration(ClientRegistrationConditionConfig.class) + .run(assertConditionMatch(false)); + } + + @Test + void testClientConditionWhenApplicationTypeIsWebApplication() { + this.contextRunner + .withPropertyValues( + "azure.activedirectory.client-id = fake-client-id", + "azure.activedirectory.application-type=web_application") + .withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class)) + .withUserConfiguration(ClientRegistrationConditionConfig.class) + .run(assertConditionMatch(true)); + } + + @Test + void testClientConditionWhenApplicationTypeIsResourceServer() { + this.contextRunner + .withPropertyValues( + "azure.activedirectory.client-id = fake-client-id", + "azure.activedirectory.application-type=resource_server") + .withUserConfiguration(ClientRegistrationConditionConfig.class) + .run(assertConditionMatch(false)); + } + + @Test + void testClientConditionWhenApplicationTypeIsResourceServerWithOBO() { + this.contextRunner + .withPropertyValues( + "azure.activedirectory.client-id = fake-client-id", + "azure.activedirectory.application-type=resource_server_with_obo") + .withUserConfiguration(ClientRegistrationConditionConfig.class) + .run(assertConditionMatch(true)); + } + + @Test + void testClientConditionWhenApplicationTypeIsWebApplicationAndResourceServer() { + this.contextRunner + .withPropertyValues( + "azure.activedirectory.client-id = fake-client-id", + "azure.activedirectory.application-type=web_application_and_resource_server") + .withUserConfiguration(ClientRegistrationConditionConfig.class) + .run(assertConditionMatch(true)); + } + + @Configuration + @Conditional(ClientRegistrationCondition.class) + static class ClientRegistrationConditionConfig extends Config { } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/ResourceServerConditionTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/ResourceServerConditionTest.java new file mode 100644 index 0000000000000..339b0285b2602 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/ResourceServerConditionTest.java @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.condition.aad; + +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; + +public class ResourceServerConditionTest extends AbstractCondition { + + @Test + void testResourceServerConditionWhenApplicationTypeIsEmpty() { + this.contextRunner + .withPropertyValues( + "azure.activedirectory.client-id = fake-client-id") + .withUserConfiguration(ResourceServerConditionConfig.class) + .run(assertConditionMatch(true)); + } + + @Test + void testResourceServerConditionWhenNoOAuth2ResourceDependency() { + this.contextRunner + .withPropertyValues("azure.activedirectory.client-id = fake-client-id") + .withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class)) + .withUserConfiguration(ResourceServerConditionConfig.class) + .run(assertConditionMatch(false)); + } + + @Test + void testResourceServerConditionWhenApplicationTypeIsWebApplication() { + this.contextRunner + .withPropertyValues( + "azure.activedirectory.client-id = fake-client-id", + "azure.activedirectory.application-type=web_application") + .withUserConfiguration(ResourceServerConditionConfig.class) + .run(assertConditionMatch(false)); + } + + @Test + void testResourceServerConditionWhenApplicationTypeIsResourceServer() { + this.contextRunner + .withPropertyValues( + "azure.activedirectory.client-id = fake-client-id", + "azure.activedirectory.application-type=resource_server") + .withUserConfiguration(ResourceServerConditionConfig.class) + .run(assertConditionMatch(true)); + } + + @Test + void testResourceServerConditionWhenApplicationTypeIsResourceServerWithOBO() { + this.contextRunner + .withPropertyValues( + "azure.activedirectory.client-id = fake-client-id", + "azure.activedirectory.application-type=resource_server_with_obo") + .withUserConfiguration(ResourceServerConditionConfig.class) + .run(assertConditionMatch(true)); + } + + @Test + void testResourceServerConditionWhenApplicationTypeIsWebApplicationAndResourceServer() { + this.contextRunner + .withPropertyValues( + "azure.activedirectory.client-id = fake-client-id", + "azure.activedirectory.application-type=web_application_and_resource_server") + .withUserConfiguration(ResourceServerConditionConfig.class) + .run(assertConditionMatch(true)); + } + + @Configuration + @Conditional(ResourceServerCondition.class) + static class ResourceServerConditionConfig extends Config { } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/WebApplicationConditionTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/WebApplicationConditionTest.java new file mode 100644 index 0000000000000..b615de631a490 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/WebApplicationConditionTest.java @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.condition.aad; + +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.client.registration.ClientRegistration; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; + +public class WebApplicationConditionTest extends AbstractCondition { + + @Test + void testWebApplicationConditionWhenApplicationTypeIsEmpty() { + this.contextRunner + .withPropertyValues( + "azure.activedirectory.client-id = fake-client-id") + .withUserConfiguration(WebApplicationConditionConfig.class) + .run(assertConditionMatch(false)); + } + + @Test + void testWebAppConditionWhenNoOAuth2ResourceDependency() { + this.contextRunner + .withPropertyValues("azure.activedirectory.client-id = fake-client-id") + .withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class)) + .withUserConfiguration(WebApplicationConditionConfig.class) + .run(assertConditionMatch(true)); + } + + @Test + void testWebAppConditionWhenNoOAuth2ClientDependency() { + this.contextRunner + .withPropertyValues("azure.activedirectory.client-id = fake-client-id") + .withClassLoader(new FilteredClassLoader(ClientRegistration.class)) + .withUserConfiguration(WebApplicationConditionConfig.class) + .run(assertConditionMatch(false)); + } + + @Test + void testWebAppConditionWhenApplicationTypeIsWebApplication() { + this.contextRunner + .withPropertyValues( + "azure.activedirectory.client-id = fake-client-id", + "azure.activedirectory.application-type=web_application") + .withUserConfiguration(WebApplicationConditionConfig.class) + .run(assertConditionMatch(true)); + } + + @Test + void testWebAppConditionWhenApplicationTypeIsResourceServer() { + this.contextRunner + .withPropertyValues( + "azure.activedirectory.client-id = fake-client-id", + "azure.activedirectory.application-type=resource_server") + .withUserConfiguration(WebApplicationConditionConfig.class) + .run(assertConditionMatch(false)); + } + + @Test + void testWebAppConditionWhenApplicationTypeIsResourceServerWithOBO() { + this.contextRunner + .withPropertyValues( + "azure.activedirectory.client-id = fake-client-id", + "azure.activedirectory.application-type=resource_server_with_obo") + .withUserConfiguration(WebApplicationConditionConfig.class) + .run(assertConditionMatch(false)); + } + + @Test + void testWebAppConditionWhenApplicationTypeIsWebApplicationAndResourceServer() { + this.contextRunner + .withPropertyValues( + "azure.activedirectory.client-id = fake-client-id", + "azure.activedirectory.application-type=web_application_and_resource_server") + .withUserConfiguration(WebApplicationConditionConfig.class) + .run(assertConditionMatch(true)); + } + + @Configuration + @Conditional(WebApplicationCondition.class) + static class WebApplicationConditionConfig extends Config { } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/resources/aadb2c.enable.config b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/resources/aadb2c.enable.config new file mode 100644 index 0000000000000..2995a4d0e7491 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/resources/aadb2c.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/resources/jwt-bad-issuer.txt b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/resources/jwt-bad-issuer.txt new file mode 100644 index 0000000000000..28afc8898855b --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/resources/jwt-bad-issuer.txt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3dyb25naXNzdWVyLm5ldHQvdGVzdCIsInN1YiI6InRlc3RAZXhhbXBsZS5jb20iLCJuYmYiOjE1NDUwMDg5MDYsImV4cCI6OTk5OTk5OTk5OTksImlhdCI6MTU0NTAwODkwNiwianRpIjoidGVzdGlkIiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9.lK-pSsNMHHui8IxSCYs7e4gH9I29ug7CyNnT8GIJBWEZCeLs3IRnlkLG923Zn7eT_4A0aWFyEnjCBYIKtqX7AoaLBwCa08yB9x7c0DbJQvdKwFjGzc5zkpNxnzBZxXLJr7D9nMAjeQrYLkgoy4XXHL_m_Z6PTf9Jwl6tTYqUS06gd5ZokV1DtBTTPeDJj7KKzNhY3PQ1Hh_-RLoCspqIiZFZ8dfPgDCc2OXVCsH8_2tUFCktuPuVYD11Ws7_hFG6sq8AF1jyugrtYnwMhbzpMCtkL-SoZsmBtmAUFW20vTNYV6Vri-VEqz5VkHef9ZqZmlNPR0vH8hcZVj0IX8t7yA \ No newline at end of file diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/resources/jwt-null-issuer.txt b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/resources/jwt-null-issuer.txt new file mode 100644 index 0000000000000..ac1382b4f5eb6 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/resources/jwt-null-issuer.txt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmJmIjoxNTQ1MDA5MjQ5LCJleHAiOjk5OTk5OTk5OTk5LCJpYXQiOjE1NDUwMDkyNDksImp0aSI6InRlc3RpZCIsInR5cCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vcmVnaXN0ZXIifQ.2rHU8-_UWjE0U2Vq9KDmtt1ztlj_G9OW777O-kZ_di7dOBZPt6H2eMba34Qf5wILfs1bHBubMNIs64B9mLffJzXp_FKyMdcCsYecJAOaSscrSLjHYdnZqhRIETOloz-nbxiH_AhaJP6Hb482Hu7It4XhcxWU_tZ9kRD1brfoyb_-8Qh4vmrR4eddtfLZDlr3xFfTSD9FKDeECDWu59wGLBVS_32Y42XYV82f5PD1FsAG62vC-t2XdVS-y6aQIT1QElsKcc66xY21XgXq4fkFGxyoYPB1hCLIPz_QMJxRXql7AnVoxkueQxMzH4NCT64i1Aj7texhHbZh4-_jG29-zg \ No newline at end of file diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/resources/jwt-signed.txt b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/resources/jwt-signed.txt new file mode 100644 index 0000000000000..0ca89f3afca7f --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/resources/jwt-signed.txt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC90ZXN0Iiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIsIm5iZiI6MTU0NTAwODE5NiwiZXhwIjo5OTk5OTk5OTk5OSwiaWF0IjoxNTQ1MDA4MTk2LCJqdGkiOiJ0ZXN0aWQiLCJ0eXAiOiJodHRwczovL2V4YW1wbGUuY29tL3JlZ2lzdGVyIn0.ZQceiSqNKiEHrNaPhKCKW2EVEnhGbyh4TjbhqB-P7E70NRS3Ad89ISBaSyhpwRS6lwdpMrwNEETFloGm8H6nv623gcWzTCnb7bqaOWKCNTV9TjvhecjIe69AkNHfvkqyopbyRktKosWm89e2nAgiGtp-Y1Pyrt1_iiwOtvahtGyaWqs82-WkFY61DFI1e4iRBI6WSIGLUUpc4vXCGdQ33OyN6wAQ2IYeHCURmB-stVT-GcoMcDZKJBqnerQsu5WDbSwkZfcVTWDK-l_sz1WSdFGTdSWATZJ_LKvxa8IPX--s0-JRmZf-0dwadjcbCNLwYtYDvtaZyczouZKGGBoWZA \ No newline at end of file diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/resources/jwt-valid-issuer.txt b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/resources/jwt-valid-issuer.txt new file mode 100644 index 0000000000000..0ca89f3afca7f --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/resources/jwt-valid-issuer.txt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC90ZXN0Iiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIsIm5iZiI6MTU0NTAwODE5NiwiZXhwIjo5OTk5OTk5OTk5OSwiaWF0IjoxNTQ1MDA4MTk2LCJqdGkiOiJ0ZXN0aWQiLCJ0eXAiOiJodHRwczovL2V4YW1wbGUuY29tL3JlZ2lzdGVyIn0.ZQceiSqNKiEHrNaPhKCKW2EVEnhGbyh4TjbhqB-P7E70NRS3Ad89ISBaSyhpwRS6lwdpMrwNEETFloGm8H6nv623gcWzTCnb7bqaOWKCNTV9TjvhecjIe69AkNHfvkqyopbyRktKosWm89e2nAgiGtp-Y1Pyrt1_iiwOtvahtGyaWqs82-WkFY61DFI1e4iRBI6WSIGLUUpc4vXCGdQ33OyN6wAQ2IYeHCURmB-stVT-GcoMcDZKJBqnerQsu5WDbSwkZfcVTWDK-l_sz1WSdFGTdSWATZJ_LKvxa8IPX--s0-JRmZf-0dwadjcbCNLwYtYDvtaZyczouZKGGBoWZA \ No newline at end of file From f5b83ea3e6aabc7086d1f27db70f70d23e829c04 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Wed, 10 Nov 2021 14:18:18 +0800 Subject: [PATCH 2/4] Delete package-info.java in this package and is sub packages: com.azure.spring.cloud.autoconfigure.active.directory.implementation. --- .../active/directory/implementation/aad/package-info.java | 6 ------ .../directory/implementation/aad/webapi/package-info.java | 6 ------ .../implementation/aad/webapi/validator/package-info.java | 6 ------ .../directory/implementation/aad/webapp/package-info.java | 6 ------ .../implementation/autoconfigure/aad/package-info.java | 6 ------ .../implementation/autoconfigure/b2c/package-info.java | 6 ------ .../autoconfigure/condition/aad/package-info.java | 6 ------ 7 files changed, 42 deletions(-) delete mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/package-info.java delete mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/package-info.java delete mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/package-info.java delete mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/package-info.java delete mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/package-info.java delete mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/package-info.java delete mode 100644 sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/package-info.java diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/package-info.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/package-info.java deleted file mode 100644 index e03397cc583da..0000000000000 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/** - * Package com.azure.spring.aad - */ -package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad; diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/package-info.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/package-info.java deleted file mode 100644 index 2e64679339ae1..0000000000000 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/** - * Package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi - */ -package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi; diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/package-info.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/package-info.java deleted file mode 100644 index 12ee27a8e0581..0000000000000 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapi/validator/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/** - * Package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.resource.server.validator - */ -package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapi.validator; diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/package-info.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/package-info.java deleted file mode 100644 index ea86a7125287d..0000000000000 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/aad/webapp/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/** - * Package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp - */ -package com.azure.spring.cloud.autoconfigure.active.directory.implementation.aad.webapp; diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/package-info.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/package-info.java deleted file mode 100644 index 70d691f9c4d9a..0000000000000 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/aad/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/** - * Package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad - */ -package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.aad; diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/package-info.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/package-info.java deleted file mode 100644 index a43db843f5f48..0000000000000 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/b2c/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/** - * Package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c - */ -package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.b2c; diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/package-info.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/package-info.java deleted file mode 100644 index d43643070a005..0000000000000 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/active/directory/implementation/autoconfigure/condition/aad/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/** - * Package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.condition.aad - */ -package com.azure.spring.cloud.autoconfigure.active.directory.implementation.autoconfigure.condition.aad; From 0be23271d43d6e844c74ddfd2babd44473ac8556 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Wed, 10 Nov 2021 14:52:56 +0800 Subject: [PATCH 3/4] Create 2 starters: spring-cloud-azure-starter-active-directory and spring-cloud-azure-starter-active-directory-b2c. --- eng/versioning/version_client.txt | 2 + sdk/spring/pom.xml | 4 + .../pom.xml | 242 ++++++++++++++++++ .../src/main/resources/aadb2c.enable.config | 1 + .../pom.xml | 228 +++++++++++++++++ .../src/main/resources/aad.enable.config | 1 + 6 files changed, 478 insertions(+) create mode 100644 sdk/spring/spring-cloud-azure-starter-active-directory-b2c/pom.xml create mode 100644 sdk/spring/spring-cloud-azure-starter-active-directory-b2c/src/main/resources/aadb2c.enable.config create mode 100644 sdk/spring/spring-cloud-azure-starter-active-directory/pom.xml create mode 100644 sdk/spring/spring-cloud-azure-starter-active-directory/src/main/resources/aad.enable.config diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index 9b9e1a697b95b..3d5a2d4a8349d 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -168,6 +168,8 @@ com.azure.spring:spring-cloud-azure-actuator;4.0.0-beta.1;4.0.0-beta.1 com.azure.spring:spring-cloud-azure-autoconfigure;4.0.0-beta.1;4.0.0-beta.1 com.azure.spring:spring-cloud-azure-resourcemanager;4.0.0-beta.1;4.0.0-beta.1 com.azure.spring:spring-cloud-azure-service;4.0.0-beta.1;4.0.0-beta.1 +com.azure.spring:spring-cloud-azure-starter-active-directory;4.0.0-beta.1;4.0.0-beta.1 +com.azure.spring:spring-cloud-azure-starter-active-directory-b2c;4.0.0-beta.1;4.0.0-beta.1 com.azure.spring:spring-cloud-azure-starter-actuator;4.0.0-beta.1;4.0.0-beta.1 com.azure.spring:spring-cloud-azure-starter-appconfiguration;4.0.0-beta.1;4.0.0-beta.1 com.azure.spring:spring-cloud-azure-starter-cosmos;4.0.0-beta.1;4.0.0-beta.1 diff --git a/sdk/spring/pom.xml b/sdk/spring/pom.xml index 96a4a198bd02e..5265ef2a4bf22 100644 --- a/sdk/spring/pom.xml +++ b/sdk/spring/pom.xml @@ -173,6 +173,8 @@ spring-cloud-azure-actuator spring-cloud-azure-actuator-autoconfigure spring-cloud-azure-autoconfigure + spring-cloud-azure-starter-active-directory + spring-cloud-azure-starter-active-directory-b2c spring-cloud-azure-starter-actuator spring-cloud-azure-starter-appconfiguration spring-cloud-azure-starter-cosmos @@ -221,6 +223,8 @@ spring-cloud-azure-actuator spring-cloud-azure-actuator-autoconfigure spring-cloud-azure-autoconfigure + spring-cloud-azure-starter-active-directory + spring-cloud-azure-starter-active-directory-b2c spring-cloud-azure-starter-actuator spring-cloud-azure-starter-appconfiguration spring-cloud-azure-starter-cosmos diff --git a/sdk/spring/spring-cloud-azure-starter-active-directory-b2c/pom.xml b/sdk/spring/spring-cloud-azure-starter-active-directory-b2c/pom.xml new file mode 100644 index 0000000000000..535204c93b102 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-starter-active-directory-b2c/pom.xml @@ -0,0 +1,242 @@ + + + 4.0.0 + + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.azure.spring + spring-cloud-azure-starter-active-directory-b2c + 4.0.0-beta.1 + + Spring Cloud Azure Starter Active Directory B2C + Spring Cloud Azure Starter Active Directory B2C + https://github.com/Azure/azure-sdk-for-java + + + + + com.azure.spring + spring-cloud-azure-starter + 4.0.0-beta.1 + + + + org.springframework.boot + spring-boot-starter + 2.5.4 + + + + org.springframework.boot + spring-boot-starter-validation + 2.5.4 + + + + + org.springframework + spring-web + 5.3.9 + + + + javax.validation + validation-api + 2.0.1.Final + + + + + org.springframework.security + spring-security-core + 5.5.2 + + + + org.springframework.security + spring-security-web + 5.5.2 + + + + org.springframework.security + spring-security-config + 5.5.2 + + + + org.springframework.security + spring-security-oauth2-core + 5.5.2 + + + + org.springframework.security + spring-security-oauth2-client + 5.5.2 + + + + org.springframework.security + spring-security-oauth2-jose + 5.5.2 + + + + org.springframework.security + spring-security-oauth2-resource-server + 5.5.2 + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + javax.validation:validation-api:[2.0.1.Final] + org.springframework:spring-web:[5.3.9] + org.springframework.boot:spring-boot-starter:[2.5.4] + org.springframework.boot:spring-boot-starter-validation:[2.5.4] + org.springframework.security:spring-security-config:[5.5.2] + org.springframework.security:spring-security-core:[5.5.2] + org.springframework.security:spring-security-oauth2-client:[5.5.2] + org.springframework.security:spring-security-oauth2-core:[5.5.2] + org.springframework.security:spring-security-oauth2-jose:[5.5.2] + org.springframework.security:spring-security-oauth2-resource-server:[5.5.2] + org.springframework.security:spring-security-web:[5.5.2] + + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.3.1 + + + attach-javadocs + + jar + + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.2 + + + + empty-javadoc-jar-with-readme + package + + jar + + + javadoc + ${project.basedir}/javadocTemp + + + + + + empty-sources-jar-with-readme + package + + jar + + + sources + ${project.basedir}/sourceTemp + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.8 + + + copy-readme-to-javadocTemp + prepare-package + + + Deleting existing ${project.basedir}/javadocTemp + + + + Copying ${project.basedir}/README.md to + ${project.basedir}/javadocTemp/README.md + + + + + + run + + + + copy-readme-to-sourceTemp + prepare-package + + + Deleting existing ${project.basedir}/sourceTemp + + + + Copying ${project.basedir}/README.md to + ${project.basedir}/sourceTemp/README.md + + + + + + run + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.0.1 + + + attach-sources + none + + + + + + + diff --git a/sdk/spring/spring-cloud-azure-starter-active-directory-b2c/src/main/resources/aadb2c.enable.config b/sdk/spring/spring-cloud-azure-starter-active-directory-b2c/src/main/resources/aadb2c.enable.config new file mode 100644 index 0000000000000..2995a4d0e7491 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-starter-active-directory-b2c/src/main/resources/aadb2c.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/spring-cloud-azure-starter-active-directory/pom.xml b/sdk/spring/spring-cloud-azure-starter-active-directory/pom.xml new file mode 100644 index 0000000000000..cdb766abbfa9d --- /dev/null +++ b/sdk/spring/spring-cloud-azure-starter-active-directory/pom.xml @@ -0,0 +1,228 @@ + + + 4.0.0 + + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.azure.spring + spring-cloud-azure-starter-active-directory + 4.0.0-beta.1 + + Spring Cloud Azure Starter Active Directory + Spring Cloud Azure Starter Active Directory + https://github.com/Azure/azure-sdk-for-java + + + + com.azure.spring + spring-cloud-azure-starter + 4.0.0-beta.1 + + + org.springframework.boot + spring-boot-starter + 2.5.4 + + + org.springframework.boot + spring-boot-starter-validation + 2.5.4 + + + org.springframework.boot + spring-boot-starter-webflux + 2.5.4 + + + org.springframework + spring-web + 5.3.9 + + + org.springframework.security + spring-security-core + 5.5.2 + + + org.springframework.security + spring-security-web + 5.5.2 + + + org.springframework.security + spring-security-config + 5.5.2 + + + com.microsoft.azure + msal4j + 1.11.0 + + + com.nimbusds + nimbus-jose-jwt + 9.10.1 + + + com.fasterxml.jackson.core + jackson-databind + 2.12.5 + + + io.projectreactor.netty + reactor-netty + 1.0.11 + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + com.fasterxml.jackson.core:jackson-databind:[2.12.5] + com.microsoft.azure:msal4j:[1.11.0] + com.nimbusds:nimbus-jose-jwt:[9.10.1] + io.projectreactor.netty:reactor-netty:[1.0.11] + org.springframework.boot:spring-boot-starter-validation:[2.5.4] + org.springframework.boot:spring-boot-starter-webflux:[2.5.4] + org.springframework.boot:spring-boot-starter:[2.5.4] + org.springframework.security:spring-security-config:[5.5.2] + org.springframework.security:spring-security-core:[5.5.2] + org.springframework.security:spring-security-web:[5.5.2] + org.springframework:spring-web:[5.3.9] + + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.3.1 + + + attach-javadocs + + jar + + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.2 + + + + empty-javadoc-jar-with-readme + package + + jar + + + javadoc + ${project.basedir}/javadocTemp + + + + + + empty-sources-jar-with-readme + package + + jar + + + sources + ${project.basedir}/sourceTemp + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.8 + + + copy-readme-to-javadocTemp + prepare-package + + + Deleting existing ${project.basedir}/javadocTemp + + + + Copying ${project.basedir}/README.md to + ${project.basedir}/javadocTemp/README.md + + + + + + run + + + + copy-readme-to-sourceTemp + prepare-package + + + Deleting existing ${project.basedir}/sourceTemp + + + + Copying ${project.basedir}/README.md to + ${project.basedir}/sourceTemp/README.md + + + + + + run + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.0.1 + + + attach-sources + none + + + + + + + diff --git a/sdk/spring/spring-cloud-azure-starter-active-directory/src/main/resources/aad.enable.config b/sdk/spring/spring-cloud-azure-starter-active-directory/src/main/resources/aad.enable.config new file mode 100644 index 0000000000000..2995a4d0e7491 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-starter-active-directory/src/main/resources/aad.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file From 4416b6dfa6c93ae3897797d29046ad91ecb5e61b Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Wed, 10 Nov 2021 15:15:27 +0800 Subject: [PATCH 4/4] Delete unnecessary contents in the 2 starters: spring-cloud-azure-starter-active-directory and spring-cloud-azure-starter-active-directory-b2c. --- .../pom.xml | 152 +----------------- .../pom.xml | 151 +---------------- 2 files changed, 2 insertions(+), 301 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-starter-active-directory-b2c/pom.xml b/sdk/spring/spring-cloud-azure-starter-active-directory-b2c/pom.xml index 535204c93b102..4a569c7ebb6a7 100644 --- a/sdk/spring/spring-cloud-azure-starter-active-directory-b2c/pom.xml +++ b/sdk/spring/spring-cloud-azure-starter-active-directory-b2c/pom.xml @@ -2,14 +2,8 @@ - 4.0.0 - - com.azure - azure-client-sdk-parent - 1.7.0 - ../../parents/azure-client-sdk-parent - + 4.0.0 com.azure.spring spring-cloud-azure-starter-active-directory-b2c @@ -20,7 +14,6 @@ https://github.com/Azure/azure-sdk-for-java - com.azure.spring spring-cloud-azure-starter @@ -96,147 +89,4 @@ - - - - org.apache.maven.plugins - maven-enforcer-plugin - 3.0.0-M3 - - - - - javax.validation:validation-api:[2.0.1.Final] - org.springframework:spring-web:[5.3.9] - org.springframework.boot:spring-boot-starter:[2.5.4] - org.springframework.boot:spring-boot-starter-validation:[2.5.4] - org.springframework.security:spring-security-config:[5.5.2] - org.springframework.security:spring-security-core:[5.5.2] - org.springframework.security:spring-security-oauth2-client:[5.5.2] - org.springframework.security:spring-security-oauth2-core:[5.5.2] - org.springframework.security:spring-security-oauth2-jose:[5.5.2] - org.springframework.security:spring-security-oauth2-resource-server:[5.5.2] - org.springframework.security:spring-security-web:[5.5.2] - - - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.3.1 - - - attach-javadocs - - jar - - - true - - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.1.2 - - - - empty-javadoc-jar-with-readme - package - - jar - - - javadoc - ${project.basedir}/javadocTemp - - - - - - empty-sources-jar-with-readme - package - - jar - - - sources - ${project.basedir}/sourceTemp - - - - - - - org.apache.maven.plugins - maven-antrun-plugin - 1.8 - - - copy-readme-to-javadocTemp - prepare-package - - - Deleting existing ${project.basedir}/javadocTemp - - - - Copying ${project.basedir}/README.md to - ${project.basedir}/javadocTemp/README.md - - - - - - run - - - - copy-readme-to-sourceTemp - prepare-package - - - Deleting existing ${project.basedir}/sourceTemp - - - - Copying ${project.basedir}/README.md to - ${project.basedir}/sourceTemp/README.md - - - - - - run - - - - - - org.apache.maven.plugins - maven-source-plugin - 3.0.1 - - - attach-sources - none - - - - - - diff --git a/sdk/spring/spring-cloud-azure-starter-active-directory/pom.xml b/sdk/spring/spring-cloud-azure-starter-active-directory/pom.xml index cdb766abbfa9d..62db19d7692b5 100644 --- a/sdk/spring/spring-cloud-azure-starter-active-directory/pom.xml +++ b/sdk/spring/spring-cloud-azure-starter-active-directory/pom.xml @@ -2,14 +2,8 @@ - 4.0.0 - - com.azure - azure-client-sdk-parent - 1.7.0 - ../../parents/azure-client-sdk-parent - + 4.0.0 com.azure.spring spring-cloud-azure-starter-active-directory @@ -82,147 +76,4 @@ - - - - org.apache.maven.plugins - maven-enforcer-plugin - 3.0.0-M3 - - - - - com.fasterxml.jackson.core:jackson-databind:[2.12.5] - com.microsoft.azure:msal4j:[1.11.0] - com.nimbusds:nimbus-jose-jwt:[9.10.1] - io.projectreactor.netty:reactor-netty:[1.0.11] - org.springframework.boot:spring-boot-starter-validation:[2.5.4] - org.springframework.boot:spring-boot-starter-webflux:[2.5.4] - org.springframework.boot:spring-boot-starter:[2.5.4] - org.springframework.security:spring-security-config:[5.5.2] - org.springframework.security:spring-security-core:[5.5.2] - org.springframework.security:spring-security-web:[5.5.2] - org.springframework:spring-web:[5.3.9] - - - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.3.1 - - - attach-javadocs - - jar - - - true - - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.1.2 - - - - empty-javadoc-jar-with-readme - package - - jar - - - javadoc - ${project.basedir}/javadocTemp - - - - - - empty-sources-jar-with-readme - package - - jar - - - sources - ${project.basedir}/sourceTemp - - - - - - - org.apache.maven.plugins - maven-antrun-plugin - 1.8 - - - copy-readme-to-javadocTemp - prepare-package - - - Deleting existing ${project.basedir}/javadocTemp - - - - Copying ${project.basedir}/README.md to - ${project.basedir}/javadocTemp/README.md - - - - - - run - - - - copy-readme-to-sourceTemp - prepare-package - - - Deleting existing ${project.basedir}/sourceTemp - - - - Copying ${project.basedir}/README.md to - ${project.basedir}/sourceTemp/README.md - - - - - - run - - - - - - org.apache.maven.plugins - maven-source-plugin - 3.0.1 - - - attach-sources - none - - - - - -