diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index 78d860a95490..6d976efe5415 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -99,6 +99,7 @@ org.springframework.boot:spring-boot-starter-logging;2.3.5.RELEASE org.springframework.boot:spring-boot-starter-test;2.3.5.RELEASE org.springframework.boot:spring-boot-starter-validation;2.3.5.RELEASE org.springframework.boot:spring-boot-starter-web;2.3.5.RELEASE +org.springframework.boot:spring-boot-starter-webflux;2.3.5.RELEASE org.springframework.boot:spring-boot-starter;2.3.5.RELEASE org.springframework.boot:spring-boot;2.3.5.RELEASE org.springframework.data:spring-data-commons;2.3.5.RELEASE diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-backend-v2/src/main/java/com/azure/spring/sample/aad/security/AADOAuth2LoginSecurityConfig.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-backend-v2/src/main/java/com/azure/spring/sample/aad/security/AADOAuth2LoginSecurityConfig.java index 8638bdd39eee..d760379946a4 100644 --- a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-backend-v2/src/main/java/com/azure/spring/sample/aad/security/AADOAuth2LoginSecurityConfig.java +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-backend-v2/src/main/java/com/azure/spring/sample/aad/security/AADOAuth2LoginSecurityConfig.java @@ -3,9 +3,7 @@ package com.azure.spring.sample.aad.security; -import com.azure.spring.autoconfigure.aad.AADAuthenticationFailureHandler; import com.azure.spring.autoconfigure.aad.AADOAuth2AuthorizationRequestResolver; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; @@ -31,16 +29,15 @@ protected void configure(HttpSecurity http) throws Exception { final ClientRegistrationRepository clientRegistrationRepository = applicationContext.getBean(ClientRegistrationRepository.class); http.authorizeRequests() - .anyRequest().authenticated() - .and() - .oauth2Login() - .userInfoEndpoint() - .oidcUserService(oidcUserService) - .and() - .authorizationEndpoint() - .authorizationRequestResolver( - new AADOAuth2AuthorizationRequestResolver(clientRegistrationRepository)) - .and() - .failureHandler(new AADAuthenticationFailureHandler()); + .anyRequest().authenticated() + .and() + .oauth2Login() + .userInfoEndpoint() + .oidcUserService(oidcUserService) + .and() + .authorizationEndpoint() + .authorizationRequestResolver( + new AADOAuth2AuthorizationRequestResolver(clientRegistrationRepository) + ); } } diff --git a/sdk/spring/azure-spring-boot-starter-active-directory/CHANGELOG.md b/sdk/spring/azure-spring-boot-starter-active-directory/CHANGELOG.md index 50320edb1ccf..8fc4db6a1e5b 100644 --- a/sdk/spring/azure-spring-boot-starter-active-directory/CHANGELOG.md +++ b/sdk/spring/azure-spring-boot-starter-active-directory/CHANGELOG.md @@ -2,6 +2,7 @@ ## 3.0.0-beta.1 (Unreleased) ### Breaking Changes +- Conditional access policy is not supported temporary, we may recover it in the future. - Configuration items like `spring.security.oauth2.client.xxx` is not supported anymore. Please use the following configuration items instead: ``` azure.activedirectory.tenant-id=xxxxxx-your-tenant-id-xxxxxx diff --git a/sdk/spring/azure-spring-boot-starter-active-directory/README.md b/sdk/spring/azure-spring-boot-starter-active-directory/README.md index 88597e6f1bfc..5ba94ee17f1e 100644 --- a/sdk/spring/azure-spring-boot-starter-active-directory/README.md +++ b/sdk/spring/azure-spring-boot-starter-active-directory/README.md @@ -204,39 +204,6 @@ azure.activedirectory.environment=cn-v2-graph Please refer to [azure-spring-boot-sample-active-directory-backend-v2](https://github.com/Azure/azure-sdk-for-java/blob/master/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-backend-v2/README.md) to see a sample configured to use the Microsoft Graph API. -### AAD Conditional Access Policy -Now azure-active-directory-spring-boot-starter has supported AAD conditional access policy, if you are using this policy, you need add **AADOAuth2AuthorizationRequestResolver** and **AADAuthenticationFailureHandler** to your WebSecurityConfigurerAdapter. - -```java -@EnableWebSecurity -@EnableGlobalMethodSecurity(prePostEnabled = true) -public class AADOAuth2LoginConditionalPolicyConfigSample extends WebSecurityConfigurerAdapter { - - @Autowired - private OAuth2UserService oidcUserService; - - @Autowired - ApplicationContext applicationContext; - - @Override - protected void configure(HttpSecurity http) throws Exception { - final ClientRegistrationRepository clientRegistrationRepository = - applicationContext.getBean(ClientRegistrationRepository.class); - - http.authorizeRequests() - .anyRequest().authenticated() - .and() - .oauth2Login() - .userInfoEndpoint() - .oidcUserService(oidcUserService) - .and() - .authorizationEndpoint() - .authorizationRequestResolver(new AADOAuth2AuthorizationRequestResolver(clientRegistrationRepository)) - .and() - .failureHandler(new AADAuthenticationFailureHandler()); - } -} -``` ### Customize scopes in authorize requests By default, `azure-spring-boot-starter-active-directory` configures scopes of `openid`, `profile` and `https://graph.microsoft.com/user.read` to implement OpenID Connect protocol and access of Microsoft Graph API. For customization of scope, developers need to configure in the `application.properties`: diff --git a/sdk/spring/azure-spring-boot-starter-active-directory/pom.xml b/sdk/spring/azure-spring-boot-starter-active-directory/pom.xml index 7e60b4cef3fe..0b78b3647e05 100644 --- a/sdk/spring/azure-spring-boot-starter-active-directory/pom.xml +++ b/sdk/spring/azure-spring-boot-starter-active-directory/pom.xml @@ -30,6 +30,11 @@ spring-boot-starter-validation 2.3.5.RELEASE + + org.springframework.boot + spring-boot-starter-webflux + 2.3.5.RELEASE + com.azure.spring azure-spring-boot @@ -70,6 +75,11 @@ jackson-databind 2.11.3 + + io.projectreactor.netty + reactor-netty + 0.9.13.RELEASE + @@ -85,12 +95,14 @@ com.fasterxml.jackson.core:jackson-databind:[2.11.3] com.microsoft.azure:msal4j:[1.8.0] com.nimbusds:nimbus-jose-jwt:[8.19] - org.springframework:spring-web:[5.2.10.RELEASE] - org.springframework.boot:spring-boot-starter:[2.3.5.RELEASE] + io.projectreactor.netty:reactor-netty:[0.9.13.RELEASE] org.springframework.boot:spring-boot-starter-validation:[2.3.5.RELEASE] + org.springframework.boot:spring-boot-starter-webflux:[2.3.5.RELEASE] + org.springframework.boot:spring-boot-starter:[2.3.5.RELEASE] org.springframework.security:spring-security-config:[5.3.5.RELEASE] org.springframework.security:spring-security-core:[5.3.5.RELEASE] org.springframework.security:spring-security-web:[5.3.5.RELEASE] + org.springframework:spring-web:[5.2.10.RELEASE] diff --git a/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AppAutoConfigTest.java b/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AppAutoConfigTest.java index 498b78e30b5f..36f41f1acceb 100644 --- a/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AppAutoConfigTest.java +++ b/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AppAutoConfigTest.java @@ -1,8 +1,8 @@ package com.azure.test.aad.auth; -import com.azure.spring.aad.implementation.AzureClientRegistrationRepository; -import com.azure.spring.aad.implementation.DefaultClient; -import com.azure.spring.aad.implementation.IdentityEndpoints; +import com.azure.spring.autoconfigure.aad.AuthorizationServerEndpoints; +import com.azure.spring.autoconfigure.aad.AzureClientRegistrationRepository; +import com.azure.spring.autoconfigure.aad.DefaultClient; import com.azure.test.utils.AppRunner; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -34,17 +34,17 @@ public void clientRegistered() { assertEquals("fake-client-id", azureClientRegistration.getClientId()); assertEquals("fake-client-secret", azureClientRegistration.getClientSecret()); - IdentityEndpoints identityEndpoints = new IdentityEndpoints(); + AuthorizationServerEndpoints authorizationServerEndpoints = new AuthorizationServerEndpoints(); assertEquals( - identityEndpoints.authorizationEndpoint("fake-tenant-id"), + authorizationServerEndpoints.authorizationEndpoint("fake-tenant-id"), azureClientRegistration.getProviderDetails().getAuthorizationUri() ); assertEquals( - identityEndpoints.tokenEndpoint("fake-tenant-id"), + authorizationServerEndpoints.tokenEndpoint("fake-tenant-id"), azureClientRegistration.getProviderDetails().getTokenUri() ); assertEquals( - identityEndpoints.jwkSetEndpoint("fake-tenant-id"), + authorizationServerEndpoints.jwkSetEndpoint("fake-tenant-id"), azureClientRegistration.getProviderDetails().getJwkSetUri() ); assertEquals( @@ -58,7 +58,10 @@ public void clientRegistered() { @Test public void clientRequiresPermissionRegistered() { try (AppRunner appRunner = createApp()) { - appRunner.property("azure.activedirectory.authorization.graph.scope", "Calendars.Read"); + appRunner.property( + "azure.activedirectory.authorization.graph.scope", + "https://graph.microsoft.com/Calendars.Read" + ); appRunner.start(); ClientRegistrationRepository clientRegistrationRepository = @@ -67,17 +70,23 @@ public void clientRequiresPermissionRegistered() { ClientRegistration graphClientRegistration = clientRegistrationRepository.findByRegistrationId("graph"); assertNotNull(azureClientRegistration); - assertDefaultScopes(azureClientRegistration, "openid", "profile", "offline_access", "Calendars.Read"); + assertDefaultScopes( + azureClientRegistration, + "openid", "profile", "offline_access", "https://graph.microsoft.com/Calendars.Read" + ); assertNotNull(graphClientRegistration); - assertDefaultScopes(graphClientRegistration, "Calendars.Read"); + assertDefaultScopes(graphClientRegistration, "https://graph.microsoft.com/Calendars.Read"); } } @Test public void clientRequiresMultiPermissions() { try (AppRunner appRunner = createApp()) { - appRunner.property("azure.activedirectory.authorization.graph.scope", "Calendars.Read"); + appRunner.property( + "azure.activedirectory.authorization.graph.scope", + "https://graph.microsoft.com/Calendars.Read" + ); appRunner.property( "azure.activedirectory.authorization.arm.scope", "https://management.core.windows.net/user_impersonation" @@ -95,19 +104,22 @@ public void clientRequiresMultiPermissions() { "openid", "profile", "offline_access", - "Calendars.Read", + "https://graph.microsoft.com/Calendars.Read", "https://management.core.windows.net/user_impersonation" ); assertNotNull(graphClientRegistration); - assertDefaultScopes(graphClientRegistration, "Calendars.Read"); + assertDefaultScopes(graphClientRegistration, "https://graph.microsoft.com/Calendars.Read"); } } @Test public void clientRequiresPermissionInDefaultClient() { try (AppRunner appRunner = createApp()) { - appRunner.property("azure.activedirectory.authorization.azure.scope", "Calendars.Read"); + appRunner.property( + "azure.activedirectory.authorization.azure.scope", + "https://graph.microsoft.com/Calendars.Read" + ); appRunner.start(); ClientRegistrationRepository clientRegistrationRepository = @@ -117,7 +129,7 @@ public void clientRequiresPermissionInDefaultClient() { assertNotNull(azureClientRegistration); assertDefaultScopes( azureClientRegistration, - "openid", "profile", "offline_access", "Calendars.Read" + "openid", "profile", "offline_access", "https://graph.microsoft.com/Calendars.Read" ); } } @@ -125,7 +137,10 @@ public void clientRequiresPermissionInDefaultClient() { @Test public void aadAwareClientRepository() { try (AppRunner appRunner = createApp()) { - appRunner.property("azure.activedirectory.authorization.graph.scope", "Calendars.Read"); + appRunner.property( + "azure.activedirectory.authorization.graph.scope", + "https://graph.microsoft.com/Calendars.Read") + ; appRunner.start(); AzureClientRegistrationRepository azureClientRegistrationRepository = @@ -155,22 +170,25 @@ public void aadAwareClientRepository() { @Test public void defaultClientWithAuthzScope() { try (AppRunner appRunner = createApp()) { - appRunner.property("azure.activedirectory.authorization.azure.scope", "Calendars.Read"); + appRunner.property( + "azure.activedirectory.authorization.azure.scope", + "https://graph.microsoft.com/Calendars.Read" + ); appRunner.start(); AzureClientRegistrationRepository azureClientRegistrationRepository = appRunner.getBean(AzureClientRegistrationRepository.class); assertDefaultScopes( azureClientRegistrationRepository.defaultClient(), - "openid", "profile", "offline_access", "Calendars.Read" + "openid", "profile", "offline_access", "https://graph.microsoft.com/Calendars.Read" ); } } @Test - public void customizeUri() { + public void customizeEnvironment() { try (AppRunner appRunner = createApp()) { - appRunner.property("azure.activedirectory.uri", "http://localhost/"); + appRunner.property("azure.activedirectory.environment", "cn-v2-graph"); appRunner.start(); AzureClientRegistrationRepository azureClientRegistrationRepository = @@ -178,17 +196,18 @@ public void customizeUri() { ClientRegistration azureClientRegistration = azureClientRegistrationRepository.findByRegistrationId("azure"); - IdentityEndpoints endpoints = new IdentityEndpoints("http://localhost/"); + AuthorizationServerEndpoints authorizationServerEndpoints = + new AuthorizationServerEndpoints("https://login.partner.microsoftonline.cn"); assertEquals( - endpoints.authorizationEndpoint("fake-tenant-id"), + authorizationServerEndpoints.authorizationEndpoint("fake-tenant-id"), azureClientRegistration.getProviderDetails().getAuthorizationUri() ); assertEquals( - endpoints.tokenEndpoint("fake-tenant-id"), + authorizationServerEndpoints.tokenEndpoint("fake-tenant-id"), azureClientRegistration.getProviderDetails().getTokenUri() ); assertEquals( - endpoints.jwkSetEndpoint("fake-tenant-id"), + authorizationServerEndpoints.jwkSetEndpoint("fake-tenant-id"), azureClientRegistration.getProviderDetails().getJwkSetUri() ); } diff --git a/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AuthorizedClientRepoTest.java b/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AuthorizedClientRepoTest.java index 6450c5d9bef7..e2c3fe29d060 100644 --- a/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AuthorizedClientRepoTest.java +++ b/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AuthorizedClientRepoTest.java @@ -1,7 +1,7 @@ package com.azure.test.aad.auth; -import com.azure.spring.aad.implementation.AzureClientRegistrationRepository; -import com.azure.spring.aad.implementation.AzureOAuth2AuthorizedClientRepository; +import com.azure.spring.autoconfigure.aad.AzureClientRegistrationRepository; +import com.azure.spring.autoconfigure.aad.AzureOAuth2AuthorizedClientRepository; import com.azure.test.utils.AppRunner; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; diff --git a/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AuthzCodeGrantRequestEntityConverterTest.java b/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AuthzCodeGrantRequestEntityConverterTest.java index 091bbd2cca0a..c7f16e2377ee 100644 --- a/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AuthzCodeGrantRequestEntityConverterTest.java +++ b/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AuthzCodeGrantRequestEntityConverterTest.java @@ -1,7 +1,7 @@ package com.azure.test.aad.auth; -import com.azure.spring.aad.implementation.AzureClientRegistrationRepository; -import com.azure.spring.aad.implementation.AzureOAuth2AuthorizationCodeGrantRequestEntityConverter; +import com.azure.spring.autoconfigure.aad.AzureClientRegistrationRepository; +import com.azure.spring.autoconfigure.aad.AzureOAuth2AuthorizationCodeGrantRequestEntityConverter; import com.azure.test.utils.AppRunner; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; diff --git a/sdk/spring/azure-spring-boot/CHANGELOG.md b/sdk/spring/azure-spring-boot/CHANGELOG.md index 48d0a844038b..2fe161c150fb 100644 --- a/sdk/spring/azure-spring-boot/CHANGELOG.md +++ b/sdk/spring/azure-spring-boot/CHANGELOG.md @@ -2,6 +2,7 @@ ## 3.0.0-beta.1 (Unreleased) ### Breaking Changes +- Conditional access policy is not supported temporary, we may recover it in the future. - Configuration items like `spring.security.oauth2.client.xxx` is not supported anymore. Please use the following configuration items instead: ``` azure.activedirectory.tenant-id=xxxxxx-your-tenant-id-xxxxxx diff --git a/sdk/spring/azure-spring-boot/pom.xml b/sdk/spring/azure-spring-boot/pom.xml index ae5d378ace43..1982181f09f1 100644 --- a/sdk/spring/azure-spring-boot/pom.xml +++ b/sdk/spring/azure-spring-boot/pom.xml @@ -105,6 +105,18 @@ 4.0.1 true + + org.springframework.boot + spring-boot-starter-webflux + 2.3.5.RELEASE + true + + + io.projectreactor.netty + reactor-netty + 0.9.13.RELEASE + true + @@ -140,6 +152,24 @@ qpid-jms-client 0.53.0 true + + + io.netty + netty-common + + + io.netty + netty-buffer + + + io.netty + netty-transport + + + io.netty + netty-transport-native-kqueue + + @@ -154,6 +184,12 @@ azure-identity 1.2.0 true + + + io.netty + netty-transport-native-kqueue + + @@ -296,12 +332,14 @@ org.springframework.boot:spring-boot-configuration-processor:[2.3.5.RELEASE] org.springframework.boot:spring-boot-starter-test:[2.3.5.RELEASE] org.springframework.boot:spring-boot-starter-web:[2.3.5.RELEASE] + org.springframework.boot:spring-boot-starter-webflux:[2.3.5.RELEASE] org.springframework.security:spring-security-config:[5.3.5.RELEASE] org.springframework.security:spring-security-core:[5.3.5.RELEASE] org.springframework.security:spring-security-oauth2-client:[5.3.5.RELEASE] org.springframework.security:spring-security-oauth2-jose:[5.3.5.RELEASE] org.springframework.security:spring-security-web:[5.3.5.RELEASE] org.apache.qpid:qpid-jms-client:[0.53.0] + io.projectreactor.netty:reactor-netty:[0.9.13.RELEASE] diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADAuthenticationFailureHandler.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADAuthenticationFailureHandler.java deleted file mode 100644 index 6bab923cedb8..000000000000 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADAuthenticationFailureHandler.java +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.spring.autoconfigure.aad; - -import com.microsoft.aad.msal4j.MsalServiceException; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.web.authentication.AuthenticationFailureHandler; -import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; -import org.springframework.security.web.savedrequest.DefaultSavedRequest; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.Optional; - -/** - * Strategy used to handle a failed authentication attempt. - *

