Spring Security "on behalf of" reactive implementation - client part - spring-security

I'm working on implementing "on behalf of" in SpringBoot. I have a working implementation. Now I'm trying to move it to a fully reactive stack.
What do I mean by saing - moving to fully reactive stack?
I want to replace:
#EnableWebSecurity, ClientRegistrationRepository, OAuth2AuthorizedClientManager, ...
Using their reactive implementation, to be consistent with resource server part of my application.
The problem I ran into is:
public Mono<Data> getDataD(#RegisteredOAuth2AuthorizedClient("client_id") OAuth2AuthorizedClient authorizedClient)
authorizedClient is null, when my base implementation works ok.
#GetMapping("/data-a/d")
public Mono<Data> getDataD(#RegisteredOAuth2AuthorizedClient("client_id") OAuth2AuthorizedClient authorizedClient) {
Mono<Data> data = Mono.just(new Data("OBO POC D", "POC D"));
Mono<DataB> dataB = webClient
.get()
.uri("http://localhost:8081/data-b/b")
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(DataB.class);
Mono<Data> zippedData = data.zipWith(dataB, (a, b) -> a.setData(b));
return zippedData;
}
I tried to follow a different path:
https://docs.spring.io/spring-security/reference/6.0/reactive/oauth2/client/authorization-grants.html#oauth2Client-jwt-bearer-grant
It looks promising, because this time an authorization request is being sent, but the AD claims that the request does not contain the "requested_token_use" parameter, which is required. I don't know how to extend the request with additional parameters – this feature is only mentioned in documentation.
#GetMapping("/data-a/c")
public Mono<Data> getDataC(JwtAuthenticationToken jwtAuthentication, ServerWebExchange exchange) {
Mono<Data> data = Mono.just(new Data("OBO POC A", "POC C"));
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("obo_rs_a-obo_rs_b")
.principal(jwtAuthentication)
//.attribute("requested_token_use", "on_behalf_of")
.build();
return this.authorizedClientManager.authorize(authorizeRequest)
.map(c -> {
OAuth2AccessToken token = c.getAccessToken();
System.out.println("SCOPES: " + token.getScopes());
return token;})
.zipWith(data, (a,b) -> b);
}
Thanks for any help.

After several experiments, I've found working solution eventually.
#Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
WebClientReactiveJwtBearerTokenResponseClient jwtBearerTokenResponseClient = new WebClientReactiveJwtBearerTokenResponseClient();
jwtBearerTokenResponseClient.setParametersConverter(r -> {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.add(OAuth2ParameterNames.GRANT_TYPE, r.getGrantType().getValue());
parameters.add("requested_token_use", "on_behalf_of");
return parameters;
});
...
#GetMapping("/data-a/c")
public Mono<Data> getDataC(JwtAuthenticationToken jwtAuthentication) {
Mono<Data> data = Mono.just(new Data("OBO POC A", "POC A calls B - the hard way :-)"));
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("obo_rs_a-obo_rs_b")
.principal(jwtAuthentication)
.build();
Mono<DataB> dataB = this.authorizedClientManager.authorize(authorizeRequest)
.flatMap(c -> webClient.get()
.uri("http://localhost:8081/data-b/b")
.header("Authorization", "Bearer " + c.getAccessToken().getTokenValue())
.retrieve()
.bodyToMono(DataB.class));
Mono<Data> zippedData = data.zipWith(dataB, (a, b) -> a.setData(b));
return zippedData;
}
I'm still not sure if this implementation is correct.

Related

How OidcIdTokenDecoderFactory can support both SignatureAlgorithm.RS256 and SignatureAlgorithm.RS512

For resource server, it's as simple as
#Bean
fun jwtDecoder(): JwtDecoder {
// makes a request to the JWK Set endpoint
val jwsKeySelector: JWSKeySelector<SecurityContext> = JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL<SecurityContext>(this.jwkSetUrl)
val jwtProcessor: DefaultJWTProcessor<SecurityContext> = DefaultJWTProcessor()
jwtProcessor.jwsKeySelector = jwsKeySelector
return NimbusJwtDecoder(jwtProcessor)
}
and it fetches the algorithms from the public key endpoint.
But it doesn't work for OidcIdTokenDecoderFactory because https://github.com/spring-projects/spring-security/blob/6.0.0-M6/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactory.java create a new JwtDecoder
Any idea? or I can only customize a OidcIdTokenDecoderFactory ?
The OidcIdTokenDecoderFactory is indeed the correct configuration hook for providing the algorithm used for a client in a custom way.
For example, you can specify the following:
#Bean
public JwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
OidcIdTokenDecoderFactory idTokenDecoderFactory = new OidcIdTokenDecoderFactory();
idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> {
#SuppressWarnings("unchecked")
List<String> supportedAlgorithms =
(List<String>) clientRegistration.getProviderDetails()
.getConfigurationMetadata()
.get("id_token_signing_alg_values_supported");
return SignatureAlgorithm.from(supportedAlgorithms.get(0));
});
return idTokenDecoderFactory;
}
Note that this is a trivial example that assumes the first available algorithm. You can provide any logic you need, but it has to return a single algorithm.
There is some good background on this issue in #11049.

