diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index 89f9243f9a008..1502121e62df0 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -132,6 +132,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-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-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 e0beedb9ab072..bb6453e448e4a 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..846c28b5c15c6 --- /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,27 @@ +// 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.registration.ClientRegistrationRepository; +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; + + +@Configuration +public class WebClientConfig { + + @Bean + public static WebClient webClient(ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository) { + ServletOAuth2AuthorizedClientExchangeFilterFunction function = + new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository, + authorizedClientRepository); + return WebClient.builder() + .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 new file mode 100644 index 0000000000000..1a5f1564fd193 --- /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 obo server: {}", body); + return "Obo server response " + (null != body ? "success." : "failed."); + } else { + return "Obo server 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 7fe764b65dcde..edacd62d17991 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 @@ -34,6 +34,7 @@

Azure Active Directory OAuth 2.0 Login with Spring Security

Group2 Message | Graph Client | Arm Client | + Obo Client | diff --git a/sdk/spring/azure-spring-boot/pom.xml b/sdk/spring/azure-spring-boot/pom.xml index bb4d8f5460e7b..8a8aaa9ab0f97 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] 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..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 @@ -3,28 +3,42 @@ 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.HttpHeaders; +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; 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; +import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.net.MalformedURLException; +import java.text.ParseException; import java.time.Instant; import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutionException; /** *

@@ -86,8 +100,18 @@ public T loadAuthorizedClient(String registra oAuth2AccessToken); request.setAttribute(oboAuthorizedClientAttributeName, (T) oAuth2AuthorizedClient); return (T) oAuth2AuthorizedClient; - } catch (Throwable throwable) { - LOGGER.error("Failed to load authorized client.", throwable); + } catch (ExecutionException exception) { + // Handle conditional access policy for obo flow. + // 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) + .ifPresent(this::replyForbiddenWithWwwAuthenticateHeader); + LOGGER.error("Failed to load authorized client.", exception); + } catch (InterruptedException | ParseException exception) { + LOGGER.error("Failed to load authorized client.", exception); } return null; } @@ -130,4 +154,18 @@ private String interceptAuthorizationUri(String authorizationUri) { } return null; } + + 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 " + + "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/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/AADHandleConditionalAccessFilter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADHandleConditionalAccessFilter.java new file mode 100644 index 0000000000000..8a5a8fff81d7b --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADHandleConditionalAccessFilter.java @@ -0,0 +1,81 @@ +// 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.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; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Handle the {@link WebClientResponseException} in On-Behalf-Of flow. + * + *

+ * When the resource-server needs re-acquire token(The request requires higher privileges than provided by the access + * token in On-Behalf-Of flow.), it can sent a 403 with information in the WWW-Authenticate header to web client ,web + * client will throw {@link WebClientResponseException}, web-application can handle this exception to challenge the + * user. + */ +public class AADHandleConditionalAccessFilter extends OncePerRequestFilter { + + private static final Logger LOGGER = LoggerFactory.getLogger(AADHandleConditionalAccessFilter.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws IOException, ServletException { + try { + filterChain.doFilter(request, response); + } catch (Exception exception) { + Map authParameters = + Optional.of(exception) + .map(Throwable::getCause) + .filter(e -> e instanceof WebClientResponseException) + .map(e -> (WebClientResponseException) e) + .map(WebClientResponseException::getHeaders) + .map(httpHeaders -> httpHeaders.get(HttpHeaders.WWW_AUTHENTICATE)) + .map(list -> list.get(0)) + .map(this::parseAuthParameters) + .orElse(null); + if (authParameters != null && authParameters.containsKey(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)) { + request.getSession().setAttribute(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, + authParameters.get(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS)); + 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 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) + .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/AADOAuth2Error.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADOAuth2Error.java deleted file mode 100644 index bdad533007d81..0000000000000 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADOAuth2Error.java +++ /dev/null @@ -1,76 +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; - } - - - @Override - public String toString() { - - return "AADAuthenticationException{" - + ", error_codes='" + errorCodes + '\'' - + ", timestamp='" + timestamp + '\'' - + ", trace_id='" + traceId + '\'' - + ", correlation_id='" + correlationId + '\'' - + ", suberror='" + subError + '\'' - + ", claims='" + claims + '\'' - + '}'; - } -} 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 5d81dd95429bf..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 @@ -121,6 +121,7 @@ private Set accessTokenScopes() { return result; } + private Set openidScopes() { Set result = new HashSet<>(); result.add("openid"); @@ -198,5 +199,4 @@ protected void configure(HttpSecurity http) throws Exception { .anyRequest().authenticated(); } } - } 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..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 @@ -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.access.ExceptionTranslationFilter; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; 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(new AADHandleConditionalAccessFilter(), ExceptionTranslationFilter.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; @@ -80,8 +76,4 @@ protected OAuth2AccessTokenResponseClient a protected OAuth2AuthorizationRequestResolver requestResolver() { return new AADOAuth2AuthorizationRequestResolver(this.repo); } - - protected AuthenticationFailureHandler failureHandler() { - return new AADAuthenticationFailureHandler(); - } } 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); - } - } -} 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..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,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_URI = "/oauth2/authorization/azure"; static { Set authoritySet = new HashSet<>(); 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); - } - } -}