Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support conditional access policy in obo flow. #18354

Merged
merged 35 commits into from
Feb 26, 2021
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b18f250
fix failureHandle not error info
Dec 25, 2020
0254e88
fix failureHandle not error info
Dec 31, 2020
b2b72f7
Merge branch 'master' of git://github.com/Azure/azure-sdk-for-java in…
Jan 11, 2021
72654ff
Merge branch 'master' of git://github.com/Azure/azure-sdk-for-java in…
Jan 11, 2021
230ca9d
fix failureHandle not error info
Jan 11, 2021
726a95e
code format for checkStyle
Jan 11, 2021
33ac445
code format for checkStyle
Jan 12, 2021
76b9b15
add webClient handle conditional access policy.
Jan 12, 2021
24a7bfd
add dependency to azure-spring-boot-starter-active-directory-pom
Jan 13, 2021
6f3250c
add webflux to external_dependency.
Jan 13, 2021
6942393
Modify note.
Jan 13, 2021
df4f6e2
add svg for ConditionalAccessException
Jan 19, 2021
34bd03c
add notes for ConditionalAccess.
Jan 19, 2021
0d3ec7b
update sample for ConditionalAccess
Jan 19, 2021
502384f
resolve conversation
Jan 21, 2021
8a9e6bc
resolve conversation
Jan 21, 2021
14ffa21
update webapp sample and webclient filter
Jan 25, 2021
96a4e27
add filter for conditionalAccess.
Feb 2, 2021
7f1945c
resolve conflicts.
Feb 2, 2021
fc4da85
Merge branch 'master' of git://github.com/Azure/azure-sdk-for-java in…
Feb 2, 2021
162d5ac
update ConditionalAccessException.java
Feb 2, 2021
f103bad
fix code style.
Feb 3, 2021
47daa01
when re-authentication, update all clients.
Feb 4, 2021
2396408
remove ConditionalAccessException.java
Feb 9, 2021
809661d
Merge branch 'master' of git://github.com/Azure/azure-sdk-for-java in…
Feb 10, 2021
bd0c5cd
resolve conflicts.
Feb 10, 2021
9da07e2
Solve pipeline problems.
Feb 10, 2021
6ff1f95
Solve pipeline problems.
Feb 18, 2021
707c22d
resolve conversation.
Feb 20, 2021
6aade06
Merge branch 'master' of git://github.com/Azure/azure-sdk-for-java in…
Feb 20, 2021
3b7bf5a
Merge branch 'master' of git://github.com/Azure/azure-sdk-for-java in…
Feb 22, 2021
d2259e8
add web-flux dependency to aad stater
Feb 22, 2021
ab6be45
add web-flux dependency to aad stater
Feb 22, 2021
bfb52b5
resolve conversation.
Feb 22, 2021
713d0db
Merge branch 'master' of git://github.com/Azure/azure-sdk-for-java in…
Feb 25, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
chenrujun marked this conversation as resolved.
Show resolved Hide resolved
<version>5.2.10.RELEASE</version>
<optional>true</optional> <!-- {x-version-update;org.springframework:spring-webflux;external_dependency} -->
chenrujun marked this conversation as resolved.
Show resolved Hide resolved
</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,25 @@ 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(
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);
}
return null;
}
Expand Down Expand Up @@ -130,4 +161,15 @@ private String interceptAuthorizationUri(String authorizationUri) {
}
return null;
}

void replyForbiddenWithWwwAuthenticateHeader(HttpServletResponse response,
MsalInteractionRequiredException exception) {
han-gao marked this conversation as resolved.
Show resolved Hide resolved
Map<String, Object> 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");
chenrujun marked this conversation as resolved.
Show resolved Hide resolved
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, Constants.BEARER_PREFIX + parameters.toString());
han-gao marked this conversation as resolved.
Show resolved Hide resolved
}
}

This file was deleted.

Loading