Assert ReactiveSecurityContextHolder.getContext() in the StepVerifier of my WebFilter Webflux

I am migrating our security library which is Servlet based.
I need to use ReactiveSecurityContextHolder.getContext() instead of SecurityContextHolder.getContext().
I have a WebFLux WebFilter that modifies the Authentication.
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
...
return chain.filter(exchange).subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication));
}
I would like to assert the Authentication in ReactiveSecurityContextHolder.getContext().
StepVerifier.create(filter.filter(MockServerWebExchange.from(request), filterChain)).expectNext(...).verifyComplete();
No matter what I try, the context is null because I have to subscribe to the Mono returned by ReactiveSecurityContextHolder.getContext().
I tried ReactiveSecurityContextHolder.getContext().block() without success.
May be the way I try to test it is wrong.
A way to do it is to use the WebFilterChain which is basically a lambda.
So we can have a WebFilterChain : (exchange) -> Mono.empty()
But we need a lambda that will use ReactiveSecurityContextHolder.getContext() instead of Mono.empty();
The result looks like :
var request = MockServerHttpRequest
.get("/api/test")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + jwt);
var exchange = MockServerWebExchange.from(request);
StepVerifier.create(filter.filter(
exchange,
it -> ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.map(JWTAuthentication.class::cast)
.doOnSuccess(auth -> assertThat(auth.getUserId()).isEqualTo("username"))
.doOnSuccess(auth -> assertThat(auth.getJwt()).isEqualTo(jwt))
.doOnSuccess(auth -> assertThat(auth.getSubscriptions()).isEmpty())
.then()
)).verifyComplete();

How to customize the Authorization header of the OAuth2 token request using spring-security-oauth2 with a WebClient?