- * To redirect the user to the authentication page to allow them to try again when conditional access policy is - * configured on Azure Active Directory. - */ -public class AADAuthenticationFailureHandler implements AuthenticationFailureHandler { - private static final String DEFAULT_FAILURE_URL = "/login?error"; - private final AuthenticationFailureHandler defaultHandler; - - public AADAuthenticationFailureHandler() { - this.defaultHandler = new SimpleUrlAuthenticationFailureHandler(DEFAULT_FAILURE_URL); - } - - @Override - public void onAuthenticationFailure(HttpServletRequest request, - HttpServletResponse response, - AuthenticationException exception) throws IOException, ServletException { - // Handle conditional access policy, step 3. - MsalServiceException msalServiceException = (MsalServiceException) - Optional.of(exception) - .filter(e -> e instanceof OAuth2AuthenticationException) - .map(e -> (OAuth2AuthenticationException) e) - .filter(e -> AADOAuth2ErrorCode.CONDITIONAL_ACCESS_POLICY.equals((e.getError().getErrorCode()))) - .map(Throwable::getCause) - .filter(cause -> cause instanceof MsalServiceException) - .orElse(null); - if (msalServiceException == null) { - // Default handle logic - defaultHandler.onAuthenticationFailure(request, response, exception); - } else { - // Put claims into session - Optional.of(msalServiceException) - .map(MsalServiceException::claims) - .ifPresent(claims -> request.getSession() - .setAttribute(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, claims)); - // Redirect - response.setStatus(302); - String redirectUrl = Optional.of(request) - .map(HttpServletRequest::getSession) - .map(s -> s.getAttribute(Constants.SAVED_REQUEST)) - .map(r -> (DefaultSavedRequest) r) - .map(DefaultSavedRequest::getRedirectUrl) - .orElse(null); - response.sendRedirect(redirectUrl); - } - } -} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADAuthenticationFilter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADAuthenticationFilter.java index cb4733b03911..b110c366cf10 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADAuthenticationFilter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADAuthenticationFilter.java @@ -42,7 +42,7 @@ public class AADAuthenticationFilter extends OncePerRequestFilter { private static final String CURRENT_USER_PRINCIPAL = "CURRENT_USER_PRINCIPAL"; private final UserPrincipalManager userPrincipalManager; - private final AzureADGraphClient azureADGraphClient; + private final GraphOboClient graphOboClient; public AADAuthenticationFilter(AADAuthenticationProperties aadAuthenticationProperties, ServiceEndpointsProperties serviceEndpointsProperties, @@ -80,16 +80,18 @@ public AADAuthenticationFilter(AADAuthenticationProperties aadAuthenticationProp ServiceEndpointsProperties serviceEndpointsProperties, UserPrincipalManager userPrincipalManager) { this.userPrincipalManager = userPrincipalManager; - this.azureADGraphClient = new AzureADGraphClient( + this.graphOboClient = new GraphOboClient( aadAuthenticationProperties, serviceEndpointsProperties ); } @Override - protected void doFilterInternal(HttpServletRequest httpServletRequest, - HttpServletResponse httpServletResponse, - FilterChain filterChain) throws ServletException, IOException { + 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) @@ -106,21 +108,19 @@ protected void doFilterInternal(HttpServletRequest httpServletRequest, 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 + String accessTokenForGraphApi = graphOboClient .acquireTokenForGraphApi(aadIssuedBearerToken, tenantId) .accessToken(); - userPrincipal.setAccessTokenForGraphApi(accessTokenForGraphApi); - userPrincipal.setGroups(azureADGraphClient.getGroups(accessTokenForGraphApi)); + userPrincipal.setGroups(graphOboClient.getGroups(accessTokenForGraphApi)); httpSession.setAttribute(CURRENT_USER_PRINCIPAL, userPrincipal); } final Authentication authentication = new PreAuthenticatedAuthenticationToken( userPrincipal, null, - azureADGraphClient.toGrantedAuthoritySet(userPrincipal.getGroups()) + graphOboClient.toGrantedAuthoritySet(userPrincipal.getGroups()) ); LOGGER.info("Request token verification success. {}", authentication); SecurityContextHolder.getContext().setAuthentication(authentication); @@ -137,7 +137,6 @@ protected void doFilterInternal(HttpServletRequest httpServletRequest, } 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 { diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java index f1dbcd56948f..572b15affb5e 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java @@ -3,7 +3,6 @@ package com.azure.spring.autoconfigure.aad; -import com.azure.spring.aad.implementation.AuthorizationProperties; import com.nimbusds.jose.jwk.source.RemoteJWKSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,8 +35,6 @@ public class AADAuthenticationProperties { private static final String GROUP_RELATIONSHIP_DIRECT = "direct"; private static final String GROUP_RELATIONSHIP_TRANSITIVE = "transitive"; - private String uri; - private Map authorization = new HashMap<>(); /** @@ -269,14 +266,6 @@ public void validateUserGroupProperties() { } } - public void setUri(String uri) { - this.uri = uri; - } - - public String getUri() { - return uri; - } - public void setAuthorization(Map authorization) { this.authorization = authorization; } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADOAuth2ErrorCode.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADOAuth2ErrorCode.java deleted file mode 100644 index aaa9b557acf9..000000000000 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADOAuth2ErrorCode.java +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.spring.autoconfigure.aad; - -/** - * entity class of AADOAuth2ErrorCode - */ -public class AADOAuth2ErrorCode { - public static final String CONDITIONAL_ACCESS_POLICY = "conditional_access_policy"; - public static final String INVALID_REQUEST = "invalid_request"; - public static final String SERVER_SERVER = "server_error"; -} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADOAuth2UserService.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADOAuth2UserService.java index 70c4e67bf87e..70edf71aa5fe 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADOAuth2UserService.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADOAuth2UserService.java @@ -3,7 +3,6 @@ package com.azure.spring.autoconfigure.aad; -import com.microsoft.aad.msal4j.MsalServiceException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; @@ -12,32 +11,24 @@ import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import javax.naming.ServiceUnavailableException; -import java.io.IOException; -import java.net.MalformedURLException; import java.util.Optional; import java.util.Set; -import static com.azure.spring.autoconfigure.aad.AADOAuth2ErrorCode.CONDITIONAL_ACCESS_POLICY; -import static com.azure.spring.autoconfigure.aad.AADOAuth2ErrorCode.INVALID_REQUEST; -import static com.azure.spring.autoconfigure.aad.AADOAuth2ErrorCode.SERVER_SERVER; - /** - * This implementation will retrieve group info of user from Microsoft Graph and map groups to {@link GrantedAuthority}. + * This implementation will retrieve group info of user from Microsoft Graph and map groups to {@link + * GrantedAuthority}. */ public class AADOAuth2UserService implements OAuth2UserService { - private final AADAuthenticationProperties aadAuthenticationProperties; - private final ServiceEndpointsProperties serviceEndpointsProperties; private final OidcUserService oidcUserService; + private final GraphWebClient graphWebClient; - public AADOAuth2UserService(AADAuthenticationProperties aadAuthenticationProperties, - ServiceEndpointsProperties serviceEndpointsProperties) { - this.aadAuthenticationProperties = aadAuthenticationProperties; - this.serviceEndpointsProperties = serviceEndpointsProperties; + public AADOAuth2UserService( + GraphWebClient graphWebClient + ) { + this.graphWebClient = graphWebClient; this.oidcUserService = new OidcUserService(); } @@ -45,51 +36,16 @@ public AADOAuth2UserService(AADAuthenticationProperties aadAuthenticationPropert public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { // Delegate to the default implementation for loading a user OidcUser oidcUser = oidcUserService.loadUser(userRequest); - final Set mappedAuthorities; - try { - // https://github.com/MicrosoftDocs/azure-docs/issues/8121#issuecomment-387090099 - // In AAD App Registration configure oauth2AllowImplicitFlow to true - final AzureADGraphClient azureADGraphClient = new AzureADGraphClient( - aadAuthenticationProperties, - serviceEndpointsProperties - ); - String graphApiToken = azureADGraphClient - .acquireTokenForGraphApi( - userRequest.getIdToken().getTokenValue(), - aadAuthenticationProperties.getTenantId() - ) - .accessToken(); - mappedAuthorities = azureADGraphClient.getGrantedAuthorities(graphApiToken); - } catch (MalformedURLException e) { - throw toOAuth2AuthenticationException(INVALID_REQUEST, "Failed to acquire token for Graph API.", e); - } catch (ServiceUnavailableException e) { - throw toOAuth2AuthenticationException(SERVER_SERVER, "Failed to acquire token for Graph API.", e); - } catch (IOException e) { - throw toOAuth2AuthenticationException(SERVER_SERVER, "Failed to map group to authorities.", e); - } catch (MsalServiceException e) { - // Handle conditional access policy, step 2. - // OAuth2AuthenticationException will be caught by AADAuthenticationFailureHandler. - if (e.claims() != null && !e.claims().isEmpty()) { - throw toOAuth2AuthenticationException(CONDITIONAL_ACCESS_POLICY, "Handle conditional access policy", e); - } else { - throw e; - } - } - String nameAttributeKey = Optional.of(userRequest) - .map(OAuth2UserRequest::getClientRegistration) - .map(ClientRegistration::getProviderDetails) - .map(ClientRegistration.ProviderDetails::getUserInfoEndpoint) - .map(ClientRegistration.ProviderDetails.UserInfoEndpoint::getUserNameAttributeName) - .filter(s -> !s.isEmpty()) - .orElse(AADTokenClaim.NAME); + final Set mappedAuthorities = graphWebClient.getGrantedAuthorities(); + String nameAttributeKey = + Optional.of(userRequest) + .map(OAuth2UserRequest::getClientRegistration) + .map(ClientRegistration::getProviderDetails) + .map(ClientRegistration.ProviderDetails::getUserInfoEndpoint) + .map(ClientRegistration.ProviderDetails.UserInfoEndpoint::getUserNameAttributeName) + .filter(s -> !s.isEmpty()) + .orElse(AADTokenClaim.NAME); // Create a copy of oidcUser but use the mappedAuthorities instead return new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), nameAttributeKey); } - - private OAuth2AuthenticationException toOAuth2AuthenticationException(String errorCode, - String description, - Exception cause) { - OAuth2Error oAuth2Error = new OAuth2Error(errorCode, description, null); - return new OAuth2AuthenticationException(oAuth2Error, cause); - } } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AuthorizationProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AuthorizationProperties.java similarity index 83% rename from sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AuthorizationProperties.java rename to sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AuthorizationProperties.java index 7ee16ec92fb7..d16dbf9be56b 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AuthorizationProperties.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AuthorizationProperties.java @@ -1,11 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.aad.implementation; +package com.azure.spring.autoconfigure.aad; import java.util.Arrays; import java.util.List; +/** + * Properties for a authorized client. + */ public class AuthorizationProperties { private String[] scope = new String[0]; diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/IdentityEndpoints.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AuthorizationServerEndpoints.java similarity index 58% rename from sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/IdentityEndpoints.java rename to sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AuthorizationServerEndpoints.java index 81c98dbed9f4..65981c98bab2 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/IdentityEndpoints.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AuthorizationServerEndpoints.java @@ -1,28 +1,31 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.aad.implementation; +package com.azure.spring.autoconfigure.aad; import com.nimbusds.oauth2.sdk.util.StringUtils; -public class IdentityEndpoints { +/** + * Util class used to create authorization server endpoints. + */ +public class AuthorizationServerEndpoints { - private static final String IDENTITY_PLATFORM = "https://login.microsoftonline.com/"; + private static final String DEFAULT_AUTHORIZATION_SERVER_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 final String baseUri; - public IdentityEndpoints() { - this(IDENTITY_PLATFORM); + public AuthorizationServerEndpoints() { + this(DEFAULT_AUTHORIZATION_SERVER_URI); } - public IdentityEndpoints(String baseUri) { - if (StringUtils.isBlank(baseUri)) { - baseUri = IDENTITY_PLATFORM; + public AuthorizationServerEndpoints(String authorizationServerUri) { + if (StringUtils.isBlank(authorizationServerUri)) { + authorizationServerUri = DEFAULT_AUTHORIZATION_SERVER_URI; } - this.baseUri = addSlash(baseUri); + this.baseUri = addSlash(authorizationServerUri); } private String addSlash(String uri) { diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureActiveDirectoryAutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureActiveDirectoryAutoConfiguration.java similarity index 75% rename from sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureActiveDirectoryAutoConfiguration.java rename to sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureActiveDirectoryAutoConfiguration.java index e4ec1ddf1251..6b158bafbd0a 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureActiveDirectoryAutoConfiguration.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureActiveDirectoryAutoConfiguration.java @@ -1,11 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.aad.implementation; +package com.azure.spring.autoconfigure.aad; -import com.azure.spring.autoconfigure.aad.AADAuthenticationProperties; -import com.azure.spring.autoconfigure.aad.AADOAuth2UserService; -import com.azure.spring.autoconfigure.aad.ServiceEndpointsProperties; import com.azure.spring.telemetry.TelemetrySender; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -20,14 +17,18 @@ 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.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; 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.DefaultOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.util.ClassUtils; +import org.springframework.web.reactive.function.client.WebClient; import javax.annotation.PostConstruct; import java.util.ArrayList; @@ -38,10 +39,13 @@ import static com.azure.spring.telemetry.TelemetryData.SERVICE_NAME; import static com.azure.spring.telemetry.TelemetryData.getClassPackageSimpleName; +/** + * Provide necessary beans used for AAD authentication and authorization. + */ @Configuration +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @ConditionalOnResource(resources = "classpath:aad.enable.config") @ConditionalOnClass(ClientRegistrationRepository.class) -@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @ConditionalOnProperty(prefix = "azure.activedirectory", value = {"client-id", "client-secret", "tenant-id"}) @PropertySource(value = "classpath:service-endpoints.properties") @EnableConfigurationProperties({ AADAuthenticationProperties.class, ServiceEndpointsProperties.class }) @@ -54,12 +58,6 @@ public class AzureActiveDirectoryAutoConfiguration { @Autowired private ServiceEndpointsProperties serviceEndpointsProperties; - @Bean - @ConditionalOnProperty(prefix = "azure.activedirectory.user-group", value = "allowed-groups") - public OAuth2UserService oidcUserService() { - return new AADOAuth2UserService(aadAuthenticationProperties, serviceEndpointsProperties); - } - @Bean @ConditionalOnMissingBean({ ClientRegistrationRepository.class, AzureClientRegistrationRepository.class }) public AzureClientRegistrationRepository clientRegistrationRepository() { @@ -69,6 +67,45 @@ public AzureClientRegistrationRepository clientRegistrationRepository() { ); } + @Bean + @ConditionalOnMissingBean + public OAuth2AuthorizedClientRepository authorizedClientRepository(AzureClientRegistrationRepository repo) { + return new AzureOAuth2AuthorizedClientRepository(repo); + } + + @Bean + @ConditionalOnMissingBean + WebClient webClient( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository + ) { + OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager = new DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, + oAuth2AuthorizedClientRepository + ); + ServletOAuth2AuthorizedClientExchangeFilterFunction servletOAuth2AuthorizedClientExchangeFilterFunction = + new ServletOAuth2AuthorizedClientExchangeFilterFunction(oAuth2AuthorizedClientManager); + return WebClient.builder() + .apply(servletOAuth2AuthorizedClientExchangeFilterFunction.oauth2Configuration()) + .build(); + } + + @Bean + @ConditionalOnMissingBean + GraphWebClient graphWebClient(WebClient webClient) { + return new GraphWebClient( + aadAuthenticationProperties, + serviceEndpointsProperties, + webClient + ); + } + + @Bean + @ConditionalOnProperty(prefix = "azure.activedirectory.user-group", value = "allowed-groups") + public OAuth2UserService oidcUserService(GraphWebClient graphWebClient) { + return new AADOAuth2UserService(graphWebClient); + } + private DefaultClient createDefaultClient() { ClientRegistration clientRegistration = toClientRegistrationBuilder(DEFAULT_CLIENT) .scope(allScopes()) @@ -124,7 +161,10 @@ private ClientRegistration toClientRegistration(String id, AuthorizationProperti } private ClientRegistration.Builder toClientRegistrationBuilder(String registrationId) { - IdentityEndpoints endpoints = new IdentityEndpoints(aadAuthenticationProperties.getUri()); + String authorizationServerUri = + serviceEndpointsProperties.getServiceEndpoints(aadAuthenticationProperties.getEnvironment()) + .getAadSigninUri(); + AuthorizationServerEndpoints endpoints = new AuthorizationServerEndpoints(authorizationServerUri); String tenantId = aadAuthenticationProperties.getTenantId(); return ClientRegistration.withRegistrationId(registrationId) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) @@ -136,12 +176,15 @@ private ClientRegistration.Builder toClientRegistrationBuilder(String registrati .jwkSetUri(endpoints.jwkSetEndpoint(tenantId)); } - @Bean - @ConditionalOnMissingBean - public OAuth2AuthorizedClientRepository authorizedClientRepository(AzureClientRegistrationRepository repo) { - return new AzureOAuth2AuthorizedClientRepository(repo); - } - + /** + * Default configuration class for using AAD authentication and authorization. + * + * User can write another configuration bean to override it. + * If user write another configuration bean, to make sure `AzureOAuth2AuthorizationCodeGrantRequestEntityConverter` + * take effect, please: + * 1. Extends AzureOAuth2WebSecurityConfigurerAdapter instead of WebSecurityConfigurerAdapter. + * 2. Call `super.configure(http)` in your configure() + */ @Configuration @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) @EnableWebSecurity diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureClientRegistrationRepository.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureClientRegistrationRepository.java similarity index 94% rename from sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureClientRegistrationRepository.java rename to sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureClientRegistrationRepository.java index 7af598850c78..47b2dec6b166 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureClientRegistrationRepository.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureClientRegistrationRepository.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.aad.implementation; +package com.azure.spring.autoconfigure.aad; import org.jetbrains.annotations.NotNull; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -15,6 +15,9 @@ import java.util.Map; import java.util.Optional; +/** + * A ClientRegistrationRepository that manage all AAD's ClientRegistrations. + */ public class AzureClientRegistrationRepository implements ClientRegistrationRepository, Iterable { private final DefaultClient defaultClient; diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureOAuth2AuthorizationCodeGrantRequestEntityConverter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureOAuth2AuthorizationCodeGrantRequestEntityConverter.java similarity index 70% rename from sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureOAuth2AuthorizationCodeGrantRequestEntityConverter.java rename to sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureOAuth2AuthorizationCodeGrantRequestEntityConverter.java index 3609681c1d0e..15f35fc00aeb 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureOAuth2AuthorizationCodeGrantRequestEntityConverter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureOAuth2AuthorizationCodeGrantRequestEntityConverter.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.aad.implementation; +package com.azure.spring.autoconfigure.aad; import org.springframework.http.HttpEntity; import org.springframework.http.RequestEntity; @@ -11,6 +11,19 @@ import java.util.Optional; +/** + * This converter is to add 'scope' parameter when request for access token. + * + * In default oidc flow, when request for access token by authorization code, 'scope' parameter is not necessary. + * Because one consent operation only create one authorizedClient. + * + * But for Microsoft Authorization Server, one consent can created multiple authorizedClient. + * So scope parameter is necessary when request for access token. + * + * Refs: + * 1. https://tools.ietf.org/html/rfc6749#section-4.1.3 + * 2. https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-access-token + */ public class AzureOAuth2AuthorizationCodeGrantRequestEntityConverter extends OAuth2AuthorizationCodeGrantRequestEntityConverter { diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureOAuth2AuthorizedClientRepository.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureOAuth2AuthorizedClientRepository.java similarity index 97% rename from sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureOAuth2AuthorizedClientRepository.java rename to sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureOAuth2AuthorizedClientRepository.java index 22aadf5ce133..c850e1e7a156 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureOAuth2AuthorizedClientRepository.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureOAuth2AuthorizedClientRepository.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.aad.implementation; +package com.azure.spring.autoconfigure.aad; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; @@ -16,6 +16,9 @@ import java.time.temporal.ChronoUnit; import java.util.Optional; +/** + * An OAuth2AuthorizedClientRepository that manage all AAD authorizedClients. + */ public class AzureOAuth2AuthorizedClientRepository implements OAuth2AuthorizedClientRepository { private final AzureClientRegistrationRepository azureClientRegistrationRepository; diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureOAuth2WebSecurityConfigurerAdapter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureOAuth2WebSecurityConfigurerAdapter.java similarity index 90% rename from sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureOAuth2WebSecurityConfigurerAdapter.java rename to sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureOAuth2WebSecurityConfigurerAdapter.java index 77127de6b20f..c2a016e8b072 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureOAuth2WebSecurityConfigurerAdapter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureOAuth2WebSecurityConfigurerAdapter.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.aad.implementation; +package com.azure.spring.autoconfigure.aad; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -12,6 +12,9 @@ import java.util.Optional; +/** + * The main purpose of this class is to make AzureOAuth2AuthorizationCodeGrantRequestEntityConverter take effect. + */ public abstract class AzureOAuth2WebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { @Autowired diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/Constants.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/Constants.java index 87be1cd2f4e2..289b3ebd679f 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/Constants.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/Constants.java @@ -18,7 +18,6 @@ public class Constants { public static final String CLAIMS = "claims"; public static final Set DEFAULT_AUTHORITY_SET; public static final String ROLE_PREFIX = "ROLE_"; - public static final String SAVED_REQUEST = "SPRING_SECURITY_SAVED_REQUEST"; static { Set authoritySet = new HashSet<>(); diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/DefaultClient.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/DefaultClient.java similarity index 73% rename from sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/DefaultClient.java rename to sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/DefaultClient.java index 3c552b77f699..084c83dda4a6 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/DefaultClient.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/DefaultClient.java @@ -1,13 +1,18 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.aad.implementation; +package com.azure.spring.autoconfigure.aad; import org.springframework.security.oauth2.client.registration.ClientRegistration; import java.util.Arrays; import java.util.List; +/** + * The default client after user consent on microsoft login page. + * DefaultClient.clientRegistration.scopes contains all scopes consented in the login page. + * DefaultClient.scope only contains the scopes for defaultClientRegistration. + */ public class DefaultClient { private final ClientRegistration clientRegistration; diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureADGraphClient.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/GraphOboClient.java similarity index 97% rename from sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureADGraphClient.java rename to sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/GraphOboClient.java index e80a2d5668dc..f36a1293286a 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureADGraphClient.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/GraphOboClient.java @@ -42,9 +42,9 @@ /** * Microsoft Graph client encapsulation. */ -public class AzureADGraphClient { +public class GraphOboClient { - private static final Logger LOGGER = LoggerFactory.getLogger(AzureADGraphClient.class); + private static final Logger LOGGER = LoggerFactory.getLogger(GraphOboClient.class); private static final String MICROSOFT_GRAPH_SCOPE = "https://graph.microsoft.com/user.read"; private static final String AAD_GRAPH_API_SCOPE = "https://graph.windows.net/user.read"; // We use "aadfeed5" as suffix when client library is ADAL, upgrade to "aadfeed6" for MSAL @@ -55,8 +55,8 @@ public class AzureADGraphClient { private final AADAuthenticationProperties aadAuthenticationProperties; private final boolean graphApiVersionIsV2; - public AzureADGraphClient(AADAuthenticationProperties aadAuthenticationProperties, - ServiceEndpointsProperties serviceEndpointsProps) { + public GraphOboClient(AADAuthenticationProperties aadAuthenticationProperties, + ServiceEndpointsProperties serviceEndpointsProps) { this.aadAuthenticationProperties = aadAuthenticationProperties; this.serviceEndpoints = serviceEndpointsProps.getServiceEndpoints(aadAuthenticationProperties.getEnvironment()); this.graphApiVersionIsV2 = Optional.of(aadAuthenticationProperties) diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/GraphWebClient.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/GraphWebClient.java new file mode 100644 index 000000000000..5dc64db581aa --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/GraphWebClient.java @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.autoconfigure.aad; + +import com.fasterxml.jackson.core.JsonProcessingException; +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.MediaType; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.azure.spring.autoconfigure.aad.Constants.DEFAULT_AUTHORITY_SET; +import static com.azure.spring.autoconfigure.aad.Constants.ROLE_PREFIX; +import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId; + + +/** + * Microsoft Graph web client implemented by OAuth2 WebClient. + */ +public class GraphWebClient { + private static final Logger LOGGER = LoggerFactory.getLogger(GraphWebClient.class); + + private final ServiceEndpoints serviceEndpoints; + private final AADAuthenticationProperties aadAuthenticationProperties; + private final boolean graphApiVersionIsV2; + private final WebClient webClient; + + public GraphWebClient( + AADAuthenticationProperties aadAuthenticationProperties, + ServiceEndpointsProperties serviceEndpointsProps, + WebClient webClient + ) { + this.aadAuthenticationProperties = aadAuthenticationProperties; + this.serviceEndpoints = serviceEndpointsProps.getServiceEndpoints(aadAuthenticationProperties.getEnvironment()); + this.webClient = webClient; + this.graphApiVersionIsV2 = Optional.of(aadAuthenticationProperties) + .map(AADAuthenticationProperties::getEnvironment) + .map(environment -> environment.contains("v2-graph")) + .orElse(false); + } + + public Set getGrantedAuthorities() { + return toGrantedAuthoritySet(getGroupsFromGraphApi()); + } + + public Set getGroupsFromGraphApi() { + final ObjectMapper objectMapper = JacksonObjectMapperFactory.getInstance(); + Set groups = new LinkedHashSet<>(); + String aadMembershipRestUri = getAadMembershipRestUri(); + while (aadMembershipRestUri != null) { + String membershipsJson = getUserMembershipsJson(aadMembershipRestUri); + Memberships memberships; + try { + memberships = objectMapper.readValue(membershipsJson, Memberships.class); + } catch (JsonProcessingException e) { + LOGGER.error("Can not get groups.", e); + return Collections.emptySet(); + } + groups = memberships.getValue() + .stream() + .filter(this::isGroupObject) + .map(Membership::getDisplayName) + .collect(Collectors.toSet()); + aadMembershipRestUri = Optional.of(memberships) + .map(Memberships::getOdataNextLink) + .map(this::getUrlStringFromODataNextLink) + .orElse(null); + } + return groups; + } + + private String getUserMembershipsJson(String urlString) { + String responseInJson; + if (graphApiVersionIsV2) { + responseInJson = webClient + .get() + .uri(urlString) + .attributes(clientRegistrationId("graph")) + .accept(MediaType.APPLICATION_JSON) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .retrieve() + .bodyToMono(String.class) + .block(); + } else { + responseInJson = webClient + .get() + .uri(urlString) + .attributes(clientRegistrationId("graph")) + .header(HttpHeaders.ACCEPT, "application/json;odata=minimalmetadata") + .header("api-version", "1.6") + .retrieve() + .bodyToMono(String.class) + .block(); + } + if (responseInJson == null || responseInJson.isEmpty()) { + throw new IllegalStateException( + "Response is not " + HTTPResponse.SC_OK + ", response json: " + responseInJson); + } + return responseInJson; + } + + private String getUrlStringFromODataNextLink(String odataNextLink) { + if (this.graphApiVersionIsV2) { + return odataNextLink; + } else { + String skipToken = odataNextLink.split("/memberOf\\?")[1]; + return serviceEndpoints.getAadMembershipRestUri() + "&" + skipToken; + } + } + + private String getAadMembershipRestUri() { + if (AADAuthenticationProperties.getDirectGroupRelationship() + .equalsIgnoreCase( + aadAuthenticationProperties.getUserGroup().getGroupRelationship() + ) + ) { + return serviceEndpoints.getAadMembershipRestUri(); + } else { + return serviceEndpoints.getAadTransitiveMemberRestUri(); + } + } + + private boolean isGroupObject(final Membership membership) { + return membership.getObjectType().equals(aadAuthenticationProperties.getUserGroup().getValue()); + } + + public Set toGrantedAuthoritySet(final Set groups) { + Set grantedAuthoritySet = + groups.stream() + .filter(aadAuthenticationProperties::isAllowedGroup) + .map(group -> new SimpleGrantedAuthority(ROLE_PREFIX + group)) + .collect(Collectors.toSet()); + return Optional.of(grantedAuthoritySet) + .filter(g -> !g.isEmpty()) + .orElse(DEFAULT_AUTHORITY_SET); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/JacksonObjectMapperFactory.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/JacksonObjectMapperFactory.java index 059d3fcf4968..bee1a10331ed 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/JacksonObjectMapperFactory.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/JacksonObjectMapperFactory.java @@ -6,7 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; /** - * factoty class of JacksonObjectMapper + * Factory class of JacksonObjectMapper */ public final class JacksonObjectMapperFactory { diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/UserPrincipal.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/UserPrincipal.java index fea3c0d87d12..f15bf599eb0f 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/UserPrincipal.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/UserPrincipal.java @@ -66,14 +66,6 @@ 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) diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/UserPrincipalManager.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/UserPrincipalManager.java index 7e76c6276e98..ab2120024ce0 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/UserPrincipalManager.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/UserPrincipalManager.java @@ -163,6 +163,13 @@ public UserPrincipal buildUserPrincipal(String aadIssuedBearerToken) throws Pars } public boolean isTokenIssuedByAAD(String token) { + return staticIsTokenIssuedByAAD(token); + } + + public static boolean staticIsTokenIssuedByAAD(String token) { + if (token == null) { + return false; + } try { final JWT jwt = JWTParser.parse(token); return isAADIssuer(jwt.getJWTClaimsSet().getIssuer()); diff --git a/sdk/spring/azure-spring-boot/src/main/resources/META-INF/spring.factories b/sdk/spring/azure-spring-boot/src/main/resources/META-INF/spring.factories index 2f189a2583be..6aca6335a723 100644 --- a/sdk/spring/azure-spring-boot/src/main/resources/META-INF/spring.factories +++ b/sdk/spring/azure-spring-boot/src/main/resources/META-INF/spring.factories @@ -1,7 +1,7 @@ org.springframework.boot.env.EnvironmentPostProcessor=com.azure.spring.cloudfoundry.environment.VcapProcessor org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -com.azure.spring.aad.implementation.AzureActiveDirectoryAutoConfiguration, \ com.azure.spring.autoconfigure.aad.AADAuthenticationFilterAutoConfiguration, \ +com.azure.spring.autoconfigure.aad.AzureActiveDirectoryAutoConfiguration, \ com.azure.spring.autoconfigure.b2c.AADB2CAutoConfiguration,\ com.azure.spring.autoconfigure.cosmos.CosmosAutoConfiguration,\ com.azure.spring.autoconfigure.cosmos.CosmosHealthConfiguration,\ diff --git a/sdk/spring/azure-spring-boot/src/main/resources/service-endpoints.properties b/sdk/spring/azure-spring-boot/src/main/resources/service-endpoints.properties index f0ce54d44e2f..9f20034f9b91 100644 --- a/sdk/spring/azure-spring-boot/src/main/resources/service-endpoints.properties +++ b/sdk/spring/azure-spring-boot/src/main/resources/service-endpoints.properties @@ -2,23 +2,26 @@ azure.service.endpoints.cn.aadSigninUri=https://login.partner.microsoftonline.cn azure.service.endpoints.cn.aadGraphApiUri=https://graph.chinacloudapi.cn/ azure.service.endpoints.cn.aadKeyDiscoveryUri=https://login.partner.microsoftonline.cn/common/discovery/keys azure.service.endpoints.cn.aadMembershipRestUri=https://graph.chinacloudapi.cn/me/memberOf?api-version=1.6 -azure.service.endpoints.cn.aadTransitiveMemberRestUri=https://graph.chinacloudapi.cn\ - /me/transitiveMemberOf?api-version=1.6 +azure.service.endpoints.cn.aadTransitiveMemberRestUri=\ + https://graph.chinacloudapi.cn/me/transitiveMemberOf?api-version=1.6 + azure.service.endpoints.cn-v2-graph.aadSigninUri=https://login.partner.microsoftonline.cn/ azure.service.endpoints.cn-v2-graph.aadGraphApiUri=https://microsoftgraph.chinacloudapi.cn/ azure.service.endpoints.cn-v2-graph.aadKeyDiscoveryUri=https://login.partner.microsoftonline.cn/common/discovery/keys azure.service.endpoints.cn-v2-graph.aadMembershipRestUri=https://microsoftgraph.chinacloudapi.cn/v1.0/me/memberOf -azure.service.endpoints.cn-v2-graph.aadTransitiveMemberRestUri=https://microsoftgraph.chinacloudapi.cn\ - /v1.0/me/transitiveMemberOf +azure.service.endpoints.cn-v2-graph.aadTransitiveMemberRestUri=\ + https://microsoftgraph.chinacloudapi.cn/v1.0/me/transitiveMemberOf + azure.service.endpoints.global.aadSigninUri=https://login.microsoftonline.com/ azure.service.endpoints.global.aadGraphApiUri=https://graph.windows.net/ azure.service.endpoints.global.aadKeyDiscoveryUri=https://login.microsoftonline.com/common/discovery/keys/ azure.service.endpoints.global.aadMembershipRestUri=https://graph.windows.net/me/memberOf?api-version=1.6 -azure.service.endpoints.global.aadTransitiveMemberRestUri=https://graph.windows.net\ - /me/transitiveMemberOf?api-version=1.6 +azure.service.endpoints.global.aadTransitiveMemberRestUri=\ + https://graph.windows.net/me/transitiveMemberOf?api-version=1.6 + azure.service.endpoints.global-v2-graph.aadSigninUri=https://login.microsoftonline.com/ azure.service.endpoints.global-v2-graph.aadGraphApiUri=https://graph.microsoft.com/ azure.service.endpoints.global-v2-graph.aadKeyDiscoveryUri=https://login.microsoftonline.com/common/discovery/keys/ azure.service.endpoints.global-v2-graph.aadMembershipRestUri=https://graph.microsoft.com/v1.0/me/memberOf -azure.service.endpoints.global-v2-graph.aadTransitiveMemberRestUri=https://graph.microsoft.com\ - /v1.0/me/transitiveMemberOf +azure.service.endpoints.global-v2-graph.aadTransitiveMemberRestUri=\ + https://graph.microsoft.com/v1.0/me/transitiveMemberOf diff --git a/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADOAuth2LoginConditionalPolicyConfigSample.java b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADOAuth2LoginConditionalPolicyConfigSample.java deleted file mode 100644 index 5c44bf00354c..000000000000 --- a/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADOAuth2LoginConditionalPolicyConfigSample.java +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.spring.aad; - -import com.azure.spring.autoconfigure.aad.AADAuthenticationFailureHandler; -import com.azure.spring.autoconfigure.aad.AADOAuth2AuthorizationRequestResolver; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -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.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; -import org.springframework.security.oauth2.core.oidc.user.OidcUser; - -/** - * WARNING: MODIFYING THIS FILE WILL REQUIRE CORRESPONDING UPDATES TO README.md FILE. LINE NUMBERS - * ARE USED TO EXTRACT APPROPRIATE CODE SEGMENTS FROM THIS FILE. ADD NEW CODE AT THE BOTTOM TO AVOID CHANGING - * LINE NUMBERS OF EXISTING CODE SAMPLES. - *

- * Code samples for the AAD OAuth2.0 login in README.md, including handling for AAD conditional policy - */ -@EnableWebSecurity -@EnableGlobalMethodSecurity(prePostEnabled = true) -public class AADOAuth2LoginConditionalPolicyConfigSample extends WebSecurityConfigurerAdapter { - - @Autowired - private OAuth2UserService oidcUserService; - - @Autowired - ApplicationContext applicationContext; - - @Override - protected void configure(HttpSecurity http) throws Exception { - final ClientRegistrationRepository clientRegistrationRepository = - applicationContext.getBean(ClientRegistrationRepository.class); - - http.authorizeRequests() - .anyRequest().authenticated() - .and() - .oauth2Login() - .userInfoEndpoint() - .oidcUserService(oidcUserService) - .and() - .authorizationEndpoint() - .authorizationRequestResolver(new AADOAuth2AuthorizationRequestResolver(clientRegistrationRepository)) - .and() - .failureHandler(new AADAuthenticationFailureHandler()); - } -} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AADAuthenticationAutoConfigurationTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AADAuthenticationAutoConfigurationTest.java index 05a7f870b51f..7b428e0c6060 100644 --- a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AADAuthenticationAutoConfigurationTest.java +++ b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AADAuthenticationAutoConfigurationTest.java @@ -16,18 +16,20 @@ public class AADAuthenticationAutoConfigurationTest { private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(AADAuthenticationFilterAutoConfiguration.class)) - .withPropertyValues("azure.activedirectory.client-id=fake-client-id", + .withPropertyValues( + "azure.activedirectory.client-id=fake-client-id", "azure.activedirectory.client-secret=fake-client-secret", "azure.activedirectory.user-group.allowed-groups=fake-group", "azure.service.endpoints.global.aadKeyDiscoveryUri=http://fake.aad.discovery.uri", - TestConstants.ALLOW_TELEMETRY_PROPERTY + "=false"); + TestConstants.ALLOW_TELEMETRY_PROPERTY + "=false" + ); @Test public void createAADAuthenticationFilter() { this.contextRunner.run(context -> { - final AADAuthenticationFilter azureADJwtTokenFilter = context.getBean(AADAuthenticationFilter.class); - assertThat(azureADJwtTokenFilter).isNotNull(); - assertThat(azureADJwtTokenFilter).isExactlyInstanceOf(AADAuthenticationFilter.class); + final AADAuthenticationFilter aadAuthenticationFilter = context.getBean(AADAuthenticationFilter.class); + assertThat(aadAuthenticationFilter).isNotNull(); + assertThat(aadAuthenticationFilter).isExactlyInstanceOf(AADAuthenticationFilter.class); }); } diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AADAuthenticationFilterPropertiesTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AADAuthenticationFilterPropertiesTest.java index 3f7e0ee8673b..6c6d92229d48 100644 --- a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AADAuthenticationFilterPropertiesTest.java +++ b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AADAuthenticationFilterPropertiesTest.java @@ -57,7 +57,7 @@ public void defaultEnvironmentIsGlobal() { final AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class); - assertThat(properties.getEnvironment()).isEqualTo(TestConstants.DEFAULT_ENVIRONMENT); + assertThat(properties.getEnvironment()).isEqualTo("global"); } } @@ -92,7 +92,7 @@ public void emptySettingsNotAllowed() { final BindValidationException bindException = (BindValidationException) exception.getCause().getCause(); final List errors = bindException.getValidationErrors().getAllErrors(); - final List errorStrings = errors.stream().map(e -> e.toString()).collect(Collectors.toList()); + 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': " diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AADAuthenticationFilterTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AADAuthenticationFilterTest.java index 0424b8835c5d..3251c92b510a 100644 --- a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AADAuthenticationFilterTest.java +++ b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AADAuthenticationFilterTest.java @@ -5,7 +5,6 @@ import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.proc.BadJOSEException; -import org.junit.Assume; import org.junit.Ignore; import org.junit.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -30,6 +29,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; public class AADAuthenticationFilterTest { private static final String TOKEN = "dummy-token"; @@ -51,12 +51,6 @@ public AADAuthenticationFilterTest() { ); } - @Ignore - public void beforeEveryMethod() { - Assume.assumeTrue(!TestConstants.CLIENT_ID.contains("real_client_id")); - Assume.assumeTrue(!TestConstants.CLIENT_SECRET.contains("real_client_secret")); - Assume.assumeTrue(!TestConstants.BEARER_TOKEN.contains("real_jtw_bearer_token")); - } //TODO (Zhou Liu): current test case is out of date, a new test case need to cover here, do it later. @Test @@ -70,7 +64,7 @@ public void doFilterInternal() { this.contextRunner.run(context -> { final HttpServletRequest request = mock(HttpServletRequest.class); - when(request.getHeader(TestConstants.TOKEN_HEADER)).thenReturn(TestConstants.BEARER_TOKEN); + when(request.getHeader(AUTHORIZATION)).thenReturn(TestConstants.BEARER_TOKEN); final HttpServletResponse response = mock(HttpServletResponse.class); final FilterChain filterChain = mock(FilterChain.class); diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AADOAuth2ConfigTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AADOAuth2ConfigTest.java index 04f7e428605e..d31a731963bf 100644 --- a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AADOAuth2ConfigTest.java +++ b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AADOAuth2ConfigTest.java @@ -3,8 +3,6 @@ package com.azure.spring.autoconfigure.aad; -import com.azure.spring.aad.implementation.AzureActiveDirectoryAutoConfiguration; -import com.azure.spring.aad.implementation.AzureClientRegistrationRepository; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -72,7 +70,7 @@ public void testOAuth2UserServiceBeanCreatedIfPropsConfigured() { @Test public void noOAuth2UserServiceBeanCreatedIfTenantIdNotConfigured() { - testPropResource.getSource().remove(TestConstants.TENANT_ID_PROPERTY); + testPropResource.getSource().remove("azure.activedirectory.tenant-id"); testContext = initTestContext(); exception.expect(NoSuchBeanDefinitionException.class); @@ -88,7 +86,6 @@ public void testEndpointsPropertiesLoadAndOverridable() { "azure.service.endpoints.global.aadMembershipRestUri=https://test/", TestConstants.ALLOW_TELEMETRY_PROPERTY + "=false"); - final Environment environment = testContext.getEnvironment(); assertThat(environment.getProperty("azure.service.endpoints.global.aadSigninUri")) .isEqualTo("https://test/"); diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/UserPrincipalAzureADGraphTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/GraphOboClientAzureADGraphTest.java similarity index 54% rename from sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/UserPrincipalAzureADGraphTest.java rename to sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/GraphOboClientAzureADGraphTest.java index 513f00cafa8c..4a74a2bfc3e1 100644 --- a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/UserPrincipalAzureADGraphTest.java +++ b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/GraphOboClientAzureADGraphTest.java @@ -6,24 +6,13 @@ 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.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.springframework.http.HttpHeaders; import org.springframework.security.core.GrantedAuthority; -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.Collection; import java.util.Collections; @@ -40,26 +29,29 @@ import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static org.assertj.core.api.Assertions.assertThat; 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; - -public class UserPrincipalAzureADGraphTest { +public class GraphOboClientAzureADGraphTest { @Rule public WireMockRule wireMockRule = new WireMockRule(9519); - private AzureADGraphClient graphClientMock; + private GraphOboClient graphOboClient; private AADAuthenticationProperties aadAuthenticationProperties; private ServiceEndpointsProperties serviceEndpointsProperties; - private String accessToken; private static String userGroupsJson; static { try { final ObjectMapper objectMapper = new ObjectMapper(); - final Map json = objectMapper.readValue(UserPrincipalAzureADGraphTest.class.getClassLoader() - .getResourceAsStream("aad/azure-ad-graph-user-groups.json"), - new TypeReference>() { }); + final Map json = objectMapper.readValue( + GraphOboClientAzureADGraphTest.class + .getClassLoader() + .getResourceAsStream("aad/azure-ad-graph-user-groups.json"), + new TypeReference>() { + } + ); userGroupsJson = objectMapper.writeValueAsString(json); } catch (IOException e) { e.printStackTrace(); @@ -70,7 +62,6 @@ public class UserPrincipalAzureADGraphTest { @Before public void setup() { - accessToken = TestConstants.ACCESS_TOKEN; aadAuthenticationProperties = new AADAuthenticationProperties(); serviceEndpointsProperties = new ServiceEndpointsProperties(); final ServiceEndpoints serviceEndpoints = new ServiceEndpoints(); @@ -81,7 +72,7 @@ public void setup() { @Test public void getAuthoritiesByUserGroups() throws Exception { aadAuthenticationProperties.getUserGroup().setAllowedGroups(Collections.singletonList("group1")); - this.graphClientMock = new AzureADGraphClient(aadAuthenticationProperties, serviceEndpointsProperties); + this.graphOboClient = new GraphOboClient(aadAuthenticationProperties, serviceEndpointsProperties); stubFor(get(urlEqualTo("/memberOf")) .withHeader(ACCEPT, equalTo("application/json;odata=minimalmetadata")) @@ -90,19 +81,21 @@ public void getAuthoritiesByUserGroups() throws Exception { .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE) .withBody(userGroupsJson))); - assertThat(graphClientMock.getGrantedAuthorities(TestConstants.ACCESS_TOKEN)).isNotEmpty() - .extracting(GrantedAuthority::getAuthority).containsExactly("ROLE_group1"); + assertThat(graphOboClient.getGrantedAuthorities(TestConstants.ACCESS_TOKEN)) + .isNotEmpty() + .extracting(GrantedAuthority::getAuthority) + .containsExactly("ROLE_group1"); verify(getRequestedFor(urlMatching("/memberOf")) - .withHeader(HttpHeaders.AUTHORIZATION, equalTo(String.format("Bearer %s", accessToken))) + .withHeader(AUTHORIZATION, equalTo(TestConstants.BEARER_TOKEN)) .withHeader(ACCEPT, equalTo("application/json;odata=minimalmetadata")) .withHeader("api-version", equalTo("1.6"))); } @Test public void getGroups() throws Exception { - aadAuthenticationProperties.setActiveDirectoryGroups(Arrays.asList("group1", "group2", "group3")); - this.graphClientMock = new AzureADGraphClient(aadAuthenticationProperties, serviceEndpointsProperties); + aadAuthenticationProperties.getUserGroup().setAllowedGroups(Arrays.asList("group1", "group2", "group3")); + this.graphOboClient = new GraphOboClient(aadAuthenticationProperties, serviceEndpointsProperties); stubFor(get(urlEqualTo("/memberOf")) .withHeader(ACCEPT, equalTo("application/json;odata=minimalmetadata")) @@ -111,43 +104,17 @@ public void getGroups() throws Exception { .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE) .withBody(userGroupsJson))); - final Collection authorities = graphClientMock + final Collection authorities = graphOboClient .getGrantedAuthorities(TestConstants.ACCESS_TOKEN); - assertThat(authorities).isNotEmpty().extracting(GrantedAuthority::getAuthority) - .containsExactlyInAnyOrder("ROLE_group1", "ROLE_group2", "ROLE_group3"); + assertThat(authorities) + .isNotEmpty() + .extracting(GrantedAuthority::getAuthority) + .containsExactlyInAnyOrder("ROLE_group1", "ROLE_group2", "ROLE_group3"); verify(getRequestedFor(urlMatching("/memberOf")) - .withHeader(HttpHeaders.AUTHORIZATION, equalTo(String.format("Bearer %s", accessToken))) + .withHeader(AUTHORIZATION, equalTo(TestConstants.BEARER_TOKEN)) .withHeader(ACCEPT, equalTo("application/json;odata=minimalmetadata")) .withHeader("api-version", equalTo("1.6"))); } - - @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(); - - Assert.assertNotNull("Serialized UserPrincipal not null", serializedPrincipal); - Assert.assertFalse("Serialized UserPrincipal kid not empty", - StringUtils.isEmpty(serializedPrincipal.getKid())); - Assert.assertNotNull("Serialized UserPrincipal claims not null.", serializedPrincipal.getClaims()); - Assert.assertTrue("Serialized UserPrincipal claims not empty.", - serializedPrincipal.getClaims().size() > 0); - } finally { - Files.deleteIfExists(tmpOutputFile.toPath()); - } - } } diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/GraphOboClientMicrosoftGraphTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/GraphOboClientMicrosoftGraphTest.java new file mode 100644 index 000000000000..402953a21b37 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/GraphOboClientMicrosoftGraphTest.java @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.autoconfigure.aad; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.security.core.GrantedAuthority; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +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.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; + +public class GraphOboClientMicrosoftGraphTest { + @Rule + public WireMockRule wireMockRule = new WireMockRule(9519); + + private GraphOboClient graphOboClient; + private AADAuthenticationProperties aadAuthenticationProperties; + private ServiceEndpointsProperties serviceEndpointsProperties; + private static String userGroupsJson; + + static { + try { + final ObjectMapper objectMapper = new ObjectMapper(); + final Map json = objectMapper.readValue( + GraphOboClientMicrosoftGraphTest.class + .getClassLoader() + .getResourceAsStream("aad/microsoft-graph-user-groups.json"), + new TypeReference>() { + } + ); + userGroupsJson = objectMapper.writeValueAsString(json); + } catch (IOException e) { + e.printStackTrace(); + userGroupsJson = null; + } + Assert.assertNotNull(userGroupsJson); + } + + @Before + public void setup() { + aadAuthenticationProperties = new AADAuthenticationProperties(); + aadAuthenticationProperties.setEnvironment("global-v2-graph"); + aadAuthenticationProperties.getUserGroup().setKey("@odata.type"); + aadAuthenticationProperties.getUserGroup().setValue("#microsoft.graph.group"); + aadAuthenticationProperties.getUserGroup().setObjectIDKey("id"); + serviceEndpointsProperties = new ServiceEndpointsProperties(); + final ServiceEndpoints serviceEndpoints = new ServiceEndpoints(); + serviceEndpoints.setAadMembershipRestUri("http://localhost:9519/memberOf"); + serviceEndpoints.setAadTransitiveMemberRestUri("http://localhost:9519/transitiveMemberOf"); + serviceEndpointsProperties.getEndpoints().put("global-v2-graph", serviceEndpoints); + } + + @Test + public void getAuthoritiesByUserGroups() throws Exception { + aadAuthenticationProperties.getUserGroup().setGroupRelationship("direct"); + aadAuthenticationProperties.getUserGroup().setAllowedGroups(Collections.singletonList("group1")); + serviceEndpointsProperties.getServiceEndpoints("global-v2-graph") + .setAadMembershipRestUri("http://localhost:9519/memberOf"); + this.graphOboClient = new GraphOboClient(aadAuthenticationProperties, serviceEndpointsProperties); + + stubFor(get(urlEqualTo("/memberOf")) + .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE)) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .withBody(userGroupsJson))); + + assertThat(graphOboClient.getGrantedAuthorities(TestConstants.ACCESS_TOKEN)) + .isNotEmpty() + .extracting(GrantedAuthority::getAuthority) + .containsExactly("ROLE_group1"); + + verify(getRequestedFor(urlMatching("/memberOf")) + .withHeader(AUTHORIZATION, equalTo(TestConstants.BEARER_TOKEN)) + .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE))); + } + + @Test + public void getDirectGroups() throws Exception { + aadAuthenticationProperties.getUserGroup().setGroupRelationship("direct"); + AADAuthenticationProperties.UserGroupProperties userGroupProperties = + aadAuthenticationProperties.getUserGroup(); + userGroupProperties.setAllowedGroups(Arrays.asList("group1", "group2", "group3")); + aadAuthenticationProperties.setUserGroup(userGroupProperties); + this.graphOboClient = new GraphOboClient(aadAuthenticationProperties, serviceEndpointsProperties); + + stubFor(get(urlEqualTo("/memberOf")) + .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE)) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .withBody(userGroupsJson))); + + final Collection authorities = graphOboClient + .getGrantedAuthorities(TestConstants.ACCESS_TOKEN); + + assertThat(authorities) + .isNotEmpty() + .extracting(GrantedAuthority::getAuthority) + .containsExactlyInAnyOrder("ROLE_group1", "ROLE_group2", "ROLE_group3"); + + verify(getRequestedFor(urlMatching("/memberOf")) + .withHeader(AUTHORIZATION, equalTo(TestConstants.BEARER_TOKEN)) + .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE))); + } + + @Test + public void getTransitiveGroups() throws Exception { + aadAuthenticationProperties.getUserGroup().setGroupRelationship("transitive"); + AADAuthenticationProperties.UserGroupProperties userGroupProperties = + aadAuthenticationProperties.getUserGroup(); + userGroupProperties.setAllowedGroups(Arrays.asList("group1", "group2", "group3")); + aadAuthenticationProperties.setUserGroup(userGroupProperties); + this.graphOboClient = new GraphOboClient(aadAuthenticationProperties, serviceEndpointsProperties); + + stubFor(get(urlEqualTo("/transitiveMemberOf")) + .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE)) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .withBody(userGroupsJson))); + + final Collection authorities = graphOboClient + .getGrantedAuthorities(TestConstants.ACCESS_TOKEN); + + assertThat(authorities) + .isNotEmpty() + .extracting(GrantedAuthority::getAuthority) + .containsExactlyInAnyOrder("ROLE_group1", "ROLE_group2", "ROLE_group3"); + + verify(getRequestedFor(urlMatching("/transitiveMemberOf")) + .withHeader(AUTHORIZATION, equalTo(TestConstants.BEARER_TOKEN)) + .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE))); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/GraphWebClientAzureADGraphTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/GraphWebClientAzureADGraphTest.java new file mode 100644 index 000000000000..d235f4329f39 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/GraphWebClientAzureADGraphTest.java @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.autoconfigure.aad; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.security.core.GrantedAuthority; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +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.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; + +public class GraphWebClientAzureADGraphTest { + @Rule + public WireMockRule wireMockRule = new WireMockRule(9519); + + private GraphWebClient graphWebClient; + private AADAuthenticationProperties aadAuthenticationProperties; + private static String userGroupsJson; + + static { + try { + final ObjectMapper objectMapper = new ObjectMapper(); + final Map json = objectMapper.readValue( + GraphWebClientAzureADGraphTest.class + .getClassLoader() + .getResourceAsStream("aad/azure-ad-graph-user-groups.json"), + new TypeReference>() { + } + ); + userGroupsJson = objectMapper.writeValueAsString(json); + } catch (IOException e) { + e.printStackTrace(); + userGroupsJson = null; + } + Assert.assertNotNull(userGroupsJson); + } + + @Before + public void setup() { + aadAuthenticationProperties = new AADAuthenticationProperties(); + ServiceEndpointsProperties serviceEndpointsProperties = new ServiceEndpointsProperties(); + final ServiceEndpoints serviceEndpoints = new ServiceEndpoints(); + serviceEndpoints.setAadMembershipRestUri("http://localhost:9519/memberOf"); + serviceEndpointsProperties.getEndpoints().put("global", serviceEndpoints); + this.graphWebClient = new GraphWebClient( + aadAuthenticationProperties, + serviceEndpointsProperties, + GraphWebClientTestUtil.createWebClientForTest() + ); + } + + @Test + public void getAuthoritiesByUserGroups() { + aadAuthenticationProperties.getUserGroup().setAllowedGroups(Collections.singletonList("group1")); + stubFor(get(urlEqualTo("/memberOf")) + .withHeader(ACCEPT, equalTo("application/json;odata=minimalmetadata")) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .withBody(userGroupsJson))); + + assertThat(graphWebClient.getGrantedAuthorities()) + .isNotEmpty() + .extracting(GrantedAuthority::getAuthority) + .containsExactly("ROLE_group1"); + + verify(getRequestedFor(urlMatching("/memberOf")) + .withHeader(AUTHORIZATION, equalTo(String.format("Bearer %s", TestConstants.ACCESS_TOKEN))) + .withHeader(ACCEPT, equalTo("application/json;odata=minimalmetadata")) + .withHeader("api-version", equalTo("1.6"))); + } + + @Test + public void getGroups() { + aadAuthenticationProperties.getUserGroup().setAllowedGroups(Arrays.asList("group1", "group2", "group3")); + stubFor(get(urlEqualTo("/memberOf")) + .withHeader(ACCEPT, equalTo("application/json;odata=minimalmetadata")) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .withBody(userGroupsJson))); + + final Collection authorities = graphWebClient.getGrantedAuthorities(); + + assertThat(authorities) + .isNotEmpty() + .extracting(GrantedAuthority::getAuthority) + .containsExactlyInAnyOrder("ROLE_group1", "ROLE_group2", "ROLE_group3"); + + verify(getRequestedFor(urlMatching("/memberOf")) + .withHeader(AUTHORIZATION, equalTo(String.format("Bearer %s", TestConstants.ACCESS_TOKEN))) + .withHeader(ACCEPT, equalTo("application/json;odata=minimalmetadata")) + .withHeader("api-version", equalTo("1.6"))); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/GraphWebClientMicrosoftGraphTest.java similarity index 66% rename from sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java rename to sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/GraphWebClientMicrosoftGraphTest.java index 1f525570d99c..53f9ca652f28 100644 --- a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java +++ b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/GraphWebClientMicrosoftGraphTest.java @@ -6,23 +6,13 @@ 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.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.springframework.security.core.GrantedAuthority; -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.Collection; import java.util.Collections; @@ -43,22 +33,25 @@ import static org.springframework.http.HttpHeaders.CONTENT_TYPE; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; -public class UserPrincipalMicrosoftGraphTest { +public class GraphWebClientMicrosoftGraphTest { @Rule public WireMockRule wireMockRule = new WireMockRule(9519); - private AzureADGraphClient graphClientMock; + private GraphWebClient graphWebClient; private AADAuthenticationProperties aadAuthenticationProperties; private ServiceEndpointsProperties serviceEndpointsProperties; - 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>() { }); + final Map json = objectMapper.readValue( + GraphWebClientMicrosoftGraphTest.class + .getClassLoader() + .getResourceAsStream("aad/microsoft-graph-user-groups.json"), + new TypeReference>() { + } + ); userGroupsJson = objectMapper.writeValueAsString(json); } catch (IOException e) { e.printStackTrace(); @@ -69,7 +62,6 @@ public class UserPrincipalMicrosoftGraphTest { @Before public void setup() { - accessToken = MicrosoftGraphConstants.BEARER_TOKEN; aadAuthenticationProperties = new AADAuthenticationProperties(); aadAuthenticationProperties.setEnvironment("global-v2-graph"); aadAuthenticationProperties.getUserGroup().setKey("@odata.type"); @@ -83,12 +75,16 @@ public void setup() { } @Test - public void getAuthoritiesByUserGroups() throws Exception { + public void getAuthoritiesByUserGroups() { aadAuthenticationProperties.getUserGroup().setGroupRelationship("direct"); aadAuthenticationProperties.getUserGroup().setAllowedGroups(Collections.singletonList("group1")); serviceEndpointsProperties.getServiceEndpoints("global-v2-graph") .setAadMembershipRestUri("http://localhost:9519/memberOf"); - this.graphClientMock = new AzureADGraphClient(aadAuthenticationProperties, serviceEndpointsProperties); + this.graphWebClient = new GraphWebClient( + aadAuthenticationProperties, + serviceEndpointsProperties, + GraphWebClientTestUtil.createWebClientForTest() + ); stubFor(get(urlEqualTo("/memberOf")) .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE)) @@ -97,23 +93,27 @@ public void getAuthoritiesByUserGroups() throws Exception { .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE) .withBody(userGroupsJson))); - assertThat(graphClientMock.getGrantedAuthorities(MicrosoftGraphConstants.BEARER_TOKEN)) + assertThat(graphWebClient.getGrantedAuthorities()) .isNotEmpty() .extracting(GrantedAuthority::getAuthority) .containsExactly("ROLE_group1"); verify(getRequestedFor(urlMatching("/memberOf")) - .withHeader(AUTHORIZATION, equalTo(String.format("Bearer %s", accessToken))) + .withHeader(AUTHORIZATION, equalTo(String.format("Bearer %s", TestConstants.ACCESS_TOKEN))) .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE))); } @Test - public void getDirectGroups() throws Exception { + public void getDirectGroups() { aadAuthenticationProperties.getUserGroup().setGroupRelationship("direct"); AADAuthenticationProperties.UserGroupProperties userGroupProperties = aadAuthenticationProperties.getUserGroup(); userGroupProperties.setAllowedGroups(Arrays.asList("group1", "group2", "group3")); aadAuthenticationProperties.setUserGroup(userGroupProperties); - this.graphClientMock = new AzureADGraphClient(aadAuthenticationProperties, serviceEndpointsProperties); + this.graphWebClient = new GraphWebClient( + aadAuthenticationProperties, + serviceEndpointsProperties, + GraphWebClientTestUtil.createWebClientForTest() + ); stubFor(get(urlEqualTo("/memberOf")) .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE)) @@ -122,8 +122,7 @@ public void getDirectGroups() throws Exception { .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE) .withBody(userGroupsJson))); - final Collection authorities = graphClientMock - .getGrantedAuthorities(MicrosoftGraphConstants.BEARER_TOKEN); + final Collection authorities = graphWebClient.getGrantedAuthorities(); assertThat(authorities) .isNotEmpty() @@ -131,17 +130,21 @@ public void getDirectGroups() throws Exception { .containsExactlyInAnyOrder("ROLE_group1", "ROLE_group2", "ROLE_group3"); verify(getRequestedFor(urlMatching("/memberOf")) - .withHeader(AUTHORIZATION, equalTo(String.format("Bearer %s", accessToken))) + .withHeader(AUTHORIZATION, equalTo(String.format("Bearer %s", TestConstants.ACCESS_TOKEN))) .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE))); } @Test - public void getTransitiveGroups() throws Exception { + public void getTransitiveGroups() { aadAuthenticationProperties.getUserGroup().setGroupRelationship("transitive"); AADAuthenticationProperties.UserGroupProperties userGroupProperties = aadAuthenticationProperties.getUserGroup(); userGroupProperties.setAllowedGroups(Arrays.asList("group1", "group2", "group3")); aadAuthenticationProperties.setUserGroup(userGroupProperties); - this.graphClientMock = new AzureADGraphClient(aadAuthenticationProperties, serviceEndpointsProperties); + this.graphWebClient = new GraphWebClient( + aadAuthenticationProperties, + serviceEndpointsProperties, + GraphWebClientTestUtil.createWebClientForTest() + ); stubFor(get(urlEqualTo("/transitiveMemberOf")) .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE)) @@ -150,8 +153,7 @@ public void getTransitiveGroups() throws Exception { .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE) .withBody(userGroupsJson))); - final Collection authorities = graphClientMock - .getGrantedAuthorities(MicrosoftGraphConstants.BEARER_TOKEN); + final Collection authorities = graphWebClient.getGrantedAuthorities(); assertThat(authorities) .isNotEmpty() @@ -159,35 +161,7 @@ public void getTransitiveGroups() throws Exception { .containsExactlyInAnyOrder("ROLE_group1", "ROLE_group2", "ROLE_group3"); verify(getRequestedFor(urlMatching("/transitiveMemberOf")) - .withHeader(AUTHORIZATION, equalTo(String.format("Bearer %s", accessToken))) + .withHeader(AUTHORIZATION, equalTo(String.format("Bearer %s", TestConstants.ACCESS_TOKEN))) .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(); - - Assert.assertNotNull("Serialized UserPrincipal not null", serializedPrincipal); - Assert.assertFalse("Serialized UserPrincipal kid not empty", - StringUtils.isEmpty(serializedPrincipal.getKid())); - Assert.assertNotNull("Serialized UserPrincipal claims not null.", serializedPrincipal.getClaims()); - Assert.assertTrue("Serialized UserPrincipal claims not empty.", - serializedPrincipal.getClaims().size() > 0); - } finally { - Files.deleteIfExists(tmpOutputFile.toPath()); - } - } } diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AzureADGraphClientTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/GraphWebClientTest.java similarity index 84% rename from sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AzureADGraphClientTest.java rename to sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/GraphWebClientTest.java index bcd6de70161e..ec581b33908e 100644 --- a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AzureADGraphClientTest.java +++ b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/GraphWebClientTest.java @@ -19,11 +19,9 @@ import static org.assertj.core.api.Assertions.assertThat; @RunWith(MockitoJUnitRunner.class) -public class AzureADGraphClientTest { +public class GraphWebClientTest { - private AzureADGraphClient adGraphClient; - - private AADAuthenticationProperties aadAuthenticationProperties; + private GraphWebClient adGraphClient; @Mock private ServiceEndpointsProperties endpointsProps; @@ -32,9 +30,9 @@ public class AzureADGraphClientTest { public void setup() { final List activeDirectoryGroups = new ArrayList<>(); activeDirectoryGroups.add("Test_Group"); - aadAuthenticationProperties = new AADAuthenticationProperties(); + AADAuthenticationProperties aadAuthenticationProperties = new AADAuthenticationProperties(); aadAuthenticationProperties.getUserGroup().setAllowedGroups(activeDirectoryGroups); - adGraphClient = new AzureADGraphClient(aadAuthenticationProperties, endpointsProps); + adGraphClient = new GraphWebClient(aadAuthenticationProperties, endpointsProps, null); } @Test diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/GraphWebClientTestUtil.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/GraphWebClientTestUtil.java new file mode 100644 index 000000000000..1ba4fd72690b --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/GraphWebClientTestUtil.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.autoconfigure.aad; + +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.web.reactive.function.client.WebClient; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +public class GraphWebClientTestUtil { + + public static WebClient createWebClientForTest() { + ClientRegistration clientRegistration = + ClientRegistration.withRegistrationId("graph") + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}") + .clientId("test") + .clientSecret("test") + .authorizationUri("test") + .tokenUri("test") + .jwkSetUri("test") + .build(); + OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager = authorizeRequest -> new OAuth2AuthorizedClient( + clientRegistration, + "principalName", + new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, + TestConstants.ACCESS_TOKEN, + Instant.now().minus(10, ChronoUnit.MINUTES), + Instant.now().plus(10, ChronoUnit.MINUTES) + ) + ); + ServletOAuth2AuthorizedClientExchangeFilterFunction servletOAuth2AuthorizedClientExchangeFilterFunction = + new ServletOAuth2AuthorizedClientExchangeFilterFunction(oAuth2AuthorizedClientManager); + return WebClient.builder() + .apply(servletOAuth2AuthorizedClientExchangeFilterFunction.oauth2Configuration()) + .build(); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/MicrosoftGraphConstants.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/MicrosoftGraphConstants.java deleted file mode 100644 index 8529dfcd3660..000000000000 --- a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/MicrosoftGraphConstants.java +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.spring.autoconfigure.aad; - -import java.util.Arrays; -import java.util.List; - -public class MicrosoftGraphConstants { - - public static final String SERVICE_ENVIRONMENT_PROPERTY = "azure.activedirectory.environment"; - public static final String CLIENT_ID_PROPERTY = "azure.activedirectory.client-id"; - public static final String CLIENT_SECRET_PROPERTY = "azure.activedirectory.client-secret"; - public static final String TARGETED_GROUPS_PROPERTY = "azure.activedirectory.user-group.allowed-groups"; - public static final String TENANT_ID_PROPERTY = "azure.activedirectory.tenant-id"; - - public static final String DEFAULT_ENVIRONMENT = "global"; - 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_jtw_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/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/ResourceRetrieverTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/ResourceRetrieverTest.java index 46b75b523b09..2cd00720ca9d 100644 --- a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/ResourceRetrieverTest.java +++ b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/ResourceRetrieverTest.java @@ -14,11 +14,13 @@ public class ResourceRetrieverTest { private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(AADAuthenticationFilterAutoConfiguration.class)) - .withPropertyValues("azure.activedirectory.client-id=fake-client-id", - "azure.activedirectory.client-secret=fake-client-secret", - "azure.activedirectory.user-group.allowed-groups=fake-group", - "azure.service.endpoints.global.aadKeyDiscoveryUri=http://fake.aad.discovery.uri"); + .withConfiguration(AutoConfigurations.of(AADAuthenticationFilterAutoConfiguration.class)) + .withPropertyValues( + "azure.activedirectory.client-id=fake-client-id", + "azure.activedirectory.client-secret=fake-client-secret", + "azure.activedirectory.user-group.allowed-groups=fake-group", + "azure.service.endpoints.global.aadKeyDiscoveryUri=http://fake.aad.discovery.uri" + ); @Test public void resourceRetrieverDefaultConfig() { diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/TestConstants.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/TestConstants.java index 00b37e65d235..ec08008cfd5a 100644 --- a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/TestConstants.java +++ b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/TestConstants.java @@ -11,28 +11,12 @@ public class TestConstants { public static final String CLIENT_ID_PROPERTY = "azure.activedirectory.client-id"; public static final String CLIENT_SECRET_PROPERTY = "azure.activedirectory.client-secret"; public static final String TARGETED_GROUPS_PROPERTY = "azure.activedirectory.user-group.allowed-groups"; - public static final String TENANT_ID_PROPERTY = "azure.activedirectory.tenant-id"; public static final String ALLOW_TELEMETRY_PROPERTY = "azure.activedirectory.allow-telemetry"; - public static final String DEFAULT_ENVIRONMENT = "global"; 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 ACCESS_TOKEN = "real_jwt_access_token"; - 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"; + public static final String BEARER_TOKEN = "Bearer " + ACCESS_TOKEN; } diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/UserPrincipalManagerTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/UserPrincipalManagerTest.java index d11d8cd3ab7a..5d867887a3f5 100644 --- a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/UserPrincipalManagerTest.java +++ b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/UserPrincipalManagerTest.java @@ -3,37 +3,61 @@ package com.azure.spring.autoconfigure.aad; +import com.nimbusds.jose.JWSObject; 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.JWTClaimsSet; import com.nimbusds.jwt.proc.BadJWTException; import junitparams.FileParameters; import junitparams.JUnitParamsRunner; +import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +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.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import java.text.ParseException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.Assert.assertTrue; @RunWith(JUnitParamsRunner.class) public class UserPrincipalManagerTest { + /** Token from https://docs.microsoft.com/azure/active-directory/develop/v2-id-and-access-tokens */ + private 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"; + private static ImmutableJWKSet immutableJWKSet; @BeforeClass 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))); + immutableJWKSet = new ImmutableJWKSet<>(new JWKSet(JWK.parse(cert))); } private UserPrincipalManager userPrincipalManager; @@ -43,19 +67,28 @@ public static void setupClass() throws Exception { 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"); + 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( + assertThatCode(() -> + userPrincipalManager.buildUserPrincipal( new String(Files.readAllBytes( - Paths.get("src/test/resources/jwt-bad-issuer.txt")), StandardCharsets.UTF_8))) - .isInstanceOf(BadJWTException.class); + Paths.get("src/test/resources/jwt-bad-issuer.txt")), + StandardCharsets.UTF_8 + ) + ) + ).isInstanceOf(BadJWTException.class); } @Test @@ -70,9 +103,47 @@ public void validIssuer(final String token) { @Test public void nullIssuer() { userPrincipalManager = new UserPrincipalManager(immutableJWKSet); - assertThatCode(() -> userPrincipalManager.buildUserPrincipal( + assertThatCode(() -> + userPrincipalManager.buildUserPrincipal( new String(Files.readAllBytes( - Paths.get("src/test/resources/jwt-null-issuer.txt")), StandardCharsets.UTF_8))) - .isInstanceOf(BadJWTException.class); + Paths.get("src/test/resources/jwt-null-issuer.txt")), + StandardCharsets.UTF_8 + ) + ) + ).isInstanceOf(BadJWTException.class); + } + + + + @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(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(); + + Assert.assertNotNull("Serialized UserPrincipal not null", serializedPrincipal); + Assert.assertFalse( + "Serialized UserPrincipal kid not empty", + StringUtils.isEmpty(serializedPrincipal.getKid()) + ); + Assert.assertNotNull("Serialized UserPrincipal claims not null.", serializedPrincipal.getClaims()); + assertTrue( + "Serialized UserPrincipal claims not empty.", + serializedPrincipal.getClaims().size() > 0 + ); + } finally { + Files.deleteIfExists(tmpOutputFile.toPath()); + } } }