Skip to content

Commit 6e7e9fb

Browse files
authored
Support conditional access policy in obo flow. (Azure#18354)
1 parent c69db76 commit 6e7e9fb

File tree

17 files changed

+237
-329
lines changed

17 files changed

+237
-329
lines changed

eng/versioning/external_dependencies.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ org.springframework:spring-messaging;5.2.10.RELEASE
132132
org.springframework:spring-tx;5.2.10.RELEASE
133133
org.springframework:spring-web;5.2.10.RELEASE
134134
org.springframework:spring-webmvc;5.2.10.RELEASE
135+
org.springframework:spring-webflux;5.2.10.RELEASE
135136

136137
# spring-boot-starter-parent is not managed by spring-boot-dependencies or spring-cloud-dependencies.
137138
org.springframework.boot:spring-boot-starter-parent;2.3.7.RELEASE

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

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,13 @@
1515
public class AADSampleConfiguration {
1616

1717
@Bean
18-
public OAuth2AuthorizedClientManager authorizedClientManager(
19-
ClientRegistrationRepository clientRegistrationRepository,
20-
OAuth2AuthorizedClientRepository authorizedClientRepository) {
21-
OAuth2AuthorizedClientProvider authorizedClientProvider =
22-
OAuth2AuthorizedClientProviderBuilder.builder()
23-
.refreshToken()
24-
.build();
25-
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
26-
new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
27-
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
28-
return authorizedClientManager;
29-
}
30-
31-
@Bean
32-
public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
33-
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
34-
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
18+
public static WebClient webClient(ClientRegistrationRepository clientRegistrationRepository,
19+
OAuth2AuthorizedClientRepository authorizedClientRepository) {
20+
ServletOAuth2AuthorizedClientExchangeFilterFunction function =
21+
new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository,
22+
authorizedClientRepository);
3523
return WebClient.builder()
36-
.apply(oauth2Client.oauth2Configuration())
24+
.apply(function.oauth2Configuration())
3725
.build();
3826
}
3927
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public class SampleController {
2828

2929
private static final String GRAPH_ME_ENDPOINT = "https://graph.microsoft.com/v1.0/me";
3030

31-
private static final String CUSTOM_LOCAL_FILE_ENDPOINT = "http://localhost:8080/file";
31+
private static final String CUSTOM_LOCAL_FILE_ENDPOINT = "http://localhost:8082/file";
3232

3333
@Autowired
3434
private WebClient webClient;

sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-resource-server/src/main/resources/application.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
# 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.
33
# If you are using v1.0 tokens, configure both to properly complete the audience validation.
44

5+
server:
6+
port: 8082
7+
58
#azure:
69
# activedirectory:
710
# client-id: <client-id>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.spring.sample.aad.config;
5+
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
9+
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
10+
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
11+
import org.springframework.web.reactive.function.client.WebClient;
12+
13+
14+
@Configuration
15+
public class WebClientConfig {
16+
17+
@Bean
18+
public static WebClient webClient(ClientRegistrationRepository clientRegistrationRepository,
19+
OAuth2AuthorizedClientRepository authorizedClientRepository) {
20+
ServletOAuth2AuthorizedClientExchangeFilterFunction function =
21+
new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository,
22+
authorizedClientRepository);
23+
return WebClient.builder()
24+
.apply(function.oauth2Configuration())
25+
.build();
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.spring.sample.aad.controller;
5+
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
10+
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
11+
import org.springframework.stereotype.Controller;
12+
import org.springframework.web.bind.annotation.GetMapping;
13+
import org.springframework.web.bind.annotation.ResponseBody;
14+
import org.springframework.web.reactive.function.client.WebClient;
15+
16+
17+
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient;
18+
19+
@Controller
20+
public class CallOboServerController {
21+
22+
private static final Logger LOGGER = LoggerFactory.getLogger(CallOboServerController.class);
23+
24+
private static final String CUSTOM_LOCAL_FILE_ENDPOINT = "http://localhost:8081/call-custom";
25+
26+
@Autowired
27+
private WebClient webClient;
28+
29+
/**
30+
* Call obo server, combine all the response and return.
31+
* @param obo authorized client for Custom
32+
* @return Response Graph and Custom data.
33+
*/
34+
@GetMapping("/obo")
35+
@ResponseBody
36+
public String callOboServer(@RegisteredOAuth2AuthorizedClient("obo") OAuth2AuthorizedClient obo) {
37+
return callOboEndpoint(obo);
38+
}
39+
40+
/**
41+
* Call obo local file endpoint
42+
* @param obo Authorized Client
43+
* @return Response string data.
44+
*/
45+
private String callOboEndpoint(OAuth2AuthorizedClient obo) {
46+
if (null != obo) {
47+
String body = webClient
48+
.get()
49+
.uri(CUSTOM_LOCAL_FILE_ENDPOINT)
50+
.attributes(oauth2AuthorizedClient(obo))
51+
.retrieve()
52+
.bodyToMono(String.class)
53+
.block();
54+
LOGGER.info("Response from obo server: {}", body);
55+
return "Obo server response " + (null != body ? "success." : "failed.");
56+
} else {
57+
return "Obo server response failed.";
58+
}
59+
}
60+
}

sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-webapp/src/main/resources/templates/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ <h1>Azure Active Directory OAuth 2.0 Login with Spring Security</h1>
3434
<a href="/group2" >Group2 Message</a> |
3535
<a href="/graph" >Graph Client</a> |
3636
<a href="/arm" >Arm Client</a> |
37+
<a href="/obo" >Obo Client</a> |
3738
</div>
3839
</body>
3940
</html>

sdk/spring/azure-spring-boot/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,12 @@
270270
<artifactId>spring-core</artifactId>
271271
<version>5.2.10.RELEASE</version> <!-- {x-version-update;org.springframework:spring-core;external_dependency} -->
272272
</dependency>
273+
<dependency>
274+
<groupId>org.springframework</groupId>
275+
<artifactId>spring-webflux</artifactId>
276+
<version>5.2.10.RELEASE</version> <!-- {x-version-update;org.springframework:spring-webflux;external_dependency} -->
277+
<optional>true</optional>
278+
</dependency>
273279
<dependency>
274280
<groupId>org.apache.httpcomponents</groupId>
275281
<artifactId>httpclient</artifactId>
@@ -302,6 +308,7 @@
302308
<include>org.springframework:spring-core:[5.2.10.RELEASE]</include> <!-- {x-include-update;org.springframework:spring-core;external_dependency} -->
303309
<include>org.springframework:spring-web:[5.2.10.RELEASE]</include> <!-- {x-include-update;org.springframework:spring-web;external_dependency} -->
304310
<include>org.springframework:spring-jms:[5.2.10.RELEASE]</include> <!-- {x-include-update;org.springframework:spring-jms;external_dependency} -->
311+
<include>org.springframework:spring-webflux:[5.2.10.RELEASE]</include> <!-- {x-include-update;org.springframework:spring-webflux;external_dependency} -->
305312
<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} -->
306313
<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} -->
307314
<include>org.springframework.boot:spring-boot-autoconfigure:[2.3.5.RELEASE]</include> <!-- {x-include-update;org.springframework.boot:spring-boot-autoconfigure;external_dependency} -->

sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapi/AADOAuth2OboAuthorizedClientRepository.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,42 @@
33

44
package com.azure.spring.aad.webapi;
55

6+
import com.azure.spring.autoconfigure.aad.Constants;
67
import com.microsoft.aad.msal4j.ClientCredentialFactory;
78
import com.microsoft.aad.msal4j.ConfidentialClientApplication;
89
import com.microsoft.aad.msal4j.IClientSecret;
10+
import com.microsoft.aad.msal4j.MsalInteractionRequiredException;
911
import com.microsoft.aad.msal4j.OnBehalfOfParameters;
1012
import com.microsoft.aad.msal4j.UserAssertion;
1113
import com.nimbusds.jwt.JWT;
1214
import com.nimbusds.jwt.JWTParser;
1315
import org.slf4j.Logger;
1416
import org.slf4j.LoggerFactory;
17+
import org.springframework.http.HttpHeaders;
18+
import org.springframework.http.HttpStatus;
1519
import org.springframework.security.core.Authentication;
1620
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
1721
import org.springframework.security.oauth2.client.registration.ClientRegistration;
1822
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
1923
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
2024
import org.springframework.security.oauth2.core.OAuth2AccessToken;
25+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
26+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
2127
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
28+
import org.springframework.util.Assert;
29+
import org.springframework.web.context.request.RequestContextHolder;
30+
import org.springframework.web.context.request.ServletRequestAttributes;
2231

2332
import javax.servlet.http.HttpServletRequest;
2433
import javax.servlet.http.HttpServletResponse;
2534
import java.net.MalformedURLException;
35+
import java.text.ParseException;
2636
import java.time.Instant;
2737
import java.util.Date;
38+
import java.util.LinkedHashMap;
39+
import java.util.Map;
40+
import java.util.Optional;
41+
import java.util.concurrent.ExecutionException;
2842

2943
/**
3044
* <p>
@@ -86,8 +100,18 @@ public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String registra
86100
oAuth2AccessToken);
87101
request.setAttribute(oboAuthorizedClientAttributeName, (T) oAuth2AuthorizedClient);
88102
return (T) oAuth2AuthorizedClient;
89-
} catch (Throwable throwable) {
90-
LOGGER.error("Failed to load authorized client.", throwable);
103+
} catch (ExecutionException exception) {
104+
// Handle conditional access policy for obo flow.
105+
// A user interaction is required, but we are in a web API, and therefore, we need to report back to the
106+
// client through a 'WWW-Authenticate' header https://tools.ietf.org/html/rfc6750#section-3.1
107+
Optional.of(exception)
108+
.map(Throwable::getCause)
109+
.filter(e -> e instanceof MsalInteractionRequiredException)
110+
.map(e -> (MsalInteractionRequiredException) e)
111+
.ifPresent(this::replyForbiddenWithWwwAuthenticateHeader);
112+
LOGGER.error("Failed to load authorized client.", exception);
113+
} catch (InterruptedException | ParseException exception) {
114+
LOGGER.error("Failed to load authorized client.", exception);
91115
}
92116
return null;
93117
}
@@ -130,4 +154,18 @@ private String interceptAuthorizationUri(String authorizationUri) {
130154
}
131155
return null;
132156
}
157+
158+
void replyForbiddenWithWwwAuthenticateHeader(MsalInteractionRequiredException exception) {
159+
ServletRequestAttributes attr =
160+
(ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
161+
HttpServletResponse response = attr.getResponse();
162+
Assert.notNull(response, "HttpServletResponse should not be null.");
163+
response.setStatus(HttpStatus.FORBIDDEN.value());
164+
Map<String, Object> parameters = new LinkedHashMap<>();
165+
parameters.put(Constants.CONDITIONAL_ACCESS_POLICY_CLAIMS, exception.claims());
166+
parameters.put(OAuth2ParameterNames.ERROR, OAuth2ErrorCodes.INVALID_TOKEN);
167+
parameters.put(OAuth2ParameterNames.ERROR_DESCRIPTION, "The resource server requires higher privileges than "
168+
+ "provided by the access token");
169+
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, Constants.BEARER_PREFIX + parameters.toString());
170+
}
133171
}

sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/webapp/AADAuthenticationFailureHandler.java

Lines changed: 0 additions & 59 deletions
This file was deleted.

0 commit comments

Comments
 (0)