I am trying to upgrade to spring security 5.5.1 on a WebClient call.
I found out that the oauth2 clientId and secret are now URL encoded in AbstractWebClientReactiveOAuth2AccessTokenResponseClient, but my token provider does not support this (for example if the secret contains a + character it works only when it is sent as a + not as %2B).
I understand this is seen as a bug fix from spring-security side ), but I cannot make the token provider change its behavior easily.
So I tried to find a way to work around this.
The [documentation] (https://docs.spring.io/spring-security/site/docs/current/reference/html5/#customizing-the-access-token-request) on how to customize the access token request does not seem to apply when you use a WebClient configuration (which is my case).
In order to remove the clientid/secret encoding I had to extend and copy most of the existing code from AbstractWebClientReactiveOAuth2AccessTokenResponseClient to customize the WebClientReactiveClientCredentialsTokenResponseClient because most of it has private/default visibility.
I traced this in an enhancement issue in the spring-security project.
Is there an easier way to customize the Authorization header of the token request, in order to skip the url encoding ?
There is definitely room for improvement in some of the APIs around customization, and for sure these types of questions/requests/issues from the community will continue to help highlight those areas.
Regarding the AbstractWebClientReactiveOAuth2AccessTokenResponseClient in particular, there is currently no way to override the internal method to populate basic auth credentials in the Authorization header. However, you can customize the WebClient that is used to make the API call. If it's acceptable in your use case (temporarily, while the behavior change is being addressed and/or a customization option is added) you should be able to intercept the request in the WebClient.
Here's a configuration that will create a WebClient capable of using an OAuth2AuthorizedClient:
#Configuration
public class WebClientConfiguration {
#Bean
public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
// #formatter:off
ServerOAuth2AuthorizedClientExchangeFilterFunction exchangeFilterFunction =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
exchangeFilterFunction.setDefaultOAuth2AuthorizedClient(true);
return WebClient.builder()
.filter(exchangeFilterFunction)
.build();
// #formatter:on
}
#Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
// #formatter:off
WebClientReactiveClientCredentialsTokenResponseClient accessTokenResponseClient =
new WebClientReactiveClientCredentialsTokenResponseClient();
accessTokenResponseClient.setWebClient(createAccessTokenResponseWebClient());
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials(consumer ->
consumer.accessTokenResponseClient(accessTokenResponseClient)
.build())
.build();
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
// #formatter:on
return authorizedClientManager;
}
protected WebClient createAccessTokenResponseWebClient() {
// #formatter:off
return WebClient.builder()
.filter((clientRequest, exchangeFunction) -> {
HttpHeaders headers = clientRequest.headers();
String authorizationHeader = headers.getFirst("Authorization");
Assert.notNull(authorizationHeader, "Authorization header cannot be null");
Assert.isTrue(authorizationHeader.startsWith("Basic "),
"Authorization header should start with Basic");
String encodedCredentials = authorizationHeader.substring("Basic ".length());
byte[] decodedBytes = Base64.getDecoder().decode(encodedCredentials);
String credentialsString = new String(decodedBytes, StandardCharsets.UTF_8);
Assert.isTrue(credentialsString.contains(":"), "Decoded credentials should contain a \":\"");
String[] credentials = credentialsString.split(":");
String clientId = URLDecoder.decode(credentials[0], StandardCharsets.UTF_8);
String clientSecret = URLDecoder.decode(credentials[1], StandardCharsets.UTF_8);
ClientRequest newClientRequest = ClientRequest.from(clientRequest)
.headers(httpHeaders -> httpHeaders.setBasicAuth(clientId, clientSecret))
.build();
return exchangeFunction.exchange(newClientRequest);
})
.build();
// #formatter:on
}
}
This test demonstrates that the credentials are decoded for the internal access token response WebClient:
#ExtendWith(MockitoExtension.class)
public class WebClientConfigurationTests {
private WebClientConfiguration webClientConfiguration;
#Mock
private ExchangeFunction exchangeFunction;
#Captor
private ArgumentCaptor<ClientRequest> clientRequestCaptor;
#BeforeEach
public void setUp() {
webClientConfiguration = new WebClientConfiguration();
}
#Test
public void exchangeWhenBasicAuthThenDecoded() {
WebClient webClient = webClientConfiguration.createAccessTokenResponseWebClient()
.mutate()
.exchangeFunction(exchangeFunction)
.build();
when(exchangeFunction.exchange(any(ClientRequest.class)))
.thenReturn(Mono.just(ClientResponse.create(HttpStatus.OK).build()));
webClient.post()
.uri("/oauth/token")
.headers(httpHeaders -> httpHeaders.setBasicAuth("aladdin", URLEncoder.encode("open sesame", StandardCharsets.UTF_8)))
.retrieve()
.bodyToMono(Void.class)
.block();
verify(exchangeFunction).exchange(clientRequestCaptor.capture());
ClientRequest clientRequest = clientRequestCaptor.getValue();
String authorizationHeader = clientRequest.headers().getFirst("Authorization");
assertThat(authorizationHeader).isNotNull();
String encodedCredentials = authorizationHeader.substring("Basic ".length());
byte[] decodedBytes = Base64.getDecoder().decode(encodedCredentials);
String credentialsString = new String(decodedBytes, StandardCharsets.UTF_8);
String[] credentials = credentialsString.split(":");
assertThat(credentials[0]).isEqualTo("aladdin");
assertThat(credentials[1]).isEqualTo("open sesame");
}
}

Cannot parse id for appbundles using Design Automation SDK

Here I am again trying to use the Design Automation SDK and I get this error when I try to retrieve bundle aliases, versions or other information that require the id.
I am testing that using one of the existing appbundles available...
public static async Task<dynamic> GetAppBundleVersionsAsync(ForgeService service, Token token, string id)
{
try
{
if (token.ExpiresAt < DateTime.Now)
token = Get2LeggedToken();
AppBundlesApi appBundlesApi = new AppBundlesApi(service);
Dictionary<string, string> headers = new Dictionary<string, string>();
headers.Add("Authorization", "Bearer " + token.AccessToken);
headers.Add("content-type", "application/json");
var aliases = await appBundlesApi.GetAppBundleVersionsAsync(id, null, null, headers);
return aliases;
}
catch (Exception ex)
{
Console.WriteLine(string.Format("Error : {0}", ex.Message));
return null;
}
}
Almost thinking to go to my previous RestSharp implementation :)
There are 2 kinds of IDs:
Fully qualified (string in format owner.name+alias)
Unqualified (just name)
You are trying to list versions of your own AppBundle, so you need to use Unqualified. It seems your ID is fully qualified form.
For more info look at API documentation description of endpoint id parameter you are using https://forge.autodesk.com/en/docs/design-automation/v3/reference/http/design-automation-appbundles-id-versions-GET/#uri-parameters

