Skip to content

Commit

Permalink
Support conditional access policy in obo flow. (Azure#18354)
Browse files Browse the repository at this point in the history
  • Loading branch information
han-gao authored Feb 26, 2021
1 parent c69db76 commit 6e7e9fb
Show file tree
Hide file tree
Showing 17 changed files with 237 additions and 329 deletions.
1 change: 1 addition & 0 deletions eng/versioning/external_dependencies.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: <client-id>
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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.";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ <h1>Azure Active Directory OAuth 2.0 Login with Spring Security</h1>
<a href="/group2" >Group2 Message</a> |
<a href="/graph" >Graph Client</a> |
<a href="/arm" >Arm Client</a> |
<a href="/obo" >Obo Client</a> |
</div>
</body>
</html>
7 changes: 7 additions & 0 deletions sdk/spring/azure-spring-boot/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,12 @@
<artifactId>spring-core</artifactId>
<version>5.2.10.RELEASE</version> <!-- {x-version-update;org.springframework:spring-core;external_dependency} -->
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<version>5.2.10.RELEASE</version> <!-- {x-version-update;org.springframework:spring-webflux;external_dependency} -->
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
Expand Down Expand Up @@ -302,6 +308,7 @@
<include>org.springframework:spring-core:[5.2.10.RELEASE]</include> <!-- {x-include-update;org.springframework:spring-core;external_dependency} -->
<include>org.springframework:spring-web:[5.2.10.RELEASE]</include> <!-- {x-include-update;org.springframework:spring-web;external_dependency} -->
<include>org.springframework:spring-jms:[5.2.10.RELEASE]</include> <!-- {x-include-update;org.springframework:spring-jms;external_dependency} -->
<include>org.springframework:spring-webflux:[5.2.10.RELEASE]</include> <!-- {x-include-update;org.springframework:spring-webflux;external_dependency} -->
<include>org.springframework.boot:spring-boot-actuator-autoconfigure:[2.3.5.RELEASE]</include> <!-- {x-include-update;org.springframework.boot:spring-boot-actuator-autoconfigure;external_dependency} -->
<include>org.springframework.boot:spring-boot-autoconfigure-processor:[2.3.5.RELEASE]</include> <!-- {x-include-update;org.springframework.boot:spring-boot-autoconfigure-processor;external_dependency} -->
<include>org.springframework.boot:spring-boot-autoconfigure:[2.3.5.RELEASE]</include> <!-- {x-include-update;org.springframework.boot:spring-boot-autoconfigure;external_dependency} -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
* <p>
Expand Down Expand Up @@ -86,8 +100,18 @@ public <T extends OAuth2AuthorizedClient> 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;
}
Expand Down Expand Up @@ -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<String, Object> 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());
}
}

This file was deleted.

Loading

0 comments on commit 6e7e9fb

Please sign in to comment.