From b18f2500ce128c0a6be07ad2d8f781496df8beb9 Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Fri, 25 Dec 2020 10:27:55 +0800 Subject: [PATCH 01/28] fix failureHandle not error info --- .../spring/aad/webapp/AzureOAuth2Configuration.java | 3 ++- .../com/azure/spring/aad/webapp/AzureOAuth2Error.java | 9 +-------- .../aad/webapp/AzureOAuthenticationFailureHandler.java | 3 +-- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AzureOAuth2Configuration.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AzureOAuth2Configuration.java index cc6e156c9875e..1c6654715ad99 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AzureOAuth2Configuration.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AzureOAuth2Configuration.java @@ -31,6 +31,7 @@ */ public abstract class AzureOAuth2Configuration extends WebSecurityConfigurerAdapter { + private static final String DEFAULT_FAILURE_URL = "/login?error"; @Autowired private AADWebAppClientRegistrationRepository repo; @Autowired @@ -54,7 +55,7 @@ protected void configure(HttpSecurity http) throws Exception { .userInfoEndpoint() .oidcUserService(oidcUserService) .and() - .failureHandler(failureHandler()) + .failureHandler(failureHandler()).failureUrl(DEFAULT_FAILURE_URL) .and() .logout() .logoutSuccessHandler(oidcLogoutSuccessHandler()) diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AzureOAuth2Error.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AzureOAuth2Error.java index 1046771dd7a66..38e50e6cecea0 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AzureOAuth2Error.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AzureOAuth2Error.java @@ -64,13 +64,6 @@ public final String getClaims() { @Override public String toString() { - return "AADAuthenticationException{" - + ", error_codes='" + errorCodes + '\'' - + ", timestamp='" + timestamp + '\'' - + ", trace_id='" + traceId + '\'' - + ", correlation_id='" + correlationId + '\'' - + ", suberror='" + subError + '\'' - + ", claims='" + claims + '\'' - + '}'; + return super.toString(); } } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AzureOAuthenticationFailureHandler.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AzureOAuthenticationFailureHandler.java index 086ea2c1ac16b..c2f0e4c7447fe 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AzureOAuthenticationFailureHandler.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AzureOAuthenticationFailureHandler.java @@ -20,11 +20,10 @@ * Redirect URL for handling OAuthentication failure */ public class AzureOAuthenticationFailureHandler implements AuthenticationFailureHandler { - private static final String DEFAULT_FAILURE_URL = "/login?error"; private final AuthenticationFailureHandler defaultHandler; public AzureOAuthenticationFailureHandler() { - this.defaultHandler = new SimpleUrlAuthenticationFailureHandler(DEFAULT_FAILURE_URL); + this.defaultHandler = new SimpleUrlAuthenticationFailureHandler(); } @Override From 0254e884d3eede4ad0ab46d7bf106e15cfb9aefb Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Thu, 31 Dec 2020 10:39:40 +0800 Subject: [PATCH 02/28] fix failureHandle not error info --- .../aad/webapp/AzureOAuth2Configuration.java | 17 ++++++++++++++++- .../spring/aad/webapp/AzureOAuth2Error.java | 7 ------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AzureOAuth2Configuration.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AzureOAuth2Configuration.java index 1c6654715ad99..9a261d4d4fe1b 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AzureOAuth2Configuration.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AzureOAuth2Configuration.java @@ -8,6 +8,7 @@ import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; @@ -19,6 +20,7 @@ import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; @@ -55,7 +57,9 @@ protected void configure(HttpSecurity http) throws Exception { .userInfoEndpoint() .oidcUserService(oidcUserService) .and() - .failureHandler(failureHandler()).failureUrl(DEFAULT_FAILURE_URL) + .failureHandler(failureHandler()) + .and() + .apply(new AADHttpConfigurer()) .and() .logout() .logoutSuccessHandler(oidcLogoutSuccessHandler()) @@ -91,4 +95,15 @@ protected OAuth2AuthorizationRequestResolver requestResolver() { protected AuthenticationFailureHandler failureHandler() { return new AzureOAuthenticationFailureHandler(); } + + /** + * Used to fix custom failureHandler with no error info. + */ + private final static class AADHttpConfigurer extends AbstractHttpConfigurer{ + @Override + public void init(HttpSecurity http) { + DefaultLoginPageGeneratingFilter sharedObject = http.getSharedObject(DefaultLoginPageGeneratingFilter.class); + sharedObject.setFailureUrl(DEFAULT_FAILURE_URL); + } + } } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AzureOAuth2Error.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AzureOAuth2Error.java index 38e50e6cecea0..a70984b3d34b5 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AzureOAuth2Error.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AzureOAuth2Error.java @@ -59,11 +59,4 @@ public final String getSubError() { public final String getClaims() { return claims; } - - - @Override - public String toString() { - - return super.toString(); - } } From 230ca9dca52f4ce63bab84e79f1dc4b48af81958 Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Mon, 11 Jan 2021 11:15:33 +0800 Subject: [PATCH 03/28] fix failureHandle not error info --- sdk/spring/azure-spring-boot/pom.xml | 6 ++ ...ADOAuth2OboAuthorizedClientRepository.java | 30 ++++++ .../aad/webapp/AADWebAppConfiguration.java | 102 ++++++++++++++++++ .../AADWebSecurityConfigurerAdapter.java | 2 +- 4 files changed, 139 insertions(+), 1 deletion(-) diff --git a/sdk/spring/azure-spring-boot/pom.xml b/sdk/spring/azure-spring-boot/pom.xml index 8b04d6b5e81b9..987a4ddcbf78b 100644 --- a/sdk/spring/azure-spring-boot/pom.xml +++ b/sdk/spring/azure-spring-boot/pom.xml @@ -270,6 +270,12 @@ spring-core 5.2.10.RELEASE + + org.springframework + spring-webflux + 5.2.10.RELEASE + compile + diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java index 82c74fbaa2516..4e40e86097a4e 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java @@ -3,15 +3,18 @@ package com.azure.spring.aad.webapi; +import com.azure.spring.autoconfigure.aad.Constants; import com.microsoft.aad.msal4j.ClientCredentialFactory; import com.microsoft.aad.msal4j.ConfidentialClientApplication; import com.microsoft.aad.msal4j.IClientSecret; +import com.microsoft.aad.msal4j.MsalInteractionRequiredException; import com.microsoft.aad.msal4j.OnBehalfOfParameters; import com.microsoft.aad.msal4j.UserAssertion; import com.nimbusds.jwt.JWT; import com.nimbusds.jwt.JWTParser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -19,12 +22,17 @@ import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; import java.net.MalformedURLException; import java.time.Instant; import java.util.Date; +import java.util.Optional; /** *

@@ -87,6 +95,28 @@ public T loadAuthorizedClient(String registra request.setAttribute(oboAuthorizedClientAttributeName, (T) oAuth2AuthorizedClient); return (T) oAuth2AuthorizedClient; } catch (Throwable throwable) { + String claims = Optional.of(throwable) + .map(Throwable::getCause) + .filter(e -> e instanceof MsalInteractionRequiredException) + .map(e -> (MsalInteractionRequiredException) e) + .map(MsalInteractionRequiredException::claims) + .orElse(null); + + if (claims != null) { + ServletRequestAttributes attr = + (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + HttpServletResponse response = attr.getResponse(); + response.setStatus(HttpStatus.FORBIDDEN.value()); + try { + ServletOutputStream outputStream = response.getOutputStream(); + String result = + Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS + claims + Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS; + outputStream.write(result.getBytes()); + outputStream.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + } LOGGER.error("Failed to load authorized client.", throwable); } return null; diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java index c1c0f025e239e..2fbefee8ded92 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java @@ -5,6 +5,7 @@ import com.azure.spring.aad.AADAuthorizationServerEndpoints; import com.azure.spring.autoconfigure.aad.AADAuthenticationProperties; +import com.azure.spring.autoconfigure.aad.Constants; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -17,14 +18,28 @@ import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder; import org.springframework.security.oauth2.client.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.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -121,6 +136,21 @@ private Set accessTokenScopes() { return result; } + @ControllerAdvice + public class GlobalExceptionAdvice { + @ExceptionHandler(AADConditionalAccessException.class) + public void handleUserNotFound(HttpServletRequest request, HttpServletResponse response, Exception e) throws IOException { + if(e instanceof AADConditionalAccessException){ + response.setStatus(302); + SecurityContextHolder.clearContext(); + AADConditionalAccessException conditionalAccessException = (AADConditionalAccessException)e; + request.getSession().setAttribute(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, conditionalAccessException.getClaims()); + String redirectUrl = request.getRequestURL().toString(); + response.sendRedirect(redirectUrl); + } + } + } + private Set openidScopes() { Set result = new HashSet<>(); result.add("openid"); @@ -196,4 +226,76 @@ protected void configure(HttpSecurity http) throws Exception { } } + + @Bean + public OAuth2AuthorizedClientManager authorizedClientManager( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository) { + + OAuth2AuthorizedClientProvider authorizedClientProvider = + OAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build(); + + DefaultOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; + } + + @Bean + public static WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { + ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = + new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + return WebClient.builder() + .apply(oauth2Client.oauth2Configuration()) + .filter(errorHandlingFilter()) + .build(); + } + + + private static ExchangeFilterFunction errorHandlingFilter() { + return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { + if (clientResponse.statusCode().is4xxClientError()) { + return clientResponse.bodyToMono(String.class) + .flatMap(errorBody -> { + if (isConditionalAccessError(errorBody)) { + return Mono.error(convertToException(errorBody)); + } + return Mono.just(clientResponse); + }); + } + return Mono.just(clientResponse); + } + ); + } + + + private static boolean isConditionalAccessError(String body) { + return body.startsWith(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS); + } + + private static AADConditionalAccessException convertToException(String body) { + String claims = body.split(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)[1]; + return new AADConditionalAccessException(claims); + } + + protected static class AADConditionalAccessException extends RuntimeException{ + String claims; + protected AADConditionalAccessException(String claims){ + this.claims = claims; + } + public String getClaims() { + return claims; + } + + public void setClaims(String claims) { + this.claims = claims; + } + } } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java index 4ad2903f802f2..c0683b882874e 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java @@ -94,7 +94,7 @@ protected AuthenticationFailureHandler failureHandler() { } /** - * Used to fix custom failureHandler with no error info. + * Fix the default error info not displayed when the setting failureHandler in configure */ private final static class AADHttpConfigurer extends AbstractHttpConfigurer{ @Override From 726a95e2c08ded0c4d04f51dd16b23e0c8d5b428 Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Mon, 11 Jan 2021 18:09:55 +0800 Subject: [PATCH 04/28] code format for checkStyle --- sdk/spring/azure-spring-boot/pom.xml | 4 +- ...ADOAuth2OboAuthorizedClientRepository.java | 8 +- .../aad/webapp/AADWebAppConfiguration.java | 76 ++++++------------- .../AADWebSecurityConfigurerAdapter.java | 9 ++- 4 files changed, 37 insertions(+), 60 deletions(-) diff --git a/sdk/spring/azure-spring-boot/pom.xml b/sdk/spring/azure-spring-boot/pom.xml index 987a4ddcbf78b..34e4b25d95bc3 100644 --- a/sdk/spring/azure-spring-boot/pom.xml +++ b/sdk/spring/azure-spring-boot/pom.xml @@ -273,8 +273,7 @@ org.springframework spring-webflux - 5.2.10.RELEASE - compile + 5.2.10.RELEASE @@ -302,6 +301,7 @@ org.springframework:spring-core:[5.2.10.RELEASE] org.springframework:spring-web:[5.2.10.RELEASE] org.springframework:spring-jms:[5.2.10.RELEASE] + org.springframework:spring-webflux:[5.2.10.RELEASE] org.springframework.boot:spring-boot-actuator-autoconfigure:[2.3.5.RELEASE] org.springframework.boot:spring-boot-autoconfigure-processor:[2.3.5.RELEASE] org.springframework.boot:spring-boot-autoconfigure:[2.3.5.RELEASE] diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java index 4e40e86097a4e..e52f01ee0de20 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java @@ -30,6 +30,7 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.MalformedURLException; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Date; import java.util.Optional; @@ -106,12 +107,13 @@ public T loadAuthorizedClient(String registra ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); HttpServletResponse response = attr.getResponse(); + assert response != null; response.setStatus(HttpStatus.FORBIDDEN.value()); try { ServletOutputStream outputStream = response.getOutputStream(); - String result = - Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS + claims + Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS; - outputStream.write(result.getBytes()); + String result = Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS + + claims + Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS; + outputStream.write(result.getBytes(StandardCharsets.UTF_8)); outputStream.flush(); } catch (IOException e) { e.printStackTrace(); diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java index 2fbefee8ded92..78e23875621af 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java @@ -19,22 +19,16 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder; import org.springframework.security.oauth2.client.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.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; -import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; import javax.servlet.http.HttpServletRequest; @@ -136,18 +130,28 @@ private Set accessTokenScopes() { return result; } + /** + * Handle conditional access error in obo flow. + */ @ControllerAdvice - public class GlobalExceptionAdvice { + public static class GlobalExceptionAdvice { @ExceptionHandler(AADConditionalAccessException.class) - public void handleUserNotFound(HttpServletRequest request, HttpServletResponse response, Exception e) throws IOException { - if(e instanceof AADConditionalAccessException){ - response.setStatus(302); - SecurityContextHolder.clearContext(); - AADConditionalAccessException conditionalAccessException = (AADConditionalAccessException)e; - request.getSession().setAttribute(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, conditionalAccessException.getClaims()); - String redirectUrl = request.getRequestURL().toString(); - response.sendRedirect(redirectUrl); - } + public void handleUserNotFound(HttpServletRequest request, + HttpServletResponse response, Exception exception) { + Optional.of(exception) + .filter(e -> e instanceof AADConditionalAccessException) + .map(e -> (AADConditionalAccessException) e) + .ifPresent(aadConditionalAccessException -> { + response.setStatus(302); + SecurityContextHolder.clearContext(); + request.getSession().setAttribute(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, + aadConditionalAccessException.getClaims()); + try { + response.sendRedirect(request.getRequestURL().toString()); + } catch (IOException e) { + e.printStackTrace(); + } + }); } } @@ -227,39 +231,7 @@ protected void configure(HttpSecurity http) throws Exception { } - @Bean - public OAuth2AuthorizedClientManager authorizedClientManager( - ClientRegistrationRepository clientRegistrationRepository, - OAuth2AuthorizedClientRepository authorizedClientRepository) { - - OAuth2AuthorizedClientProvider authorizedClientProvider = - OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build(); - - DefaultOAuth2AuthorizedClientManager authorizedClientManager = - new DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - return authorizedClientManager; - } - - @Bean - public static WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { - ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = - new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); - return WebClient.builder() - .apply(oauth2Client.oauth2Configuration()) - .filter(errorHandlingFilter()) - .build(); - } - - - private static ExchangeFilterFunction errorHandlingFilter() { + public static ExchangeFilterFunction webClientErrorHandlingFilter() { return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { if (clientResponse.statusCode().is4xxClientError()) { return clientResponse.bodyToMono(String.class) @@ -285,11 +257,13 @@ private static AADConditionalAccessException convertToException(String body) { return new AADConditionalAccessException(claims); } - protected static class AADConditionalAccessException extends RuntimeException{ + protected static class AADConditionalAccessException extends RuntimeException { String claims; - protected AADConditionalAccessException(String claims){ + + protected AADConditionalAccessException(String claims) { this.claims = claims; } + public String getClaims() { return claims; } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java index c0683b882874e..d280cda5a7d67 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java @@ -28,8 +28,8 @@ import java.util.Arrays; /** - * Abstract configuration class, used to make AzureClientRegistrationRepository - * and AuthzCodeGrantRequestEntityConverter take effect. + * Abstract configuration class, used to make AzureClientRegistrationRepository and AuthzCodeGrantRequestEntityConverter + * take effect. */ public abstract class AADWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { @@ -96,10 +96,11 @@ protected AuthenticationFailureHandler failureHandler() { /** * Fix the default error info not displayed when the setting failureHandler in configure */ - private final static class AADHttpConfigurer extends AbstractHttpConfigurer{ + private static final class AADHttpConfigurer extends AbstractHttpConfigurer { @Override public void init(HttpSecurity http) { - DefaultLoginPageGeneratingFilter sharedObject = http.getSharedObject(DefaultLoginPageGeneratingFilter.class); + DefaultLoginPageGeneratingFilter sharedObject = + http.getSharedObject(DefaultLoginPageGeneratingFilter.class); sharedObject.setFailureUrl(DEFAULT_FAILURE_URL); } } From 33ac445954190fa9f8655650819859ee58c0a223 Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Tue, 12 Jan 2021 09:52:54 +0800 Subject: [PATCH 05/28] code format for checkStyle --- .../aad/webapi/AADOAuth2OboAuthorizedClientRepository.java | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java index e52f01ee0de20..895630ea9eae3 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java @@ -96,6 +96,7 @@ public T loadAuthorizedClient(String registra request.setAttribute(oboAuthorizedClientAttributeName, (T) oAuth2AuthorizedClient); return (T) oAuth2AuthorizedClient; } catch (Throwable throwable) { + // Handle conditional access policy for obo flow. String claims = Optional.of(throwable) .map(Throwable::getCause) .filter(e -> e instanceof MsalInteractionRequiredException) From 76b9b15821b9a96a296e35977a51749fdb90fbb6 Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Tue, 12 Jan 2021 17:40:29 +0800 Subject: [PATCH 06/28] add webClient handle conditional access policy. --- sdk/spring/azure-spring-boot/pom.xml | 1 + ...ADOAuth2OboAuthorizedClientRepository.java | 7 +-- .../aad/webapp/AADWebAppConfiguration.java | 60 ++++++------------- .../AADWebSecurityConfigurerAdapter.java | 5 +- .../webapp/ConditionalAccessException.java | 36 +++++++++++ 5 files changed, 59 insertions(+), 50 deletions(-) create mode 100644 sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java diff --git a/sdk/spring/azure-spring-boot/pom.xml b/sdk/spring/azure-spring-boot/pom.xml index 34e4b25d95bc3..aa89762b9344d 100644 --- a/sdk/spring/azure-spring-boot/pom.xml +++ b/sdk/spring/azure-spring-boot/pom.xml @@ -274,6 +274,7 @@ org.springframework spring-webflux 5.2.10.RELEASE + true diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java index 895630ea9eae3..4c7fcd9e7c462 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java @@ -3,7 +3,7 @@ package com.azure.spring.aad.webapi; -import com.azure.spring.autoconfigure.aad.Constants; +import com.azure.spring.aad.webapp.ConditionalAccessException; import com.microsoft.aad.msal4j.ClientCredentialFactory; import com.microsoft.aad.msal4j.ConfidentialClientApplication; import com.microsoft.aad.msal4j.IClientSecret; @@ -112,12 +112,11 @@ public T loadAuthorizedClient(String registra response.setStatus(HttpStatus.FORBIDDEN.value()); try { ServletOutputStream outputStream = response.getOutputStream(); - String result = Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS - + claims + Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS; + String result = ConditionalAccessException.claimsToHttpBody(claims); outputStream.write(result.getBytes(StandardCharsets.UTF_8)); outputStream.flush(); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("An exception occurred while operating the responseOutputStream.", e); } } LOGGER.error("Failed to load authorized client.", throwable); diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java index 78e23875621af..e8dc2934980be 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java @@ -4,8 +4,11 @@ package com.azure.spring.aad.webapp; import com.azure.spring.aad.AADAuthorizationServerEndpoints; +import com.azure.spring.aad.webapi.AADOAuth2OboAuthorizedClientRepository; import com.azure.spring.autoconfigure.aad.AADAuthenticationProperties; import com.azure.spring.autoconfigure.aad.Constants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -57,6 +60,8 @@ @EnableConfigurationProperties(AADAuthenticationProperties.class) public class AADWebAppConfiguration { + private static final Logger LOGGER = LoggerFactory.getLogger(AADOAuth2OboAuthorizedClientRepository.class); + @Autowired private AADAuthenticationProperties properties; @@ -135,12 +140,11 @@ private Set accessTokenScopes() { */ @ControllerAdvice public static class GlobalExceptionAdvice { - @ExceptionHandler(AADConditionalAccessException.class) + @ExceptionHandler(ConditionalAccessException.class) public void handleUserNotFound(HttpServletRequest request, HttpServletResponse response, Exception exception) { Optional.of(exception) - .filter(e -> e instanceof AADConditionalAccessException) - .map(e -> (AADConditionalAccessException) e) + .map(e -> (ConditionalAccessException) e) .ifPresent(aadConditionalAccessException -> { response.setStatus(302); SecurityContextHolder.clearContext(); @@ -149,7 +153,7 @@ public void handleUserNotFound(HttpServletRequest request, try { response.sendRedirect(request.getRequestURL().toString()); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("An exception occurred while redirecting url.", e); } }); } @@ -231,45 +235,15 @@ protected void configure(HttpSecurity http) throws Exception { } - public static ExchangeFilterFunction webClientErrorHandlingFilter() { - return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { - if (clientResponse.statusCode().is4xxClientError()) { - return clientResponse.bodyToMono(String.class) - .flatMap(errorBody -> { - if (isConditionalAccessError(errorBody)) { - return Mono.error(convertToException(errorBody)); - } - return Mono.just(clientResponse); - }); - } - return Mono.just(clientResponse); - } + public static ExchangeFilterFunction conditionalAccessExchangeFilterFunction() { + return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> + clientResponse.bodyToMono(String.class) + .flatMap(httpBody -> { + if (ConditionalAccessException.isConditionAccessException(httpBody)) { + return Mono.error(ConditionalAccessException.fromHttpBody(httpBody)); + } + return Mono.just(clientResponse); + }) ); } - - - private static boolean isConditionalAccessError(String body) { - return body.startsWith(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS); - } - - private static AADConditionalAccessException convertToException(String body) { - String claims = body.split(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)[1]; - return new AADConditionalAccessException(claims); - } - - protected static class AADConditionalAccessException extends RuntimeException { - String claims; - - protected AADConditionalAccessException(String claims) { - this.claims = claims; - } - - public String getClaims() { - return claims; - } - - public void setClaims(String claims) { - this.claims = claims; - } - } } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java index d280cda5a7d67..97c3131d3ec3d 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java @@ -33,7 +33,6 @@ */ public abstract class AADWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { - private static final String DEFAULT_FAILURE_URL = "/login?error"; @Autowired private AADWebAppClientRegistrationRepository repo; @Autowired @@ -99,9 +98,9 @@ protected AuthenticationFailureHandler failureHandler() { private static final class AADHttpConfigurer extends AbstractHttpConfigurer { @Override public void init(HttpSecurity http) { - DefaultLoginPageGeneratingFilter sharedObject = + DefaultLoginPageGeneratingFilter filter = http.getSharedObject(DefaultLoginPageGeneratingFilter.class); - sharedObject.setFailureUrl(DEFAULT_FAILURE_URL); + filter.setFailureUrl("/login?error"); } } } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java new file mode 100644 index 0000000000000..8511f206a25f2 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.aad.webapp; + +import com.azure.spring.autoconfigure.aad.Constants; + +/** + * handle ConditionalAccess from obo flow. + */ +public final class ConditionalAccessException extends RuntimeException { + private final String claims; + + protected ConditionalAccessException(String claims) { + this.claims = claims; + } + + public String getClaims() { + return claims; + } + + public static ConditionalAccessException fromHttpBody(String httpBody) { + return new ConditionalAccessException(httpBodyToClaims(httpBody)); + } + + public static String httpBodyToClaims(String httpBody) { + return httpBody.split(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)[1]; + } + + public static String claimsToHttpBody(String claims) { + return Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS + claims + Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS; + } + + public static boolean isConditionAccessException(String httpBody) { + return httpBody.startsWith(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS); + } +} From 24a7bfddba8a21a01175568bf36ec3328e75bf06 Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Wed, 13 Jan 2021 10:27:58 +0800 Subject: [PATCH 07/28] add dependency to azure-spring-boot-starter-active-directory-pom --- .../pom.xml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) 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 b9828b34542f6..319d6e5a39004 100644 --- a/sdk/spring/azure-spring-boot-starter-active-directory/pom.xml +++ b/sdk/spring/azure-spring-boot-starter-active-directory/pom.xml @@ -45,6 +45,11 @@ spring-web 5.2.10.RELEASE + + org.springframework + spring-webflux + 5.2.10.RELEASE + org.springframework.security spring-security-core @@ -96,12 +101,16 @@ com.microsoft.azure:msal4j:[1.8.0] com.nimbusds:nimbus-jose-jwt:[8.19] io.projectreactor.netty:reactor-netty:[0.9.15.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-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-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-webflux:[5.2.10.RELEASE] org.springframework:spring-web:[5.2.10.RELEASE] From 6f3250cc0c53a43390206d5870b206c9dd453b8b Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Wed, 13 Jan 2021 14:01:08 +0800 Subject: [PATCH 08/28] add webflux to external_dependency. --- eng/versioning/external_dependencies.txt | 1 + .../pom.xml | 15 +++------------ .../aad/webapp/ConditionalAccessException.java | 2 +- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index 553847eebbefd..b879818c234b1 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -131,6 +131,7 @@ org.springframework:spring-messaging;5.2.10.RELEASE org.springframework:spring-tx;5.2.10.RELEASE org.springframework:spring-web;5.2.10.RELEASE org.springframework:spring-webmvc;5.2.10.RELEASE +org.springframework:spring-webflux;5.2.10.RELEASE # spring-boot-starter-parent is not managed by spring-boot-dependencies or spring-cloud-dependencies. org.springframework.boot:spring-boot-starter-parent;2.3.7.RELEASE 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 319d6e5a39004..b9828b34542f6 100644 --- a/sdk/spring/azure-spring-boot-starter-active-directory/pom.xml +++ b/sdk/spring/azure-spring-boot-starter-active-directory/pom.xml @@ -45,11 +45,6 @@ spring-web 5.2.10.RELEASE - - org.springframework - spring-webflux - 5.2.10.RELEASE - org.springframework.security spring-security-core @@ -101,16 +96,12 @@ com.microsoft.azure:msal4j:[1.8.0] com.nimbusds:nimbus-jose-jwt:[8.19] io.projectreactor.netty:reactor-netty:[0.9.15.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-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-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-webflux:[5.2.10.RELEASE] org.springframework:spring-web:[5.2.10.RELEASE] diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java index 8511f206a25f2..01b4dfd47fb3b 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java @@ -5,7 +5,7 @@ import com.azure.spring.autoconfigure.aad.Constants; /** - * handle ConditionalAccess from obo flow. + * Create ConditionalAccessException to handle conditionalAccess in obo flow. */ public final class ConditionalAccessException extends RuntimeException { private final String claims; From 694239305f9a4bae1604f665c45cef2a562284a6 Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Wed, 13 Jan 2021 14:20:36 +0800 Subject: [PATCH 09/28] Modify note. --- .../spring/aad/webapp/AADWebSecurityConfigurerAdapter.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java index 97c3131d3ec3d..1f4d5242990c5 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java @@ -28,8 +28,8 @@ import java.util.Arrays; /** - * Abstract configuration class, used to make AzureClientRegistrationRepository and AuthzCodeGrantRequestEntityConverter - * take effect. + * Abstract configuration class, used to make AzureClientRegistrationRepository + * and AuthzCodeGrantRequestEntityConverter take effect. */ public abstract class AADWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { @@ -93,7 +93,7 @@ protected AuthenticationFailureHandler failureHandler() { } /** - * Fix the default error info not displayed when the setting failureHandler in configure + * Fix the default error info not displayed when the setting failureHandler in securityConfigure */ private static final class AADHttpConfigurer extends AbstractHttpConfigurer { @Override From df4f6e2b2597644a216a40b2ff312d7afb33074d Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Tue, 19 Jan 2021 10:55:55 +0800 Subject: [PATCH 10/28] add svg for ConditionalAccessException --- .../doc-files/ConditionalAccessException.svg | 554 ++++++++++++++++++ 1 file changed, 554 insertions(+) create mode 100644 sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/doc-files/ConditionalAccessException.svg diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/doc-files/ConditionalAccessException.svg b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/doc-files/ConditionalAccessException.svg new file mode 100644 index 0000000000000..9b3a0cbe263ef --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/doc-files/ConditionalAccessException.svg @@ -0,0 +1,554 @@ + + + + + + + + + + Web application + + + + + + + + + + Azure AD + + + + + + + + + + + + + + + Web API A  + + + + + + + + + + +
+
+
+ Acquire access token
 for Web API B +
+
+
+
+
+ Acquire access to... +
+
+
+ + + + + + + + + Web API B + + + + + + + + + + +
+
+
+ User is already authenticated,
and acquire token for Web API A. +
+
+
+
+
+
+ User is already authenti... +
+
+
+ + + + + + + + + +
+
+
1
+
+
+
+ 1 +
+
+
+ + + + + + + + + +
+
+
+ Call Web API A with  + access token A  +
+
+
+
+ Call Web API A with access t... +
+
+
+ + + + + +
+
+
2
+
+
+
+ 2 +
+
+
+ + + + + + + + + +
+
+
+  Use Web API A’s access token to
 acquire Web API B's access token
 by On-Behalf-Of request. +
+
+
+
+
+ Use Web API A’s access tok... +
+
+
+ + + + + +
+
+
3
+
+
+
+ 3 +
+
+
+ + + + + + + + + +
+
+
+ + Azure Security required that who access Web API B must do multifactor authentication. + +
+
+
+
+ Azure Security req... +
+
+
+ + + + + + + + + +
+
+
+ The application has been configured conditional Access policies( + + such as + +  multi-factor authentication) in Azure Security. +
+
+
+
+ The application... +
+
+
+ + + + + +
+
+
+
+ Return error to Web API A. +
+
+ ( + The claims field  +
+
+ in error is importan + t + ) + +   + +
+
+
+
+
+ Return error to Web... +
+
+
+ + + + + +
+
+
4
+
+
+
+ 4 +
+
+
+ + + + + +
+
+
+ Return the claims field 
 to Web Application +
+
+
+
+
+ Return the claims... +
+
+
+ + + + + +
+
+
5
+
+
+
+ 5 +
+
+
+ + + + + + + + + + + + + + +
+
+
+ Return source date  +
+
+
+
+ Return source d... +
+
+
+ + + + + +
+
+
6
+
+
+
+ 6 +
+
+
+ + + + + +
+
+
7
+
+
+
+ 7 +
+
+
+ + + + + +
+
+
+ Call Web API A with  +
+
+ access token A  +
+
+
+
+ Call Web API A wi... +
+
+
+ + + + + +
+
+
+ User carries the claims field
 to re-authorize.(User need do
multi-factor-authentication
to get new access token.)   +
+
+
+
+
+ User carries the claims... +
+
+
+ + + + + + + + + +
+
+
8
+
+
+
+ 8 +
+
+
+ + + + + + + + + +
+
+
9
+
+
+
+ 9 +
+
+
+ + + + + + + + + +
+
+
+ Return access token B +
+
+
+
+ Return access toke... +
+
+
+ + + + + + + + + +
+
+
10
+
+
+
+ 10 +
+
+
+ + + + + +
+
+
+ + Call Web API B with
  +
+ access token B  +
+
+
+
+
+
+ Call Web API B... +
+
+
+ + + + + +
+
+
11
+
+
+
+ 11 +
+
+
+
+ + + + Viewer does not support full SVG 1.1 + + +
\ No newline at end of file From 34bd03ccf29f13da9e660c61e06ccd709d10fbaf Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Tue, 19 Jan 2021 10:57:09 +0800 Subject: [PATCH 11/28] add notes for ConditionalAccess. --- .../AADAuthenticationFailureHandler.java | 59 ----------- .../spring/aad/webapp/AADOAuth2Error.java | 62 ----------- .../AADWebSecurityConfigurerAdapter.java | 22 ---- .../webapp/ConditionalAccessException.java | 43 +++++++- ...ConditionalAccessResponseErrorHandler.java | 100 ------------------ 5 files changed, 42 insertions(+), 244 deletions(-) delete mode 100644 sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADAuthenticationFailureHandler.java delete mode 100644 sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADOAuth2Error.java delete mode 100644 sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessResponseErrorHandler.java diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADAuthenticationFailureHandler.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADAuthenticationFailureHandler.java deleted file mode 100644 index 2a9ddaeb78b92..0000000000000 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADAuthenticationFailureHandler.java +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.spring.aad.webapp; - -import com.azure.spring.autoconfigure.aad.Constants; -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; - -/** - * Redirect URL for handling OAuthentication failure - */ -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 { - String claims = Optional.of(exception) - .filter(e -> e instanceof OAuth2AuthenticationException) - .map(e -> (OAuth2AuthenticationException) e) - .map(OAuth2AuthenticationException::getError) - .filter(e -> e instanceof AADOAuth2Error) - .map(e -> (AADOAuth2Error) e) - .map(AADOAuth2Error::getClaims) - .orElse(null); - - if (claims == null) { - // Default handle logic - defaultHandler.onAuthenticationFailure(request, response, exception); - } else { - // Handle conditional access policy, step 2. - response.setStatus(302); - request.getSession().setAttribute(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, claims); - 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/aad/webapp/AADOAuth2Error.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADOAuth2Error.java deleted file mode 100644 index a1a3696e9957f..0000000000000 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADOAuth2Error.java +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.spring.aad.webapp; - -import org.springframework.security.core.SpringSecurityCoreVersion; -import org.springframework.security.oauth2.core.OAuth2Error; - -/** - * Custom error with the error code returned by aad - */ -public class AADOAuth2Error extends OAuth2Error { - private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; - - private final String errorCodes; - - private final String timestamp; - - private final String traceId; - - private final String correlationId; - - private final String subError; - - private final String claims; - - public AADOAuth2Error(String error, String errorDescription, String errorCodes, String timestamp, - String traceId, String correlationId, String errorUri, String subError, String claims) { - super(error, errorDescription, errorUri); - this.errorCodes = errorCodes; - this.timestamp = timestamp; - this.traceId = traceId; - this.correlationId = correlationId; - this.subError = subError; - this.claims = claims; - } - - - public final String getErrorCodes() { - return errorCodes; - } - - public final String getTimestamp() { - return timestamp; - } - - public final String getTraceId() { - return traceId; - } - - public final String getCorrelationId() { - return correlationId; - } - - public final String getSubError() { - return subError; - } - - public final String getClaims() { - return claims; - } -} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java index 1f4d5242990c5..65c7b9ea3cb22 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java @@ -54,8 +54,6 @@ protected void configure(HttpSecurity http) throws Exception { .oidcUserService(oidcUserService) .and() .and() - .apply(new AADHttpConfigurer()) - .and() .logout() .logoutSuccessHandler(oidcLogoutSuccessHandler()) .and(); @@ -75,10 +73,6 @@ protected LogoutSuccessHandler oidcLogoutSuccessHandler() { protected OAuth2AccessTokenResponseClient accessTokenResponseClient() { DefaultAuthorizationCodeTokenResponseClient result = new DefaultAuthorizationCodeTokenResponseClient(); - RestTemplate restTemplate = new RestTemplate(Arrays.asList( - new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter())); - restTemplate.setErrorHandler(new ConditionalAccessResponseErrorHandler()); - result.setRestOperations(restTemplate); result.setRequestEntityConverter( new AADOAuth2AuthorizationCodeGrantRequestEntityConverter(repo.getAzureClient())); return result; @@ -87,20 +81,4 @@ protected OAuth2AccessTokenResponseClient a protected OAuth2AuthorizationRequestResolver requestResolver() { return new AADOAuth2AuthorizationRequestResolver(this.repo); } - - protected AuthenticationFailureHandler failureHandler() { - return new AADAuthenticationFailureHandler(); - } - - /** - * Fix the default error info not displayed when the setting failureHandler in securityConfigure - */ - private static final class AADHttpConfigurer extends AbstractHttpConfigurer { - @Override - public void init(HttpSecurity http) { - DefaultLoginPageGeneratingFilter filter = - http.getSharedObject(DefaultLoginPageGeneratingFilter.class); - filter.setFailureUrl("/login?error"); - } - } } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java index 01b4dfd47fb3b..bcbd12ee7fbb5 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java @@ -2,10 +2,51 @@ // Licensed under the MIT License. package com.azure.spring.aad.webapp; +import com.azure.spring.aad.webapi.AADOAuth2OboAuthorizedClientRepository; import com.azure.spring.autoconfigure.aad.Constants; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; /** - * Create ConditionalAccessException to handle conditionalAccess in obo flow. + * An exception handle Conditional Access in On-Behalf-Of flow. + * + *

+ * On-Behalf-Of allows you to exchange an access token that your API received for an access token to another API. For + * better understanding On-Behalf-Of, the reference documentation can help us. See the Microsoft identity + * platform and OAuth 2.0 On-Behalf-Of flow + * + *

+ * Conditional Access is the tool used by Azure Active Directory to bring signals together, to make decisions, and + * enforce organizational policies. The reference documentation is + * Azure AD Conditional Access + * documentation + * + *

+ * + * + *

+ * Step 3,4,5,6 describe Conditional Access(such as multi-factor authentication, see the Conditional + * Access: Require MFA for all users) in obo flow: + * + *

+ * step 3: {@link AADOAuth2OboAuthorizedClientRepository} sends the OBO Request to AAD. + * + *

+ * step 4 : AAD Conditional Access occurs and return an error(The claims field in this error is the reauthorization + * certificate). + * + *

+ * step 5: {@link AADOAuth2OboAuthorizedClientRepository}get the claims field create a response by {@link + * #claimsToHttpBody(String)}. + * + *

+ * step 6: {@link AADWebAppConfiguration#conditionalAccessExchangeFilterFunction()} receives the response and convert it + * into {@link ConditionalAccessException}. {@link AADWebAppConfiguration.GlobalExceptionAdvice} can catch this + * exception and put the claims field into session. then clear authorization information and redirect. At last {@link + * AADOAuth2AuthorizationRequestResolver} intercepts authorization-url, put claims into {@link + * OAuth2AuthorizationRequest} to reauthorize. */ public final class ConditionalAccessException extends RuntimeException { private final String claims; diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessResponseErrorHandler.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessResponseErrorHandler.java deleted file mode 100644 index 1acce08fb812b..0000000000000 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessResponseErrorHandler.java +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.spring.aad.webapp; - -import com.nimbusds.oauth2.sdk.token.BearerTokenError; -import org.springframework.core.convert.converter.Converter; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.security.oauth2.core.OAuth2AuthorizationException; -import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter; -import org.springframework.util.StringUtils; -import org.springframework.web.client.DefaultResponseErrorHandler; -import org.springframework.web.client.ResponseErrorHandler; - -import java.io.IOException; -import java.util.Map; - -/** - * Handle conditional access. - */ -public class ConditionalAccessResponseErrorHandler implements ResponseErrorHandler { - - private final OAuth2ErrorHttpMessageConverter oauth2ErrorConverter = new OAuth2ErrorHttpMessageConverter(); - - private final ResponseErrorHandler defaultErrorHandler = new DefaultResponseErrorHandler(); - - protected ConditionalAccessResponseErrorHandler() { - this.oauth2ErrorConverter.setErrorConverter(new AADOAuth2ErrorConverter()); - } - - @Override - public boolean hasError(ClientHttpResponse response) throws IOException { - return this.defaultErrorHandler.hasError(response); - } - - @Override - public void handleError(ClientHttpResponse response) throws IOException { - - if (!HttpStatus.BAD_REQUEST.equals(response.getStatusCode())) { - this.defaultErrorHandler.handleError(response); - } - - // A Bearer Token Error may be in the WWW-Authenticate response header - OAuth2Error oauth2Error = this.readErrorFromWwwAuthenticate(response.getHeaders()); - if (oauth2Error == null) { - oauth2Error = this.oauth2ErrorConverter.read(OAuth2Error.class, response); - } - /** - * Handle conditional access policy, step 1. - * https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-all-users-mfa - */ - throw new OAuth2AuthorizationException(oauth2Error); - } - - private OAuth2Error readErrorFromWwwAuthenticate(HttpHeaders headers) { - String wwwAuthenticateHeader = headers.getFirst(HttpHeaders.WWW_AUTHENTICATE); - if (!StringUtils.hasText(wwwAuthenticateHeader)) { - return null; - } - - BearerTokenError bearerTokenError; - try { - bearerTokenError = BearerTokenError.parse(wwwAuthenticateHeader); - } catch (Exception ex) { - return null; - } - - String errorCode = bearerTokenError.getCode() != null - ? bearerTokenError.getCode() : OAuth2ErrorCodes.SERVER_ERROR; - String errorDescription = bearerTokenError.getDescription(); - - String errorUri = bearerTokenError.getURI() != null - ? bearerTokenError.getURI().toString() : null; - - return new OAuth2Error(errorCode, errorDescription, errorUri); - } - - - private static class AADOAuth2ErrorConverter implements Converter, OAuth2Error> { - @Override - public OAuth2Error convert(Map parameters) { - String errorCode = parameters.get("error"); - String description = parameters.get("error_description"); - String errorCodes = parameters.get("error_codes"); - String timestamp = parameters.get("timestamp"); - String traceId = parameters.get("trace_id"); - String correlationId = parameters.get("correlation_id"); - String uri = parameters.get("error_uri"); - String subError = parameters.get("suberror"); - String claims = parameters.get("claims"); - - return new AADOAuth2Error(errorCode, description, errorCodes, timestamp, traceId, correlationId, - uri, subError, claims); - } - } -} From 0d3ec7ba7601aedb6a03fd8c3805b6ac2f32e4b6 Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Tue, 19 Jan 2021 18:15:31 +0800 Subject: [PATCH 12/28] update sample for ConditionalAccess --- .../aad/controller/SampleController.java | 2 +- .../src/main/resources/application.yml | 3 + .../sample/aad/config/WebClientConfig.java | 52 ++++++++++++++++ .../controller/CallOboServerController.java | 60 +++++++++++++++++++ .../src/main/resources/application.yml | 5 +- .../doc-files/ConditionalAccessException.svg | 20 +++---- .../aad/webapp/AADWebAppConfiguration.java | 4 +- .../AADWebSecurityConfigurerAdapter.java | 10 +--- .../webapp/ConditionalAccessException.java | 19 +++--- ...itionalAccessResponseErrorHandlerTest.java | 56 ----------------- 10 files changed, 145 insertions(+), 86 deletions(-) create mode 100644 sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/config/WebClientConfig.java create mode 100644 sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/controller/CallOboServerController.java delete mode 100644 sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/aad/webapp/ConditionalAccessResponseErrorHandlerTest.java diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-resource-server-obo/src/main/java/com/azure/spring/sample/aad/controller/SampleController.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-resource-server-obo/src/main/java/com/azure/spring/sample/aad/controller/SampleController.java index 0ca6afc267e1b..4c90780e41012 100644 --- a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-resource-server-obo/src/main/java/com/azure/spring/sample/aad/controller/SampleController.java +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-resource-server-obo/src/main/java/com/azure/spring/sample/aad/controller/SampleController.java @@ -28,7 +28,7 @@ public class SampleController { private static final String GRAPH_ME_ENDPOINT = "https://graph.microsoft.com/v1.0/me"; - private static final String CUSTOM_LOCAL_FILE_ENDPOINT = "http://localhost:8080/file"; + private static final String CUSTOM_LOCAL_FILE_ENDPOINT = "http://localhost:8082/file"; @Autowired private WebClient webClient; diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-resource-server/src/main/resources/application.yml b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-resource-server/src/main/resources/application.yml index 76478852f5c83..069c3ddcda628 100644 --- a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-resource-server/src/main/resources/application.yml +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-resource-server/src/main/resources/application.yml @@ -2,6 +2,9 @@ # In v2.0 tokens, this is always the client ID of the API, while in v1.0 tokens it can be the client ID or the resource URI used in the request. # If you are using v1.0 tokens, configure both to properly complete the audience validation. +server: + port: 8082 + #azure: # activedirectory: # client-id: diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/config/WebClientConfig.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/config/WebClientConfig.java new file mode 100644 index 0000000000000..05a4fda748e60 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/config/WebClientConfig.java @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.sample.aad.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; + +import static com.azure.spring.aad.webapp.AADWebAppConfiguration.conditionalAccessExceptionFilterFunction; + +@Configuration +public class WebClientConfig { + + @Bean + public OAuth2AuthorizedClientManager authorizedClientManager( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository) { + + OAuth2AuthorizedClientProvider authorizedClientProvider = + OAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build(); + + DefaultOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; + } + + @Bean + public static WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { + ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = + new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + return WebClient.builder() + .apply(oauth2Client.oauth2Configuration()) + .filter(conditionalAccessExceptionFilterFunction()) + .build(); + } +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/controller/CallOboServerController.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/controller/CallOboServerController.java new file mode 100644 index 0000000000000..0095237308dee --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/controller/CallOboServerController.java @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.sample.aad.controller; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.reactive.function.client.WebClient; + + +import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient; + +@Controller +public class CallOboServerController { + + private static final Logger LOGGER = LoggerFactory.getLogger(CallOboServerController.class); + + private static final String CUSTOM_LOCAL_FILE_ENDPOINT = "http://localhost:8081/call-custom"; + + @Autowired + private WebClient webClient; + + /** + * Call obo server, combine all the response and return. + * @param obo authorized client for Custom + * @return Response Graph and Custom data. + */ + @GetMapping("/obo") + @ResponseBody + public String callOboServer(@RegisteredOAuth2AuthorizedClient("obo") OAuth2AuthorizedClient obo) { + return callOboEndpoint(obo); + } + + /** + * Call obo local file endpoint + * @param obo Authorized Client + * @return Response string data. + */ + private String callOboEndpoint(OAuth2AuthorizedClient obo) { + if (null != obo) { + String body = webClient + .get() + .uri(CUSTOM_LOCAL_FILE_ENDPOINT) + .attributes(oauth2AuthorizedClient(obo)) + .retrieve() + .bodyToMono(String.class) + .block(); + LOGGER.info("Response from Custom: {}", body); + return "Custom response " + (null != body ? "success." : "failed."); + } else { + return "Custom response failed."; + } + } +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/resources/application.yml b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/resources/application.yml index 883151793b791..e3fb009df7e87 100644 --- a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/resources/application.yml +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/resources/application.yml @@ -13,9 +13,12 @@ azure: - https://manage.office.com/ActivityFeed.Read - https://manage.office.com/ActivityFeed.ReadDlp - https://manage.office.com/ServiceHealth.Read + obo: + scopes: + - /File.Read client-id: client-secret: tenant-id: user-group: allowed-groups: group1, group2 - post-logout-redirect-uri: http://localhost:8080 + post-logout-redirect-uri: http://localhost:8080 \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/doc-files/ConditionalAccessException.svg b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/doc-files/ConditionalAccessException.svg index 9b3a0cbe263ef..a74e6d90f4af4 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/doc-files/ConditionalAccessException.svg +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/doc-files/ConditionalAccessException.svg @@ -1,7 +1,7 @@ - @@ -216,16 +216,16 @@ - - + + - +
+ style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 104px; height: 1px; padding-top: 135px; margin-left: 536px;">
The application has been configured conditional Access policies( @@ -237,7 +237,7 @@
- The application... + The application... @@ -259,7 +259,7 @@
in error is importan - t + t )   @@ -433,7 +433,7 @@
+ style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 9px; height: 1px; padding-top: 455px; margin-left: 310px;">
8
@@ -540,7 +540,7 @@
- 11 + 11 diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java index e8dc2934980be..cce8ff675d9d5 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java @@ -139,7 +139,7 @@ private Set accessTokenScopes() { * Handle conditional access error in obo flow. */ @ControllerAdvice - public static class GlobalExceptionAdvice { + public static class ConditionalAccessExceptionAdvice { @ExceptionHandler(ConditionalAccessException.class) public void handleUserNotFound(HttpServletRequest request, HttpServletResponse response, Exception exception) { @@ -235,7 +235,7 @@ protected void configure(HttpSecurity http) throws Exception { } - public static ExchangeFilterFunction conditionalAccessExchangeFilterFunction() { + public static ExchangeFilterFunction conditionalAccessExceptionFilterFunction() { return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> clientResponse.bodyToMono(String.class) .flatMap(httpBody -> { diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java index 65c7b9ea3cb22..5d54562442d5a 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java @@ -5,10 +5,8 @@ import com.azure.spring.autoconfigure.aad.AADAuthenticationProperties; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; @@ -16,16 +14,11 @@ import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; -import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; -import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.util.StringUtils; -import org.springframework.web.client.RestTemplate; import java.net.URI; -import java.util.Arrays; /** * Abstract configuration class, used to make AzureClientRegistrationRepository @@ -47,6 +40,9 @@ protected void configure(HttpSecurity http) throws Exception { .anyRequest().authenticated() .and() .oauth2Login() + .authorizationEndpoint() + .authorizationRequestResolver(requestResolver()) + .and() .tokenEndpoint() .accessTokenResponseClient(accessTokenResponseClient()) .and() diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java index bcbd12ee7fbb5..aed8899b492e1 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java @@ -4,8 +4,11 @@ import com.azure.spring.aad.webapi.AADOAuth2OboAuthorizedClientRepository; import com.azure.spring.autoconfigure.aad.Constants; +import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import javax.servlet.http.HttpServletRequest; + /** * An exception handle Conditional Access in On-Behalf-Of flow. * @@ -25,13 +28,11 @@ * * *

- * Step 3,4,5,6 describe Conditional Access(such as multi-factor authentication, see the Conditional - * Access: Require MFA for all users) in obo flow: + * Step 3,4,5,6 describe Conditional Access(such as multi-factor authentication) in obo flow: * *

- * step 3: {@link AADOAuth2OboAuthorizedClientRepository} sends the OBO Request to AAD. + * step 3: {@link AADOAuth2OboAuthorizedClientRepository#loadAuthorizedClient(String, Authentication, + * HttpServletRequest)} sends the OBO Request to AAD. * *

* step 4 : AAD Conditional Access occurs and return an error(The claims field in this error is the reauthorization @@ -42,10 +43,10 @@ * #claimsToHttpBody(String)}. * *

- * step 6: {@link AADWebAppConfiguration#conditionalAccessExchangeFilterFunction()} receives the response and convert it - * into {@link ConditionalAccessException}. {@link AADWebAppConfiguration.GlobalExceptionAdvice} can catch this - * exception and put the claims field into session. then clear authorization information and redirect. At last {@link - * AADOAuth2AuthorizationRequestResolver} intercepts authorization-url, put claims into {@link + * step 6: {@link AADWebAppConfiguration#conditionalAccessExceptionFilterFunction()} receives the response and convert + * it into {@link ConditionalAccessException}. {@link AADWebAppConfiguration.ConditionalAccessExceptionAdvice} can + * catch this exception and put the claims field into session. then clear the authorization information and redirect. At + * last {@link AADOAuth2AuthorizationRequestResolver} intercepts authorization-url, put claims into {@link * OAuth2AuthorizationRequest} to reauthorize. */ public final class ConditionalAccessException extends RuntimeException { diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/aad/webapp/ConditionalAccessResponseErrorHandlerTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/aad/webapp/ConditionalAccessResponseErrorHandlerTest.java deleted file mode 100644 index 8922efb79d574..0000000000000 --- a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/aad/webapp/ConditionalAccessResponseErrorHandlerTest.java +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.spring.aad.webapp; - -import org.junit.Assert; -import org.junit.Test; -import org.springframework.http.HttpStatus; -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.mock.http.client.MockClientHttpResponse; -import org.springframework.security.oauth2.core.OAuth2AuthorizationException; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.ResponseErrorHandler; - -import java.io.IOException; -import java.util.Optional; - -public class ConditionalAccessResponseErrorHandlerTest { - private ResponseErrorHandler azureHandler = new ConditionalAccessResponseErrorHandler(); - private ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(("{\n" - + " \"error\": \"fake_error\",\n" - + " \"error_description\": \" fake_error_description\",\n" - + " \"error_codes\": [\n" - + " 53001\n" - + " ],\n" - + " \"timestamp\": \"fake_timestamp\",\n" - + " \"trace_id\": \"fake_trace_id\",\n" - + " \"correlation_id\": \"fake_correlation_id\",\n" - + " \"error_uri\": \"fake_error_uri\",\n" - + " \"suberror\": \"message_only\",\n" - + " \"claims\": \"{\\\"access_token\\\":{\\\"fake_token\\\":{\\\"essential\\\":true," - + "\\\"values\\\":[\\\"fake_values\\\"]}}}\"\n" - + "}").getBytes(), HttpStatus.BAD_REQUEST); - - @Test - public void azureResponseErrorHandleTest() throws IOException { - AADOAuth2Error error = null; - try { - azureHandler.handleError(clientHttpResponse); - } catch (OAuth2AuthorizationException exception) { - error = (AADOAuth2Error) Optional.of(exception) - .map(OAuth2AuthorizationException::getError).orElse(null); - } - Assert.assertNotNull(error); - } - - @Test - public void defaultErrorHandlerTest() throws IOException { - clientHttpResponse = new MockClientHttpResponse("".getBytes(), HttpStatus.UNAUTHORIZED); - try { - azureHandler.handleError(clientHttpResponse); - } catch (HttpClientErrorException exception) { - Assert.assertNotNull(exception); - } - } -} From 502384fe139aa6fb3acfbd402dd296cbc6c570f9 Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Thu, 21 Jan 2021 10:45:46 +0800 Subject: [PATCH 13/28] resolve conversation --- sdk/spring/azure-spring-boot/pom.xml | 12 ++++++------ .../AADOAuth2OboAuthorizedClientRepository.java | 10 +++++++--- .../spring/aad/webapp/AADWebAppConfiguration.java | 4 ++-- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/sdk/spring/azure-spring-boot/pom.xml b/sdk/spring/azure-spring-boot/pom.xml index aa89762b9344d..5c79f9015fd16 100644 --- a/sdk/spring/azure-spring-boot/pom.xml +++ b/sdk/spring/azure-spring-boot/pom.xml @@ -99,6 +99,12 @@ 5.3.5.RELEASE true + + org.springframework + spring-webflux + 5.2.10.RELEASE + true + com.nimbusds nimbus-jose-jwt @@ -270,12 +276,6 @@ spring-core 5.2.10.RELEASE - - org.springframework - spring-webflux - 5.2.10.RELEASE - true - diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java index 4c7fcd9e7c462..8f125a0337f32 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java @@ -31,9 +31,11 @@ import java.io.IOException; import java.net.MalformedURLException; import java.nio.charset.StandardCharsets; +import java.text.ParseException; import java.time.Instant; import java.util.Date; import java.util.Optional; +import java.util.concurrent.ExecutionException; /** *

@@ -95,9 +97,9 @@ public T loadAuthorizedClient(String registra oAuth2AccessToken); request.setAttribute(oboAuthorizedClientAttributeName, (T) oAuth2AuthorizedClient); return (T) oAuth2AuthorizedClient; - } catch (Throwable throwable) { + } catch (ExecutionException exception) { // Handle conditional access policy for obo flow. - String claims = Optional.of(throwable) + String claims = Optional.of(exception) .map(Throwable::getCause) .filter(e -> e instanceof MsalInteractionRequiredException) .map(e -> (MsalInteractionRequiredException) e) @@ -119,7 +121,9 @@ public T loadAuthorizedClient(String registra LOGGER.error("An exception occurred while operating the responseOutputStream.", e); } } - LOGGER.error("Failed to load authorized client.", throwable); + LOGGER.error("Failed to load authorized client.", exception); + }catch (InterruptedException | ParseException exception) { + LOGGER.error("Failed to load authorized client.", exception); } return null; } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java index cce8ff675d9d5..d399c06f16f81 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java @@ -141,8 +141,8 @@ private Set accessTokenScopes() { @ControllerAdvice public static class ConditionalAccessExceptionAdvice { @ExceptionHandler(ConditionalAccessException.class) - public void handleUserNotFound(HttpServletRequest request, - HttpServletResponse response, Exception exception) { + public void handleConditionalAccessException(HttpServletRequest request, + HttpServletResponse response, Exception exception) { Optional.of(exception) .map(e -> (ConditionalAccessException) e) .ifPresent(aadConditionalAccessException -> { From 8a9e6bcdc1969527796fe8a3ab565d41243268ac Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Thu, 21 Jan 2021 10:49:06 +0800 Subject: [PATCH 14/28] resolve conversation --- .../aad/webapi/AADOAuth2OboAuthorizedClientRepository.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java index 8f125a0337f32..d7ff079218025 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java @@ -22,6 +22,7 @@ import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken; +import org.springframework.util.Assert; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; @@ -110,7 +111,7 @@ public T loadAuthorizedClient(String registra ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); HttpServletResponse response = attr.getResponse(); - assert response != null; + Assert.notNull(response,"HttpServletResponse should not be null."); response.setStatus(HttpStatus.FORBIDDEN.value()); try { ServletOutputStream outputStream = response.getOutputStream(); From 14ffa212dba1dbab96649c644e8a39e33ef0ed09 Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Mon, 25 Jan 2021 10:48:23 +0800 Subject: [PATCH 15/28] update webapp sample and webclient filter --- .../controller/CallOboServerController.java | 6 +++--- .../aad/webapp/AADWebAppConfiguration.java | 20 +++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/controller/CallOboServerController.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/controller/CallOboServerController.java index 0095237308dee..1849de2e92934 100644 --- a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/controller/CallOboServerController.java +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/controller/CallOboServerController.java @@ -51,10 +51,10 @@ private String callOboEndpoint(OAuth2AuthorizedClient obo) { .retrieve() .bodyToMono(String.class) .block(); - LOGGER.info("Response from Custom: {}", body); - return "Custom response " + (null != body ? "success." : "failed."); + LOGGER.info("Response from obo: {}", body); + return "obo response " + (null != body ? "success." : "failed."); } else { - return "Custom response failed."; + return "obo response failed."; } } } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java index d399c06f16f81..cf4fcd6b17f6a 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java @@ -236,14 +236,18 @@ protected void configure(HttpSecurity http) throws Exception { public static ExchangeFilterFunction conditionalAccessExceptionFilterFunction() { - return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> - clientResponse.bodyToMono(String.class) - .flatMap(httpBody -> { - if (ConditionalAccessException.isConditionAccessException(httpBody)) { - return Mono.error(ConditionalAccessException.fromHttpBody(httpBody)); - } - return Mono.just(clientResponse); - }) + return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { + if (clientResponse.statusCode().is4xxClientError()) { + return clientResponse.bodyToMono(String.class) + .flatMap(httpBody -> { + if (ConditionalAccessException.isConditionAccessException(httpBody)) { + return Mono.error(ConditionalAccessException.fromHttpBody(httpBody)); + } + return Mono.just(clientResponse); + }); + } + return Mono.just(clientResponse); + } ); } } From 96a4e27761dc041bd5ae91459d7e716eb64912eb Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Tue, 2 Feb 2021 11:11:23 +0800 Subject: [PATCH 16/28] add filter for conditionalAccess. --- .../controller/CallOboServerController.java | 4 +- .../src/main/resources/templates/index.html | 1 + ...ADOAuth2OboAuthorizedClientRepository.java | 48 ++++++++--------- .../aad/webapp/AADWebAppConfiguration.java | 45 +++------------- .../AADWebSecurityConfigurerAdapter.java | 4 +- .../webapp/ConditionalAccessException.java | 53 ++++++++++++++----- .../aad/webapp/ExceptionHandlerFilter.java | 44 +++++++++++++++ .../spring/autoconfigure/aad/Constants.java | 1 + 8 files changed, 121 insertions(+), 79 deletions(-) create mode 100644 sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ExceptionHandlerFilter.java diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/controller/CallOboServerController.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/controller/CallOboServerController.java index 1849de2e92934..88f475633f505 100644 --- a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/controller/CallOboServerController.java +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/controller/CallOboServerController.java @@ -52,9 +52,9 @@ private String callOboEndpoint(OAuth2AuthorizedClient obo) { .bodyToMono(String.class) .block(); LOGGER.info("Response from obo: {}", body); - return "obo response " + (null != body ? "success." : "failed."); + return "Obo response " + (null != body ? "success." : "failed."); } else { - return "obo response failed."; + return "Obo response failed."; } } } diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/resources/templates/index.html b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/resources/templates/index.html index 92db35dd29fbd..d53836446626f 100644 --- a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/resources/templates/index.html +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/resources/templates/index.html @@ -35,6 +35,7 @@

Azure Active Directory OAuth 2.0 Login with Spring Security

Graph Client | Office Client | Arm Client | + Obo Client | diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java index d7ff079218025..9e776d3776866 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java @@ -4,6 +4,7 @@ package com.azure.spring.aad.webapi; import com.azure.spring.aad.webapp.ConditionalAccessException; +import com.azure.spring.autoconfigure.aad.Constants; import com.microsoft.aad.msal4j.ClientCredentialFactory; import com.microsoft.aad.msal4j.ConfidentialClientApplication; import com.microsoft.aad.msal4j.IClientSecret; @@ -26,15 +27,14 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.io.IOException; import java.net.MalformedURLException; -import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.time.Instant; import java.util.Date; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutionException; @@ -100,30 +100,26 @@ public T loadAuthorizedClient(String registra return (T) oAuth2AuthorizedClient; } catch (ExecutionException exception) { // Handle conditional access policy for obo flow. - String claims = Optional.of(exception) - .map(Throwable::getCause) - .filter(e -> e instanceof MsalInteractionRequiredException) - .map(e -> (MsalInteractionRequiredException) e) - .map(MsalInteractionRequiredException::claims) - .orElse(null); - - if (claims != null) { - ServletRequestAttributes attr = - (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); - HttpServletResponse response = attr.getResponse(); - Assert.notNull(response,"HttpServletResponse should not be null."); - response.setStatus(HttpStatus.FORBIDDEN.value()); - try { - ServletOutputStream outputStream = response.getOutputStream(); - String result = ConditionalAccessException.claimsToHttpBody(claims); - outputStream.write(result.getBytes(StandardCharsets.UTF_8)); - outputStream.flush(); - } catch (IOException e) { - LOGGER.error("An exception occurred while operating the responseOutputStream.", e); - } - } + // A user interaction is required, but we are in a web API, and therefore, we need to report back to the + // client through a 'WWW-Authenticate' header https://tools.ietf.org/html/rfc6750#section-3.1 + Optional.of(exception) + .map(Throwable::getCause) + .filter(e -> e instanceof MsalInteractionRequiredException) + .map(e -> (MsalInteractionRequiredException) e) + .map(MsalInteractionRequiredException::claims) + .ifPresent(claims -> { + Map parameters = new HashMap<>(); + ServletRequestAttributes attr = + (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + HttpServletResponse response = attr.getResponse(); + Assert.notNull(response, "HttpServletResponse should not be null."); + response.setStatus(HttpStatus.FORBIDDEN.value()); + parameters.put(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, claims); + response.addHeader("WWWAuthenticate", + ConditionalAccessException.parametersToHttpHeader(parameters)); + }); LOGGER.error("Failed to load authorized client.", exception); - }catch (InterruptedException | ParseException exception) { + } catch (InterruptedException | ParseException exception) { LOGGER.error("Failed to load authorized client.", exception); } return null; diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java index cf4fcd6b17f6a..e4ba048dab922 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java @@ -6,7 +6,6 @@ import com.azure.spring.aad.AADAuthorizationServerEndpoints; import com.azure.spring.aad.webapi.AADOAuth2OboAuthorizedClientRepository; import com.azure.spring.autoconfigure.aad.AADAuthenticationProperties; -import com.azure.spring.autoconfigure.aad.Constants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -21,7 +20,6 @@ import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.context.SecurityContextHolder; 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; @@ -29,14 +27,9 @@ import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import reactor.core.publisher.Mono; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -135,29 +128,6 @@ private Set accessTokenScopes() { return result; } - /** - * Handle conditional access error in obo flow. - */ - @ControllerAdvice - public static class ConditionalAccessExceptionAdvice { - @ExceptionHandler(ConditionalAccessException.class) - public void handleConditionalAccessException(HttpServletRequest request, - HttpServletResponse response, Exception exception) { - Optional.of(exception) - .map(e -> (ConditionalAccessException) e) - .ifPresent(aadConditionalAccessException -> { - response.setStatus(302); - SecurityContextHolder.clearContext(); - request.getSession().setAttribute(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, - aadConditionalAccessException.getClaims()); - try { - response.sendRedirect(request.getRequestURL().toString()); - } catch (IOException e) { - LOGGER.error("An exception occurred while redirecting url.", e); - } - }); - } - } private Set openidScopes() { Set result = new HashSet<>(); @@ -237,14 +207,13 @@ protected void configure(HttpSecurity http) throws Exception { public static ExchangeFilterFunction conditionalAccessExceptionFilterFunction() { return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { - if (clientResponse.statusCode().is4xxClientError()) { - return clientResponse.bodyToMono(String.class) - .flatMap(httpBody -> { - if (ConditionalAccessException.isConditionAccessException(httpBody)) { - return Mono.error(ConditionalAccessException.fromHttpBody(httpBody)); - } - return Mono.just(clientResponse); - }); + if (clientResponse.statusCode().is4xxClientError() + && clientResponse.headers().header("WWWAuthenticate").get(0) != null) { + String httpHeader = clientResponse.headers().header("WWWAuthenticate").get(0); + if (ConditionalAccessException.isConditionAccessException(httpHeader)) { + return Mono.error(ConditionalAccessException.fromHttpHeader(httpHeader)); + } + return Mono.just(clientResponse); } return Mono.just(clientResponse); } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java index 5d54562442d5a..f5fa6f1766458 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java @@ -16,6 +16,7 @@ import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; import org.springframework.util.StringUtils; import java.net.URI; @@ -36,7 +37,8 @@ public abstract class AADWebSecurityConfigurerAdapter extends WebSecurityConfigu @Override protected void configure(HttpSecurity http) throws Exception { // @formatter:off - http.authorizeRequests() + http.addFilterBefore(new ExceptionHandlerFilter(), WebAsyncManagerIntegrationFilter.class) + .authorizeRequests() .anyRequest().authenticated() .and() .oauth2Login() diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java index aed8899b492e1..79d69338250c3 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java @@ -4,10 +4,15 @@ import com.azure.spring.aad.webapi.AADOAuth2OboAuthorizedClientRepository; import com.azure.spring.autoconfigure.aad.Constants; +import net.minidev.json.JSONObject; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; /** * An exception handle Conditional Access in On-Behalf-Of flow. @@ -40,13 +45,13 @@ * *

* step 5: {@link AADOAuth2OboAuthorizedClientRepository}get the claims field create a response by {@link - * #claimsToHttpBody(String)}. + * #parametersToHttpHeader(Map)}. * *

* step 6: {@link AADWebAppConfiguration#conditionalAccessExceptionFilterFunction()} receives the response and convert - * it into {@link ConditionalAccessException}. {@link AADWebAppConfiguration.ConditionalAccessExceptionAdvice} can - * catch this exception and put the claims field into session. then clear the authorization information and redirect. At - * last {@link AADOAuth2AuthorizationRequestResolver} intercepts authorization-url, put claims into {@link + * it into {@link ConditionalAccessException}. {@link ConditionalAccessException} can catch this exception and put the + * claims field into session. then clear the authorization information and redirect. At last {@link + * AADOAuth2AuthorizationRequestResolver} intercepts authorization-url, put claims into {@link * OAuth2AuthorizationRequest} to reauthorize. */ public final class ConditionalAccessException extends RuntimeException { @@ -60,19 +65,43 @@ public String getClaims() { return claims; } - public static ConditionalAccessException fromHttpBody(String httpBody) { - return new ConditionalAccessException(httpBodyToClaims(httpBody)); + public static ConditionalAccessException fromHttpHeader(String httpHeader) { + return new ConditionalAccessException( + (String) httpHeaderToParameters(httpHeader).get(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)); } - public static String httpBodyToClaims(String httpBody) { - return httpBody.split(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)[1]; + /** + * Convert httpHeader to map structure. + * + * @param httpHeader httpHeader + * @return Map Object + */ + public static Map httpHeaderToParameters(String httpHeader) { + // TODO I'm looking for a better way to achieve it. + String[] parameters = Optional.of(httpHeader) + .map(str -> str.substring(Constants.BEARER_PREFIX.length())) + .map(str -> str.replaceAll("[\"{}]", "")) + .map(str -> str.split(",")) + .orElse(null); + return Arrays.asList(parameters) + .stream() + .map(elem -> elem.split(":")) + .collect(Collectors.toMap(e -> e[0], e -> e[1])); } - public static String claimsToHttpBody(String claims) { - return Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS + claims + Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS; + /** + * Convert parameters to JsonString structure. + * + * @param parameters returned by webApi. + * @return String Object + */ + public static String parametersToHttpHeader(Map parameters) { + return Constants.BEARER_PREFIX + JSONObject.toJSONString(parameters); } - public static boolean isConditionAccessException(String httpBody) { - return httpBody.startsWith(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS); + public static boolean isConditionAccessException(String httpHeader) { + return httpHeader.startsWith(Constants.BEARER_PREFIX) + && ConditionalAccessException.httpHeaderToParameters(httpHeader) + .get(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS) != null; } } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ExceptionHandlerFilter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ExceptionHandlerFilter.java new file mode 100644 index 0000000000000..08137f16b72f8 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ExceptionHandlerFilter.java @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.aad.webapp; + +import com.azure.spring.autoconfigure.aad.Constants; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Optional; + +/** + * Handle the specified exception. + */ +public class ExceptionHandlerFilter extends OncePerRequestFilter { + + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws IOException, ServletException { + try { + filterChain.doFilter(request, response); + } catch (Exception exception) { + String claims = Optional.of(exception) + .map(Throwable::getCause) + .filter(e -> e instanceof ConditionalAccessException) + .map(e -> (ConditionalAccessException) e) + .map(ConditionalAccessException::getClaims) + .orElse(null); + if (claims != null) { + request.getSession().setAttribute(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, + claims); + response.setStatus(302); + response.sendRedirect(Constants.DEFAULT_AUTHORITY_ENDPOINT_URL); + return; + } + throw exception; + } + } +} 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 87be1cd2f4e23..0432f16153b68 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 @@ -19,6 +19,7 @@ public class Constants { public static final Set DEFAULT_AUTHORITY_SET; public static final String ROLE_PREFIX = "ROLE_"; public static final String SAVED_REQUEST = "SPRING_SECURITY_SAVED_REQUEST"; + public static final String DEFAULT_AUTHORITY_ENDPOINT_URL = "/oauth2/authorization/azure"; static { Set authoritySet = new HashSet<>(); From 7f1945cf8892a656b951cd90032a214ea068d728 Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Tue, 2 Feb 2021 14:43:43 +0800 Subject: [PATCH 17/28] resolve conflicts. --- .../spring/aad/webapp/AADWebSecurityConfigurerAdapter.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java index f5fa6f1766458..f1409689ede68 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java @@ -37,8 +37,7 @@ public abstract class AADWebSecurityConfigurerAdapter extends WebSecurityConfigu @Override protected void configure(HttpSecurity http) throws Exception { // @formatter:off - http.addFilterBefore(new ExceptionHandlerFilter(), WebAsyncManagerIntegrationFilter.class) - .authorizeRequests() + http.authorizeRequests() .anyRequest().authenticated() .and() .oauth2Login() @@ -54,7 +53,8 @@ protected void configure(HttpSecurity http) throws Exception { .and() .logout() .logoutSuccessHandler(oidcLogoutSuccessHandler()) - .and(); + .and() + .addFilterBefore(new ExceptionHandlerFilter(), WebAsyncManagerIntegrationFilter.class); // @formatter:off } From 162d5ac96f3d0437db0633e1267ed4a55622d78c Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Tue, 2 Feb 2021 18:45:29 +0800 Subject: [PATCH 18/28] update ConditionalAccessException.java --- ...ADOAuth2OboAuthorizedClientRepository.java | 1 + .../webapp/ConditionalAccessException.java | 24 ++++++++----------- .../aad/webapp/ExceptionHandlerFilter.java | 23 +++++++++++------- .../spring/autoconfigure/aad/Constants.java | 2 +- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java index 9e776d3776866..6f91f18688e72 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java @@ -115,6 +115,7 @@ public T loadAuthorizedClient(String registra Assert.notNull(response, "HttpServletResponse should not be null."); response.setStatus(HttpStatus.FORBIDDEN.value()); parameters.put(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, claims); + parameters.put(Constants.DEFAULT_AUTHORITY_ENDPOINT_URI, "/oauth2/authorization/azure"); response.addHeader("WWWAuthenticate", ConditionalAccessException.parametersToHttpHeader(parameters)); }); diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java index 79d69338250c3..b5bc0c8579a3e 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java @@ -4,7 +4,6 @@ import com.azure.spring.aad.webapi.AADOAuth2OboAuthorizedClientRepository; import com.azure.spring.autoconfigure.aad.Constants; -import net.minidev.json.JSONObject; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; @@ -55,19 +54,18 @@ * OAuth2AuthorizationRequest} to reauthorize. */ public final class ConditionalAccessException extends RuntimeException { - private final String claims; + private final Map parameters; - protected ConditionalAccessException(String claims) { - this.claims = claims; + protected ConditionalAccessException(Map parameters) { + this.parameters = parameters; } - public String getClaims() { - return claims; + public Map getParameters() { + return parameters; } public static ConditionalAccessException fromHttpHeader(String httpHeader) { - return new ConditionalAccessException( - (String) httpHeaderToParameters(httpHeader).get(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)); + return new ConditionalAccessException(ConditionalAccessException.httpHeaderToParameters(httpHeader)); } /** @@ -77,15 +75,13 @@ public static ConditionalAccessException fromHttpHeader(String httpHeader) { * @return Map Object */ public static Map httpHeaderToParameters(String httpHeader) { - // TODO I'm looking for a better way to achieve it. String[] parameters = Optional.of(httpHeader) - .map(str -> str.substring(Constants.BEARER_PREFIX.length())) - .map(str -> str.replaceAll("[\"{}]", "")) - .map(str -> str.split(",")) + .map(str -> str.substring(Constants.BEARER_PREFIX.length() + 1,str.length() - 1)) + .map(str -> str.split(", ")) .orElse(null); return Arrays.asList(parameters) .stream() - .map(elem -> elem.split(":")) + .map(elem -> elem.split("=")) .collect(Collectors.toMap(e -> e[0], e -> e[1])); } @@ -96,7 +92,7 @@ public static Map httpHeaderToParameters(String httpHeader) { * @return String Object */ public static String parametersToHttpHeader(Map parameters) { - return Constants.BEARER_PREFIX + JSONObject.toJSONString(parameters); + return Constants.BEARER_PREFIX + parameters.toString(); } public static boolean isConditionAccessException(String httpHeader) { diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ExceptionHandlerFilter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ExceptionHandlerFilter.java index 08137f16b72f8..572418c9fb200 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ExceptionHandlerFilter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ExceptionHandlerFilter.java @@ -4,6 +4,8 @@ package com.azure.spring.aad.webapp; import com.azure.spring.autoconfigure.aad.Constants; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; @@ -11,6 +13,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Map; import java.util.Optional; /** @@ -18,6 +21,8 @@ */ public class ExceptionHandlerFilter extends OncePerRequestFilter { + @Autowired + OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, @@ -25,17 +30,17 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse try { filterChain.doFilter(request, response); } catch (Exception exception) { - String claims = Optional.of(exception) - .map(Throwable::getCause) - .filter(e -> e instanceof ConditionalAccessException) - .map(e -> (ConditionalAccessException) e) - .map(ConditionalAccessException::getClaims) - .orElse(null); - if (claims != null) { + Map parameters = Optional.of(exception) + .map(Throwable::getCause) + .filter(e -> e instanceof ConditionalAccessException) + .map(e -> (ConditionalAccessException) e) + .map(ConditionalAccessException::getParameters) + .orElse(null); + if (parameters != null && parameters.get(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS) != null) { request.getSession().setAttribute(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, - claims); + parameters.get(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)); response.setStatus(302); - response.sendRedirect(Constants.DEFAULT_AUTHORITY_ENDPOINT_URL); + response.sendRedirect(parameters.get(Constants.DEFAULT_AUTHORITY_ENDPOINT_URI).toString()); return; } throw exception; 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 0432f16153b68..024b8087cff9f 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 @@ -19,7 +19,7 @@ public class Constants { public static final Set DEFAULT_AUTHORITY_SET; public static final String ROLE_PREFIX = "ROLE_"; public static final String SAVED_REQUEST = "SPRING_SECURITY_SAVED_REQUEST"; - public static final String DEFAULT_AUTHORITY_ENDPOINT_URL = "/oauth2/authorization/azure"; + public static final String DEFAULT_AUTHORITY_ENDPOINT_URI = "DEFAULT_AUTHORITY_ENDPOINT_URI"; static { Set authoritySet = new HashSet<>(); From f103badcd9ff176a811f8cdb8467abe91bb7c7a7 Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Wed, 3 Feb 2021 09:27:18 +0800 Subject: [PATCH 19/28] fix code style. --- .../com/azure/spring/aad/webapp/ConditionalAccessException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java index b5bc0c8579a3e..a3a79a909d1e2 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java @@ -76,7 +76,7 @@ public static ConditionalAccessException fromHttpHeader(String httpHeader) { */ public static Map httpHeaderToParameters(String httpHeader) { String[] parameters = Optional.of(httpHeader) - .map(str -> str.substring(Constants.BEARER_PREFIX.length() + 1,str.length() - 1)) + .map(str -> str.substring(Constants.BEARER_PREFIX.length() + 1, str.length() - 1)) .map(str -> str.split(", ")) .orElse(null); return Arrays.asList(parameters) From 47daa01d5a1500ae6ef3703185fcd59128895511 Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Thu, 4 Feb 2021 14:15:20 +0800 Subject: [PATCH 20/28] when re-authentication, update all clients. --- .../azure/spring/aad/webapp/ConditionalAccessException.java | 2 +- .../com/azure/spring/aad/webapp/ExceptionHandlerFilter.java | 5 ----- .../JacksonHttpSessionOAuth2AuthorizedClientRepository.java | 6 ++++++ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java index a3a79a909d1e2..3c38dda6ed257 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java @@ -48,7 +48,7 @@ * *

* step 6: {@link AADWebAppConfiguration#conditionalAccessExceptionFilterFunction()} receives the response and convert - * it into {@link ConditionalAccessException}. {@link ConditionalAccessException} can catch this exception and put the + * it into {@link ConditionalAccessException}. {@link ExceptionHandlerFilter} can catch this exception and put the * claims field into session. then clear the authorization information and redirect. At last {@link * AADOAuth2AuthorizationRequestResolver} intercepts authorization-url, put claims into {@link * OAuth2AuthorizationRequest} to reauthorize. diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ExceptionHandlerFilter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ExceptionHandlerFilter.java index 572418c9fb200..f826ee12dbed8 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ExceptionHandlerFilter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ExceptionHandlerFilter.java @@ -4,8 +4,6 @@ package com.azure.spring.aad.webapp; import com.azure.spring.autoconfigure.aad.Constants; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; @@ -21,9 +19,6 @@ */ public class ExceptionHandlerFilter extends OncePerRequestFilter { - @Autowired - OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository; - @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/JacksonHttpSessionOAuth2AuthorizedClientRepository.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/JacksonHttpSessionOAuth2AuthorizedClientRepository.java index a47809813f94b..edccfe4743893 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/JacksonHttpSessionOAuth2AuthorizedClientRepository.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/JacksonHttpSessionOAuth2AuthorizedClientRepository.java @@ -62,6 +62,12 @@ public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authen Assert.notNull(authorizedClient, "authorizedClient cannot be null"); Assert.notNull(request, "request cannot be null"); Assert.notNull(response, "response cannot be null"); + if (authorizedClient.getClientRegistration().getRegistrationId().equals("azure")) { + Map authorizedClients = new HashMap<>(); + authorizedClients.put(authorizedClient.getClientRegistration().getRegistrationId(), authorizedClient); + request.getSession().setAttribute(AUTHORIZED_CLIENTS_ATTR_NAME, toString(authorizedClients)); + return; + } Map authorizedClients = this.getAuthorizedClients(request); authorizedClients.put(authorizedClient.getClientRegistration().getRegistrationId(), authorizedClient); request.getSession().setAttribute(AUTHORIZED_CLIENTS_ATTR_NAME, toString(authorizedClients)); From 23964083213bd1f0738a9384612c34d9fa65b8e5 Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Tue, 9 Feb 2021 19:20:10 +0800 Subject: [PATCH 21/28] remove ConditionalAccessException.java --- .../configuration/AADSampleConfiguration.java | 24 +- .../sample/aad/config/WebClientConfig.java | 37 +- .../controller/CallOboServerController.java | 6 +- .../doc-files/ConditionalAccessException.svg | 554 ------------------ ...ADOAuth2OboAuthorizedClientRepository.java | 39 +- .../aad/webapp/AADWebAppConfiguration.java | 18 - .../webapp/ConditionalAccessException.java | 103 ---- .../aad/webapp/ExceptionHandlerFilter.java | 72 ++- ...ssionOAuth2AuthorizedClientRepository.java | 6 - .../spring/autoconfigure/aad/Constants.java | 2 +- 10 files changed, 101 insertions(+), 760 deletions(-) delete mode 100644 sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/doc-files/ConditionalAccessException.svg delete mode 100644 sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-resource-server-obo/src/main/java/com/azure/spring/sample/aad/configuration/AADSampleConfiguration.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-resource-server-obo/src/main/java/com/azure/spring/sample/aad/configuration/AADSampleConfiguration.java index c1f5d8b283bc0..41ff9abd115e4 100644 --- a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-resource-server-obo/src/main/java/com/azure/spring/sample/aad/configuration/AADSampleConfiguration.java +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-resource-server-obo/src/main/java/com/azure/spring/sample/aad/configuration/AADSampleConfiguration.java @@ -15,25 +15,13 @@ public class AADSampleConfiguration { @Bean - public OAuth2AuthorizedClientManager authorizedClientManager( - ClientRegistrationRepository clientRegistrationRepository, - OAuth2AuthorizedClientRepository authorizedClientRepository) { - OAuth2AuthorizedClientProvider authorizedClientProvider = - OAuth2AuthorizedClientProviderBuilder.builder() - .refreshToken() - .build(); - DefaultOAuth2AuthorizedClientManager authorizedClientManager = - new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - return authorizedClientManager; - } - - @Bean - public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { - ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = - new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + public static WebClient webClient(ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository) { + ServletOAuth2AuthorizedClientExchangeFilterFunction function = + new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository, + authorizedClientRepository); return WebClient.builder() - .apply(oauth2Client.oauth2Configuration()) + .apply(function.oauth2Configuration()) .build(); } } diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/config/WebClientConfig.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/config/WebClientConfig.java index 05a4fda748e60..846c28b5c15c6 100644 --- a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/config/WebClientConfig.java +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/config/WebClientConfig.java @@ -5,48 +5,23 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction; import org.springframework.web.reactive.function.client.WebClient; -import static com.azure.spring.aad.webapp.AADWebAppConfiguration.conditionalAccessExceptionFilterFunction; @Configuration public class WebClientConfig { @Bean - public OAuth2AuthorizedClientManager authorizedClientManager( - ClientRegistrationRepository clientRegistrationRepository, - OAuth2AuthorizedClientRepository authorizedClientRepository) { - - OAuth2AuthorizedClientProvider authorizedClientProvider = - OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build(); - - DefaultOAuth2AuthorizedClientManager authorizedClientManager = - new DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - return authorizedClientManager; - } - - @Bean - public static WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { - ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = - new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + public static WebClient webClient(ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository) { + ServletOAuth2AuthorizedClientExchangeFilterFunction function = + new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository, + authorizedClientRepository); return WebClient.builder() - .apply(oauth2Client.oauth2Configuration()) - .filter(conditionalAccessExceptionFilterFunction()) + .apply(function.oauth2Configuration()) .build(); } } diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/controller/CallOboServerController.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/controller/CallOboServerController.java index 88f475633f505..1a5f1564fd193 100644 --- a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/controller/CallOboServerController.java +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/java/com/azure/spring/sample/aad/controller/CallOboServerController.java @@ -51,10 +51,10 @@ private String callOboEndpoint(OAuth2AuthorizedClient obo) { .retrieve() .bodyToMono(String.class) .block(); - LOGGER.info("Response from obo: {}", body); - return "Obo response " + (null != body ? "success." : "failed."); + LOGGER.info("Response from obo server: {}", body); + return "Obo server response " + (null != body ? "success." : "failed."); } else { - return "Obo response failed."; + return "Obo server response failed."; } } } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/doc-files/ConditionalAccessException.svg b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/doc-files/ConditionalAccessException.svg deleted file mode 100644 index a74e6d90f4af4..0000000000000 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/doc-files/ConditionalAccessException.svg +++ /dev/null @@ -1,554 +0,0 @@ - - - - - - - - - - Web application - - - - - - - - - - Azure AD - - - - - - - - - - - - - - - Web API A  - - - - - - - - - - -
-
-
- Acquire access token
 for Web API B -
-
-
-
-
- Acquire access to... -
-
-
- - - - - - - - - Web API B - - - - - - - - - - -
-
-
- User is already authenticated,
and acquire token for Web API A. -
-
-
-
-
-
- User is already authenti... -
-
-
- - - - - - - - - -
-
-
1
-
-
-
- 1 -
-
-
- - - - - - - - - -
-
-
- Call Web API A with  - access token A  -
-
-
-
- Call Web API A with access t... -
-
-
- - - - - -
-
-
2
-
-
-
- 2 -
-
-
- - - - - - - - - -
-
-
-  Use Web API A’s access token to
 acquire Web API B's access token
 by On-Behalf-Of request. -
-
-
-
-
- Use Web API A’s access tok... -
-
-
- - - - - -
-
-
3
-
-
-
- 3 -
-
-
- - - - - - - - - -
-
-
- - Azure Security required that who access Web API B must do multifactor authentication. - -
-
-
-
- Azure Security req... -
-
-
- - - - - - - - - -
-
-
- The application has been configured conditional Access policies( - - such as - -  multi-factor authentication) in Azure Security. -
-
-
-
- The application... -
-
-
- - - - - -
-
-
-
- Return error to Web API A. -
-
- ( - The claims field  -
-
- in error is importan - t - ) - -   - -
-
-
-
-
- Return error to Web... -
-
-
- - - - - -
-
-
4
-
-
-
- 4 -
-
-
- - - - - -
-
-
- Return the claims field 
 to Web Application -
-
-
-
-
- Return the claims... -
-
-
- - - - - -
-
-
5
-
-
-
- 5 -
-
-
- - - - - - - - - - - - - - -
-
-
- Return source date  -
-
-
-
- Return source d... -
-
-
- - - - - -
-
-
6
-
-
-
- 6 -
-
-
- - - - - -
-
-
7
-
-
-
- 7 -
-
-
- - - - - -
-
-
- Call Web API A with  -
-
- access token A  -
-
-
-
- Call Web API A wi... -
-
-
- - - - - -
-
-
- User carries the claims field
 to re-authorize.(User need do
multi-factor-authentication
to get new access token.)   -
-
-
-
-
- User carries the claims... -
-
-
- - - - - - - - - -
-
-
8
-
-
-
- 8 -
-
-
- - - - - - - - - -
-
-
9
-
-
-
- 9 -
-
-
- - - - - - - - - -
-
-
- Return access token B -
-
-
-
- Return access toke... -
-
-
- - - - - - - - - -
-
-
10
-
-
-
- 10 -
-
-
- - - - - -
-
-
- - Call Web API B with
  -
- access token B  -
-
-
-
-
-
- Call Web API B... -
-
-
- - - - - -
-
-
11
-
-
-
- 11 -
-
-
-
- - - - Viewer does not support full SVG 1.1 - - -
\ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java index 6f91f18688e72..a5fe871a6df4c 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java @@ -3,7 +3,6 @@ package com.azure.spring.aad.webapi; -import com.azure.spring.aad.webapp.ConditionalAccessException; import com.azure.spring.autoconfigure.aad.Constants; import com.microsoft.aad.msal4j.ClientCredentialFactory; import com.microsoft.aad.msal4j.ConfidentialClientApplication; @@ -15,6 +14,7 @@ import com.nimbusds.jwt.JWTParser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; @@ -22,6 +22,8 @@ import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken; import org.springframework.util.Assert; import org.springframework.web.context.request.RequestContextHolder; @@ -33,7 +35,7 @@ import java.text.ParseException; import java.time.Instant; import java.util.Date; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutionException; @@ -106,19 +108,14 @@ public T loadAuthorizedClient(String registra .map(Throwable::getCause) .filter(e -> e instanceof MsalInteractionRequiredException) .map(e -> (MsalInteractionRequiredException) e) - .map(MsalInteractionRequiredException::claims) - .ifPresent(claims -> { - Map parameters = new HashMap<>(); - ServletRequestAttributes attr = - (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); - HttpServletResponse response = attr.getResponse(); - Assert.notNull(response, "HttpServletResponse should not be null."); - response.setStatus(HttpStatus.FORBIDDEN.value()); - parameters.put(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, claims); - parameters.put(Constants.DEFAULT_AUTHORITY_ENDPOINT_URI, "/oauth2/authorization/azure"); - response.addHeader("WWWAuthenticate", - ConditionalAccessException.parametersToHttpHeader(parameters)); - }); + .ifPresent( + msalInteractionRequiredException -> { + ServletRequestAttributes attr = + (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + HttpServletResponse response = attr.getResponse(); + Assert.notNull(response, "HttpServletResponse should not be null."); + replyForbiddenWithWwwAuthenticateHeader(response, msalInteractionRequiredException); + }); LOGGER.error("Failed to load authorized client.", exception); } catch (InterruptedException | ParseException exception) { LOGGER.error("Failed to load authorized client.", exception); @@ -164,4 +161,16 @@ private String interceptAuthorizationUri(String authorizationUri) { } return null; } + + void replyForbiddenWithWwwAuthenticateHeader(HttpServletResponse response, + MsalInteractionRequiredException exception) { + Map parameters = new LinkedHashMap<>(); + response.setStatus(HttpStatus.FORBIDDEN.value()); + parameters.put(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, + exception.claims()); + parameters.put(OAuth2ParameterNames.ERROR, OAuth2ErrorCodes.INVALID_TOKEN); + parameters.put(OAuth2ParameterNames.ERROR_DESCRIPTION, "The resource server requires higher privileges than " + + "provided by the access token"); + response.addHeader(HttpHeaders.WWW_AUTHENTICATE, Constants.BEARER_PREFIX + parameters.toString()); + } } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java index e4ba048dab922..e8f84f18b89f6 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java @@ -27,8 +27,6 @@ import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import org.springframework.web.reactive.function.client.ExchangeFilterFunction; -import reactor.core.publisher.Mono; import java.util.ArrayList; import java.util.Collection; @@ -203,20 +201,4 @@ protected void configure(HttpSecurity http) throws Exception { super.configure(http); } } - - - public static ExchangeFilterFunction conditionalAccessExceptionFilterFunction() { - return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { - if (clientResponse.statusCode().is4xxClientError() - && clientResponse.headers().header("WWWAuthenticate").get(0) != null) { - String httpHeader = clientResponse.headers().header("WWWAuthenticate").get(0); - if (ConditionalAccessException.isConditionAccessException(httpHeader)) { - return Mono.error(ConditionalAccessException.fromHttpHeader(httpHeader)); - } - return Mono.just(clientResponse); - } - return Mono.just(clientResponse); - } - ); - } } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java deleted file mode 100644 index 3c38dda6ed257..0000000000000 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ConditionalAccessException.java +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.aad.webapp; - -import com.azure.spring.aad.webapi.AADOAuth2OboAuthorizedClientRepository; -import com.azure.spring.autoconfigure.aad.Constants; -import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; - -import javax.servlet.http.HttpServletRequest; -import java.util.Arrays; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; - -/** - * An exception handle Conditional Access in On-Behalf-Of flow. - * - *

- * On-Behalf-Of allows you to exchange an access token that your API received for an access token to another API. For - * better understanding On-Behalf-Of, the reference documentation can help us. See the Microsoft identity - * platform and OAuth 2.0 On-Behalf-Of flow - * - *

- * Conditional Access is the tool used by Azure Active Directory to bring signals together, to make decisions, and - * enforce organizational policies. The reference documentation is - * Azure AD Conditional Access - * documentation - * - *

- * - * - *

- * Step 3,4,5,6 describe Conditional Access(such as multi-factor authentication) in obo flow: - * - *

- * step 3: {@link AADOAuth2OboAuthorizedClientRepository#loadAuthorizedClient(String, Authentication, - * HttpServletRequest)} sends the OBO Request to AAD. - * - *

- * step 4 : AAD Conditional Access occurs and return an error(The claims field in this error is the reauthorization - * certificate). - * - *

- * step 5: {@link AADOAuth2OboAuthorizedClientRepository}get the claims field create a response by {@link - * #parametersToHttpHeader(Map)}. - * - *

- * step 6: {@link AADWebAppConfiguration#conditionalAccessExceptionFilterFunction()} receives the response and convert - * it into {@link ConditionalAccessException}. {@link ExceptionHandlerFilter} can catch this exception and put the - * claims field into session. then clear the authorization information and redirect. At last {@link - * AADOAuth2AuthorizationRequestResolver} intercepts authorization-url, put claims into {@link - * OAuth2AuthorizationRequest} to reauthorize. - */ -public final class ConditionalAccessException extends RuntimeException { - private final Map parameters; - - protected ConditionalAccessException(Map parameters) { - this.parameters = parameters; - } - - public Map getParameters() { - return parameters; - } - - public static ConditionalAccessException fromHttpHeader(String httpHeader) { - return new ConditionalAccessException(ConditionalAccessException.httpHeaderToParameters(httpHeader)); - } - - /** - * Convert httpHeader to map structure. - * - * @param httpHeader httpHeader - * @return Map Object - */ - public static Map httpHeaderToParameters(String httpHeader) { - String[] parameters = Optional.of(httpHeader) - .map(str -> str.substring(Constants.BEARER_PREFIX.length() + 1, str.length() - 1)) - .map(str -> str.split(", ")) - .orElse(null); - return Arrays.asList(parameters) - .stream() - .map(elem -> elem.split("=")) - .collect(Collectors.toMap(e -> e[0], e -> e[1])); - } - - /** - * Convert parameters to JsonString structure. - * - * @param parameters returned by webApi. - * @return String Object - */ - public static String parametersToHttpHeader(Map parameters) { - return Constants.BEARER_PREFIX + parameters.toString(); - } - - public static boolean isConditionAccessException(String httpHeader) { - return httpHeader.startsWith(Constants.BEARER_PREFIX) - && ConditionalAccessException.httpHeaderToParameters(httpHeader) - .get(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS) != null; - } -} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ExceptionHandlerFilter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ExceptionHandlerFilter.java index f826ee12dbed8..60c0689a0de12 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ExceptionHandlerFilter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ExceptionHandlerFilter.java @@ -3,8 +3,14 @@ package com.azure.spring.aad.webapp; +import com.azure.spring.aad.webapi.AADOAuth2OboAuthorizedClientRepository; import com.azure.spring.autoconfigure.aad.Constants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.reactive.function.client.WebClientResponseException; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -13,32 +19,76 @@ import java.io.IOException; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Handle the specified exception. */ public class ExceptionHandlerFilter extends OncePerRequestFilter { + private static final Logger LOGGER = LoggerFactory.getLogger(AADOAuth2OboAuthorizedClientRepository.class); + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { try { filterChain.doFilter(request, response); } catch (Exception exception) { - Map parameters = Optional.of(exception) - .map(Throwable::getCause) - .filter(e -> e instanceof ConditionalAccessException) - .map(e -> (ConditionalAccessException) e) - .map(ConditionalAccessException::getParameters) - .orElse(null); - if (parameters != null && parameters.get(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS) != null) { - request.getSession().setAttribute(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, - parameters.get(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)); - response.setStatus(302); - response.sendRedirect(parameters.get(Constants.DEFAULT_AUTHORITY_ENDPOINT_URI).toString()); + WebClientResponseException webClientResponseException = + Optional.of(exception) + .map(Throwable::getCause) + .filter(e -> e instanceof WebClientResponseException) + .map(e -> (WebClientResponseException) e) + .filter(ExceptionHandlerFilter::isConditionalAccessExceptionFromObo) + .orElse(null); + if (webClientResponseException != null) { + handleConditionalAccess(webClientResponseException, request, response); return; } throw exception; } } + + private static boolean isConditionalAccessExceptionFromObo(WebClientResponseException exception) { + String result = Optional.of(exception) + .map(WebClientResponseException::getHeaders) + .map(httpHeaders -> httpHeaders.get(HttpHeaders.WWW_AUTHENTICATE)) + .map(list -> list.get(0)) + .filter(value -> value.contains(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)) + .orElse(null); + return result != null; + } + + static void handleConditionalAccess(WebClientResponseException exception, HttpServletRequest request, + HttpServletResponse response) { + Map parameters = + Optional.of(exception) + .map(WebClientResponseException::getHeaders) + .map(httpHeaders -> httpHeaders.get(HttpHeaders.WWW_AUTHENTICATE)) + .map(list -> list.get(0)) + .map(ExceptionHandlerFilter::parseAuthParameters) + .orElse(null); + request.getSession().setAttribute(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, + parameters.get(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)); + response.setStatus(302); + try { + response.sendRedirect(Constants.DEFAULT_AUTHORITY_ENDPOINT_URI); + } catch (IOException e) { + LOGGER.error("Failed to load authorized client.", exception); + } + } + + private static Map parseAuthParameters(String wwwAuthenticateHeader) { + return Stream.of(wwwAuthenticateHeader) + .filter(header -> !StringUtils.isEmpty(header)) + .map(str -> str.substring(Constants.BEARER_PREFIX.length() + 1, str.length() - 1)) + .map(str -> str.split(", ")) + .flatMap(Stream::of) + .map(parameter -> parameter.split("=")) + .filter(parameter -> parameter.length > 1) + .collect(Collectors.toMap( + parameters -> parameters[0], + parameters -> parameters[1])); + } } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/JacksonHttpSessionOAuth2AuthorizedClientRepository.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/JacksonHttpSessionOAuth2AuthorizedClientRepository.java index edccfe4743893..a47809813f94b 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/JacksonHttpSessionOAuth2AuthorizedClientRepository.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/JacksonHttpSessionOAuth2AuthorizedClientRepository.java @@ -62,12 +62,6 @@ public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authen Assert.notNull(authorizedClient, "authorizedClient cannot be null"); Assert.notNull(request, "request cannot be null"); Assert.notNull(response, "response cannot be null"); - if (authorizedClient.getClientRegistration().getRegistrationId().equals("azure")) { - Map authorizedClients = new HashMap<>(); - authorizedClients.put(authorizedClient.getClientRegistration().getRegistrationId(), authorizedClient); - request.getSession().setAttribute(AUTHORIZED_CLIENTS_ATTR_NAME, toString(authorizedClients)); - return; - } Map authorizedClients = this.getAuthorizedClients(request); authorizedClients.put(authorizedClient.getClientRegistration().getRegistrationId(), authorizedClient); request.getSession().setAttribute(AUTHORIZED_CLIENTS_ATTR_NAME, toString(authorizedClients)); 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 024b8087cff9f..e37205c1ca84f 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 @@ -19,7 +19,7 @@ public class Constants { public static final Set DEFAULT_AUTHORITY_SET; public static final String ROLE_PREFIX = "ROLE_"; public static final String SAVED_REQUEST = "SPRING_SECURITY_SAVED_REQUEST"; - public static final String DEFAULT_AUTHORITY_ENDPOINT_URI = "DEFAULT_AUTHORITY_ENDPOINT_URI"; + public static final String DEFAULT_AUTHORITY_ENDPOINT_URI = "/oauth2/authorization/azure"; static { Set authoritySet = new HashSet<>(); From bd0c5cdbaa7c3ca1ded10e74e74ef4b54cbc5afe Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Wed, 10 Feb 2021 11:06:36 +0800 Subject: [PATCH 22/28] resolve conflicts. --- ... => AADHandleConditionalAccessFilter.java} | 30 ++++++++++++++----- .../AADWebSecurityConfigurerAdapter.java | 24 +++++++-------- 2 files changed, 33 insertions(+), 21 deletions(-) rename sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/{ExceptionHandlerFilter.java => AADHandleConditionalAccessFilter.java} (72%) diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ExceptionHandlerFilter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADHandleConditionalAccessFilter.java similarity index 72% rename from sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ExceptionHandlerFilter.java rename to sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADHandleConditionalAccessFilter.java index 60c0689a0de12..33b72b4f89523 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/ExceptionHandlerFilter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADHandleConditionalAccessFilter.java @@ -23,9 +23,25 @@ import java.util.stream.Stream; /** - * Handle the specified exception. + * Handle the {@link WebClientResponseException} in On-Behalf-Of flow. + * + *

+ * User carries token to access webapi or other resources, but the server requires higher privileges,so user need to + * carries the claims field(returned by Azure Security) re-acquire token. + * + *

+ * On-Behalf-Of allows you to exchange an access token that your API received for an access token to another API. For + * better understanding On-Behalf-Of, the reference documentation can help us. See the Microsoftidentity + * platform and OAuth 2.0 On-Behalf-Of flow + * + *

+ * Conditional Access is the tool used by Azure Active Directory to bring signals together, to make decisions, and + * enforce organizational policies. The reference documentation is + * Azure AD Conditional Access + * documentation */ -public class ExceptionHandlerFilter extends OncePerRequestFilter { +public class AADHandleConditionalAccessFilter extends OncePerRequestFilter { private static final Logger LOGGER = LoggerFactory.getLogger(AADOAuth2OboAuthorizedClientRepository.class); @@ -40,7 +56,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse .map(Throwable::getCause) .filter(e -> e instanceof WebClientResponseException) .map(e -> (WebClientResponseException) e) - .filter(ExceptionHandlerFilter::isConditionalAccessExceptionFromObo) + .filter(AADHandleConditionalAccessFilter::isConditionalAccessExceptionFromObo) .orElse(null); if (webClientResponseException != null) { handleConditionalAccess(webClientResponseException, request, response); @@ -62,20 +78,20 @@ private static boolean isConditionalAccessExceptionFromObo(WebClientResponseExce static void handleConditionalAccess(WebClientResponseException exception, HttpServletRequest request, HttpServletResponse response) { - Map parameters = + Map authParameters = Optional.of(exception) .map(WebClientResponseException::getHeaders) .map(httpHeaders -> httpHeaders.get(HttpHeaders.WWW_AUTHENTICATE)) .map(list -> list.get(0)) - .map(ExceptionHandlerFilter::parseAuthParameters) + .map(AADHandleConditionalAccessFilter::parseAuthParameters) .orElse(null); request.getSession().setAttribute(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, - parameters.get(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)); + authParameters.get(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)); response.setStatus(302); try { response.sendRedirect(Constants.DEFAULT_AUTHORITY_ENDPOINT_URI); } catch (IOException e) { - LOGGER.error("Failed to load authorized client.", exception); + LOGGER.error("Failed to redirect at this response.", exception); } } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java index 09d00f40a062f..2b66e52833d79 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java @@ -5,7 +5,6 @@ import com.azure.spring.autoconfigure.aad.AADAuthenticationProperties; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient; @@ -15,19 +14,16 @@ import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; -import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; import org.springframework.util.StringUtils; -import org.springframework.web.client.RestTemplate; import java.net.URI; -import java.util.Arrays; /** - * Abstract configuration class, used to make AzureClientRegistrationRepository - * and AuthzCodeGrantRequestEntityConverter take effect. + * Abstract configuration class, used to make AzureClientRegistrationRepository and AuthzCodeGrantRequestEntityConverter + * take effect. */ public abstract class AADWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { @@ -42,6 +38,9 @@ public abstract class AADWebSecurityConfigurerAdapter extends WebSecurityConfigu protected void configure(HttpSecurity http) throws Exception { // @formatter:off http.oauth2Login() + .authorizationEndpoint() + .authorizationRequestResolver(requestResolver()) + .and() .tokenEndpoint() .accessTokenResponseClient(accessTokenResponseClient()) .and() @@ -51,7 +50,8 @@ protected void configure(HttpSecurity http) throws Exception { .and() .logout() .logoutSuccessHandler(oidcLogoutSuccessHandler()) - .and(); + .and() + .addFilterBefore(handleConditionalAccessFilter(),WebAsyncManagerIntegrationFilter.class); // @formatter:off } @@ -68,10 +68,6 @@ protected LogoutSuccessHandler oidcLogoutSuccessHandler() { protected OAuth2AccessTokenResponseClient accessTokenResponseClient() { DefaultAuthorizationCodeTokenResponseClient result = new DefaultAuthorizationCodeTokenResponseClient(); - RestTemplate restTemplate = new RestTemplate(Arrays.asList( - new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter())); - restTemplate.setErrorHandler(new ConditionalAccessResponseErrorHandler()); - result.setRestOperations(restTemplate); result.setRequestEntityConverter( new AADOAuth2AuthorizationCodeGrantRequestEntityConverter(repo.getAzureClient())); return result; @@ -81,7 +77,7 @@ protected OAuth2AuthorizationRequestResolver requestResolver() { return new AADOAuth2AuthorizationRequestResolver(this.repo); } - protected AuthenticationFailureHandler failureHandler() { - return new AADAuthenticationFailureHandler(); + protected AADHandleConditionalAccessFilter handleConditionalAccessFilter(){ + return new AADHandleConditionalAccessFilter(); } } From 9da07e22aca4a0b63e60a89d064e62cf82f4195c Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Wed, 10 Feb 2021 11:24:51 +0800 Subject: [PATCH 23/28] Solve pipeline problems. --- .../spring/aad/webapp/AADWebSecurityConfigurerAdapter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java index 2b66e52833d79..915ea1b2e9892 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java @@ -51,7 +51,7 @@ protected void configure(HttpSecurity http) throws Exception { .logout() .logoutSuccessHandler(oidcLogoutSuccessHandler()) .and() - .addFilterBefore(handleConditionalAccessFilter(),WebAsyncManagerIntegrationFilter.class); + .addFilterBefore(handleConditionalAccessFilter(), WebAsyncManagerIntegrationFilter.class); // @formatter:off } @@ -77,7 +77,7 @@ protected OAuth2AuthorizationRequestResolver requestResolver() { return new AADOAuth2AuthorizationRequestResolver(this.repo); } - protected AADHandleConditionalAccessFilter handleConditionalAccessFilter(){ + protected AADHandleConditionalAccessFilter handleConditionalAccessFilter() { return new AADHandleConditionalAccessFilter(); } } From 6ff1f95f01131ef3f5f55b1dfbf83822ee0bc009 Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Thu, 18 Feb 2021 10:41:05 +0800 Subject: [PATCH 24/28] Solve pipeline problems. --- .../AADHandleConditionalAccessFilter.java | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADHandleConditionalAccessFilter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADHandleConditionalAccessFilter.java index 33b72b4f89523..a16686656c749 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADHandleConditionalAccessFilter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADHandleConditionalAccessFilter.java @@ -26,20 +26,9 @@ * Handle the {@link WebClientResponseException} in On-Behalf-Of flow. * *

- * User carries token to access webapi or other resources, but the server requires higher privileges,so user need to - * carries the claims field(returned by Azure Security) re-acquire token. - * - *

- * On-Behalf-Of allows you to exchange an access token that your API received for an access token to another API. For - * better understanding On-Behalf-Of, the reference documentation can help us. See the Microsoftidentity - * platform and OAuth 2.0 On-Behalf-Of flow - * - *

- * Conditional Access is the tool used by Azure Active Directory to bring signals together, to make decisions, and - * enforce organizational policies. The reference documentation is - * Azure AD Conditional Access - * documentation + * When the Web API needs re-acquire token(The request requires higher privileges than provided by the access token in + * On-Behalf-Of flow.), it can sent a 403 with information in the WWW-Authenticate header to web client ,web client + * will throw {@link WebClientResponseException}, Web APP can handle this exception to challenge the user. */ public class AADHandleConditionalAccessFilter extends OncePerRequestFilter { From 707c22dc5114a964dc8985f9b8e899ddd11df962 Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Sat, 20 Feb 2021 18:10:59 +0800 Subject: [PATCH 25/28] resolve conversation. --- sdk/spring/azure-spring-boot/pom.xml | 6 ++ ...ADOAuth2OboAuthorizedClientRepository.java | 3 +- .../AADHandleConditionalAccessFilter.java | 58 +++++++------------ .../aad/webapp/AADWebAppConfiguration.java | 5 -- 4 files changed, 27 insertions(+), 45 deletions(-) diff --git a/sdk/spring/azure-spring-boot/pom.xml b/sdk/spring/azure-spring-boot/pom.xml index 419738ea070b7..685d16fe4539f 100644 --- a/sdk/spring/azure-spring-boot/pom.xml +++ b/sdk/spring/azure-spring-boot/pom.xml @@ -276,6 +276,12 @@ spring-core 5.2.10.RELEASE + + org.springframework + spring-webflux + 5.2.10.RELEASE + compile + diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java index a5fe871a6df4c..9580e09effa76 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java @@ -166,8 +166,7 @@ void replyForbiddenWithWwwAuthenticateHeader(HttpServletResponse response, MsalInteractionRequiredException exception) { Map parameters = new LinkedHashMap<>(); response.setStatus(HttpStatus.FORBIDDEN.value()); - parameters.put(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, - exception.claims()); + parameters.put(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, exception.claims()); parameters.put(OAuth2ParameterNames.ERROR, OAuth2ErrorCodes.INVALID_TOKEN); parameters.put(OAuth2ParameterNames.ERROR_DESCRIPTION, "The resource server requires higher privileges than " + "provided by the access token"); diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADHandleConditionalAccessFilter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADHandleConditionalAccessFilter.java index a16686656c749..bb0a13439673c 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADHandleConditionalAccessFilter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADHandleConditionalAccessFilter.java @@ -3,7 +3,6 @@ package com.azure.spring.aad.webapp; -import com.azure.spring.aad.webapi.AADOAuth2OboAuthorizedClientRepository; import com.azure.spring.autoconfigure.aad.Constants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,13 +25,14 @@ * Handle the {@link WebClientResponseException} in On-Behalf-Of flow. * *

- * When the Web API needs re-acquire token(The request requires higher privileges than provided by the access token in - * On-Behalf-Of flow.), it can sent a 403 with information in the WWW-Authenticate header to web client ,web client - * will throw {@link WebClientResponseException}, Web APP can handle this exception to challenge the user. + * When the resource-server needs re-acquire token(The request requires higher privileges than provided by the access + * token in On-Behalf-Of flow.), it can sent a 403 with information in the WWW-Authenticate header to web client ,web + * client will throw {@link WebClientResponseException}, web-application can handle this exception to challenge the + * user. */ public class AADHandleConditionalAccessFilter extends OncePerRequestFilter { - private static final Logger LOGGER = LoggerFactory.getLogger(AADOAuth2OboAuthorizedClientRepository.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AADHandleConditionalAccessFilter.class); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, @@ -40,53 +40,35 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse try { filterChain.doFilter(request, response); } catch (Exception exception) { - WebClientResponseException webClientResponseException = + Map authParameters = Optional.of(exception) .map(Throwable::getCause) .filter(e -> e instanceof WebClientResponseException) .map(e -> (WebClientResponseException) e) - .filter(AADHandleConditionalAccessFilter::isConditionalAccessExceptionFromObo) + .map(WebClientResponseException::getHeaders) + .map(httpHeaders -> httpHeaders.get(HttpHeaders.WWW_AUTHENTICATE)) + .map(list -> list.get(0)) + .map(AADHandleConditionalAccessFilter::parseAuthParameters) .orElse(null); - if (webClientResponseException != null) { - handleConditionalAccess(webClientResponseException, request, response); + if (authParameters != null && authParameters.containsKey(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)) { + request.getSession().setAttribute(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, + authParameters.get(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)); + response.setStatus(302); + try { + response.sendRedirect(Constants.DEFAULT_AUTHORITY_ENDPOINT_URI); + } catch (IOException e) { + LOGGER.error("Failed to redirect at this response.", exception); + } return; } throw exception; } } - private static boolean isConditionalAccessExceptionFromObo(WebClientResponseException exception) { - String result = Optional.of(exception) - .map(WebClientResponseException::getHeaders) - .map(httpHeaders -> httpHeaders.get(HttpHeaders.WWW_AUTHENTICATE)) - .map(list -> list.get(0)) - .filter(value -> value.contains(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)) - .orElse(null); - return result != null; - } - - static void handleConditionalAccess(WebClientResponseException exception, HttpServletRequest request, - HttpServletResponse response) { - Map authParameters = - Optional.of(exception) - .map(WebClientResponseException::getHeaders) - .map(httpHeaders -> httpHeaders.get(HttpHeaders.WWW_AUTHENTICATE)) - .map(list -> list.get(0)) - .map(AADHandleConditionalAccessFilter::parseAuthParameters) - .orElse(null); - request.getSession().setAttribute(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, - authParameters.get(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)); - response.setStatus(302); - try { - response.sendRedirect(Constants.DEFAULT_AUTHORITY_ENDPOINT_URI); - } catch (IOException e) { - LOGGER.error("Failed to redirect at this response.", exception); - } - } - private static Map parseAuthParameters(String wwwAuthenticateHeader) { return Stream.of(wwwAuthenticateHeader) .filter(header -> !StringUtils.isEmpty(header)) + .filter(header -> header.startsWith(Constants.BEARER_PREFIX)) .map(str -> str.substring(Constants.BEARER_PREFIX.length() + 1, str.length() - 1)) .map(str -> str.split(", ")) .flatMap(Stream::of) diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java index 4083cfd8299e5..e24505eef3c9b 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebAppConfiguration.java @@ -4,10 +4,7 @@ package com.azure.spring.aad.webapp; import com.azure.spring.aad.AADAuthorizationServerEndpoints; -import com.azure.spring.aad.webapi.AADOAuth2OboAuthorizedClientRepository; import com.azure.spring.autoconfigure.aad.AADAuthenticationProperties; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -51,8 +48,6 @@ @EnableConfigurationProperties(AADAuthenticationProperties.class) public class AADWebAppConfiguration { - private static final Logger LOGGER = LoggerFactory.getLogger(AADOAuth2OboAuthorizedClientRepository.class); - @Autowired private AADAuthenticationProperties properties; From d2259e8ab6ea02a05e7d8129c310fed65830a307 Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Mon, 22 Feb 2021 10:24:11 +0800 Subject: [PATCH 26/28] add web-flux dependency to aad stater --- sdk/spring/azure-spring-boot/pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sdk/spring/azure-spring-boot/pom.xml b/sdk/spring/azure-spring-boot/pom.xml index 66c3ccfe2285d..4ad893edb6c38 100644 --- a/sdk/spring/azure-spring-boot/pom.xml +++ b/sdk/spring/azure-spring-boot/pom.xml @@ -270,6 +270,12 @@ spring-core 5.2.10.RELEASE + + org.springframework + spring-webflux + 5.2.10.RELEASE + true + org.apache.httpcomponents httpclient @@ -302,6 +308,7 @@ org.springframework:spring-core:[5.2.10.RELEASE] org.springframework:spring-web:[5.2.10.RELEASE] org.springframework:spring-jms:[5.2.10.RELEASE] + org.springframework:spring-webflux:[5.2.10.RELEASE] org.springframework.boot:spring-boot-actuator-autoconfigure:[2.3.5.RELEASE] org.springframework.boot:spring-boot-autoconfigure-processor:[2.3.5.RELEASE] org.springframework.boot:spring-boot-autoconfigure:[2.3.5.RELEASE] From ab6be45f9f96b6ccb5e69279113e533c1916f8f5 Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Mon, 22 Feb 2021 14:25:25 +0800 Subject: [PATCH 27/28] add web-flux dependency to aad stater --- sdk/spring/azure-spring-boot/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/spring/azure-spring-boot/pom.xml b/sdk/spring/azure-spring-boot/pom.xml index 4ad893edb6c38..be4a84fe2821f 100644 --- a/sdk/spring/azure-spring-boot/pom.xml +++ b/sdk/spring/azure-spring-boot/pom.xml @@ -273,8 +273,8 @@ org.springframework spring-webflux - 5.2.10.RELEASE true + 5.2.10.RELEASE org.apache.httpcomponents From bfb52b57e5122c350ce60df1f0618091a052d87a Mon Sep 17 00:00:00 2001 From: v-gaoh Date: Mon, 22 Feb 2021 15:55:30 +0800 Subject: [PATCH 28/28] resolve conversation. --- sdk/spring/azure-spring-boot/pom.xml | 4 ++-- ...AADOAuth2OboAuthorizedClientRepository.java | 18 +++++++----------- .../AADHandleConditionalAccessFilter.java | 4 ++-- .../AADWebSecurityConfigurerAdapter.java | 8 ++------ 4 files changed, 13 insertions(+), 21 deletions(-) diff --git a/sdk/spring/azure-spring-boot/pom.xml b/sdk/spring/azure-spring-boot/pom.xml index be4a84fe2821f..0f74674b4591b 100644 --- a/sdk/spring/azure-spring-boot/pom.xml +++ b/sdk/spring/azure-spring-boot/pom.xml @@ -273,8 +273,8 @@ org.springframework spring-webflux - true - 5.2.10.RELEASE + 5.2.10.RELEASE + true org.apache.httpcomponents diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java index 9580e09effa76..5e649b9265b6f 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java @@ -108,14 +108,7 @@ public T loadAuthorizedClient(String registra .map(Throwable::getCause) .filter(e -> e instanceof MsalInteractionRequiredException) .map(e -> (MsalInteractionRequiredException) e) - .ifPresent( - msalInteractionRequiredException -> { - ServletRequestAttributes attr = - (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); - HttpServletResponse response = attr.getResponse(); - Assert.notNull(response, "HttpServletResponse should not be null."); - replyForbiddenWithWwwAuthenticateHeader(response, msalInteractionRequiredException); - }); + .ifPresent(this::replyForbiddenWithWwwAuthenticateHeader); LOGGER.error("Failed to load authorized client.", exception); } catch (InterruptedException | ParseException exception) { LOGGER.error("Failed to load authorized client.", exception); @@ -162,10 +155,13 @@ private String interceptAuthorizationUri(String authorizationUri) { return null; } - void replyForbiddenWithWwwAuthenticateHeader(HttpServletResponse response, - MsalInteractionRequiredException exception) { - Map parameters = new LinkedHashMap<>(); + void replyForbiddenWithWwwAuthenticateHeader(MsalInteractionRequiredException exception) { + ServletRequestAttributes attr = + (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + HttpServletResponse response = attr.getResponse(); + Assert.notNull(response, "HttpServletResponse should not be null."); response.setStatus(HttpStatus.FORBIDDEN.value()); + Map parameters = new LinkedHashMap<>(); parameters.put(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, exception.claims()); parameters.put(OAuth2ParameterNames.ERROR, OAuth2ErrorCodes.INVALID_TOKEN); parameters.put(OAuth2ParameterNames.ERROR_DESCRIPTION, "The resource server requires higher privileges than " diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADHandleConditionalAccessFilter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADHandleConditionalAccessFilter.java index bb0a13439673c..8a5a8fff81d7b 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADHandleConditionalAccessFilter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADHandleConditionalAccessFilter.java @@ -48,7 +48,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse .map(WebClientResponseException::getHeaders) .map(httpHeaders -> httpHeaders.get(HttpHeaders.WWW_AUTHENTICATE)) .map(list -> list.get(0)) - .map(AADHandleConditionalAccessFilter::parseAuthParameters) + .map(this::parseAuthParameters) .orElse(null); if (authParameters != null && authParameters.containsKey(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)) { request.getSession().setAttribute(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, @@ -65,7 +65,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } } - private static Map parseAuthParameters(String wwwAuthenticateHeader) { + private Map parseAuthParameters(String wwwAuthenticateHeader) { return Stream.of(wwwAuthenticateHeader) .filter(header -> !StringUtils.isEmpty(header)) .filter(header -> header.startsWith(Constants.BEARER_PREFIX)) diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java index 915ea1b2e9892..b29a4f63ef281 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADWebSecurityConfigurerAdapter.java @@ -15,8 +15,8 @@ import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.web.access.ExceptionTranslationFilter; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; -import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; import org.springframework.util.StringUtils; import java.net.URI; @@ -51,7 +51,7 @@ protected void configure(HttpSecurity http) throws Exception { .logout() .logoutSuccessHandler(oidcLogoutSuccessHandler()) .and() - .addFilterBefore(handleConditionalAccessFilter(), WebAsyncManagerIntegrationFilter.class); + .addFilterBefore(new AADHandleConditionalAccessFilter(), ExceptionTranslationFilter.class); // @formatter:off } @@ -76,8 +76,4 @@ protected OAuth2AccessTokenResponseClient a protected OAuth2AuthorizationRequestResolver requestResolver() { return new AADOAuth2AuthorizationRequestResolver(this.repo); } - - protected AADHandleConditionalAccessFilter handleConditionalAccessFilter() { - return new AADHandleConditionalAccessFilter(); - } }