StoredProcedure return Complex Type , Data Service, Entity FrameWork and WCF

I have Entity Framework TaskTracker.edmx
There is StoredProcedure GetEmployees and used Complex type to return the Data
at TaskTrackerDataService.cs
public static void InitializeService(DataServiceConfiguration config)
{
// Grant only the rights needed to support the client application.
config.SetEntitySetAccessRule("*", EntitySetRights.All);
config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
}
[WebGet, WebInvoke(ResponseFormat = WebMessageFormat.Xml)]
public IQueryable<TaskTracker_EDM.EmployeeView> GetEmployees()
{
TaskTracker_EDM.TaskTrackerEntities ctx = new TaskTracker_EDM.TaskTrackerEntities();
return ctx.GetEmployees(string.Empty).AsQueryable();
}
at Client Site [Console Application]
var emps = ctxDSvc.Execute<EmployeeView>(new Uri("http://localhost:2402/TaskTrackerDataService.svc/GetEmployees", UriKind.RelativeOrAbsolute));
foreach (EmployeeView e in emps)
{
Console.WriteLine(string.Format("ID: {0} - {1} ", e.EmployeeID, e.Name));
}
foreach is not working, There is no data in emps.
always emps has
+ emps {System.Data.Services.Client.QueryOperationResponse<TaskTrackerConsoleTest.TaskTrackerDataService.EmployeeView>}
System.Collections.Generic.IEnumerable<TaskTrackerConsoleTest.TaskTrackerDataService.EmployeeView> {System.Data.Services.Client.QueryOperationResponse<TaskTrackerConsoleTest.TaskTrackerDataService.EmployeeView>}
I read that OData doesn't support Complex Type.[Did they fix it] or are there another solution.
Some said, use a Xml to Linq kind of approach to get the complex types. [any help]
Any help or advice.
I Serialize the result [ObjectToString] at the Server before send it to the Client application
then Deserialize it.
If there are other ideas, please don't hesitate to tell me.
1- at TaskTracker_WCF1 -> TaskTrackerData.cs
//When StoredProcedure return Complex Type , Then you need to serialise it [Object To String] before send it to Client application
//OData doesn't support Complex Type
[WebGet]
public String GetEmployees_SPreturnComplexType(String nameSearch)
{
TaskTracker_EDM.TaskTrackerEntities ctx = new TaskTracker_EDM.TaskTrackerEntities();
List<TaskTracker_EDM.EmployeeView> hiEmployeeView = (List<TaskTracker_EDM.EmployeeView>)ctx.GetEmployees("Keko88").ToList();
//Serialize object to String
XmlSerializer serializer = new XmlSerializer(hiEmployeeView.GetType());
using (StringWriter writer = new StringWriter())
{
serializer.Serialize(writer,hiEmployeeView);
return writer.ToString();
}
}
2- at Console application at program.cs
TaskTrackerDataService.TaskTrackerEntities ctxDSvc =
new TaskTrackerDataService.TaskTrackerEntities(new Uri("http://localhost:2402/TaskTrackerDataService.svc"));
//=======================================
//2-1Call StoredProcedure return Complex Type
String emps = ctxDSvc.Execute<String>(new Uri(ctxDSvc.BaseUri.ToString() +
"/GetEmployees_SPreturnComplexType", UriKind.RelativeOrAbsolute)).Single();
//2-2We need to Deserialize it before use it
var reader = new StringReader(emps);
var serializer = new XmlSerializer(typeof(List<EmployeeView>));
List<EmployeeView> instance = (List<EmployeeView>)serializer.Deserialize(reader);
//=======================================
foreach (EmployeeView e in instance)
{
Console.WriteLine(string.Format("ID: {0} - {1} ", e.EmployeeID, e.Name));
}
Console.WriteLine();
//=======================================

Resources