How to make reactive webclient follow 3XX-redirects? - spring-boot

I have created a basic REST controller which makes requests using the reactive Webclient in Spring-boot 2 using netty.
#RestController
#RequestMapping("/test")
#Log4j2
public class TestController {
private WebClient client;
#PostConstruct
public void setup() {
client = WebClient.builder()
.baseUrl("http://www.google.com/")
.exchangeStrategies(ExchangeStrategies.withDefaults())
.build();
}
#GetMapping
public Mono<String> hello() throws URISyntaxException {
return client.get().retrieve().bodyToMono(String.class);
}
}
When I get a 3XX response code back I want the webclient to follow the redirect using the Location in the response and call that URI recursively until I get a non 3XX response.
The actual result I get is the 3XX response.

You need to configure the client per the docs
WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(
HttpClient.create().followRedirect(true)
))

You could make the URL parameter of your function, and recursively call it while you're getting 3XX responses. Something like this (in real implementation you would probably want to limit the number of redirects):
public Mono<String> hello(String uri) throws URISyntaxException {
return client.get()
.uri(uri)
.exchange()
.flatMap(response -> {
if (response.statusCode().is3xxRedirection()) {
String redirectUrl = response.headers().header("Location").get(0);
return response.bodyToMono(Void.class).then(hello(redirectUrl));
}
return response.bodyToMono(String.class);
}

Related

Calling micro service from spring cloud gateway

In spring cloud gateway, added a filter that check for the authentication and authorization for further processing of request. I am calling authentication service using feign client, but I am getting the below error while invoking my service through spring cloud gateway.
java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-epoll-3\n\tat reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:83)\n\tSuppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: \nError has been observed at the following site(s):\n\t|_ checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter ....."
I would like to know is it wrong architecture I am using. How to proceed? I am stuck at this error.
#Autowired
private AuthenticationService authService;
// route validator
#Autowired
private RouterValidator routerValidator;
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
if (routerValidator.isSecured.test(request)) {
log.info("Accessing the restricted path");
if (this.isAuthMissing(request))
return this.onError(exchange, "Authorization header is missing in request", HttpStatus.UNAUTHORIZED);
final String token = this.getAuthHeader(request);
log.info("before authservice call");
AuthenticationResponse user = authService.isTokenValid(token);
log.info("after authservice call");
if (!user.isValid())
return this.onError(exchange, "Authorization header is invalid", HttpStatus.UNAUTHORIZED);
log.info("before calling populatedRequest");
this.populateRequestWithHeaders(exchange, user);
}
return chain.filter(exchange);
}
private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
return response.setComplete();
}
private String getAuthHeader(ServerHttpRequest request) {
return request.getHeaders().getOrEmpty("Authorization").get(0);
}
private boolean isAuthMissing(ServerHttpRequest request) {
log.info("inside auth missing");
return !request.getHeaders().containsKey("Authorization");
}
private void populateRequestWithHeaders(ServerWebExchange exchange, AuthenticationResponse authRes) {
log.info("About to mutate the request->{}",exchange);
exchange.getRequest().mutate()
.header("id",Integer.toString(authRes.getUserId()))
.build();
}
Feign interface
#Autowired
private AuthenticationFeign auth;
public AuthenticationResponse isTokenValid(String token) {
return auth.getValidity(token);
}
I couldn't clearly read it. But problem is that: you can not make blocking call in filter pipeline. Current reactive impl. is like that. if you want, u can use .then() method of WebClient. U should use webclient. because it's reactive.
this link may help you:
https://github.com/spring-cloud/spring-cloud-gateway/issues/980
There was a long time, but i want to give answer. I hope, this help u, please response back, it works or not.

How to send java.security.Principal with a webclient request

There is a rest api secured using keycloak(OAUTH) as below:
#PreAuthorize("hasRole('user')")
#GetMapping(value = "/employees", produces = { MediaType.APPLICATION_JSON_VALUE })
public ResponseEntity<List<Employee>> findAll(java.security.Principal principal,
#RequestParam(name = "filterationCriteria", required = false) String fields) {
if(Objects.nonNull(principal)){
return employeeManagementService.findAll(fields);
}
return null;
}
i want to consume this api using webclient as below:
public <T> T get(URI url, Class<T> responseType, Principal principal) {
return WebClient.builder().build().get().uri(url)
.header("Authorization", "Basic " + principal)
.retrieve().bodyToMono(responseType).block();
}
above method is throwing below exception from findAll() method, which generally happens if the principal is not found:
org.springframework.web.reactive.function.client.WebClientResponseException$Unauthorized:
401 Unauthorized from GET
Am i doing something wrong, is that the correct way to send principal with the request?
Any suggestion is appreciated.
Thanks in advance.
UPDATE
I am sending request to the secured rest-api using webclient from a different service and i need to pass the principal manually(may be including it into the header).
if i simply do below:
WebClient.builder().build().get().uri(url).retrieve().bodyToMono(responseType).block()
then the principal is coming null in the findAll method
Security config
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
.anyRequest().permitAll().and()
.csrf().disable();
}
NOTE: i have put Authorize constraint at method level.
I have found a solution. the principal has to be sent like this:
public <T> T get(URI url, Class<T> responseType, Principal principal) {
RequestHeadersSpec uri = webClient.get().uri(url);
if (Objects.nonNull(principal)) {
uri = uri.header("Authorization", "Bearer "
+ ((KeycloakAuthenticationToken) principal).getAccount().getKeycloakSecurityContext().getTokenString());
}
ResponseSpec response = uri.retrieve();
Mono<T> bodyToMono = response.bodyToMono(responseType);
return bodyToMono.block();
}

Spring Framework 3.2 : How to add basic authentication for all Requests through RestTemplate

I'm working on a Spring framework 3.2 version project. Requirement is to call an external Web-service which requires basic authentication.
Currently, I'm using HttpClient and adding basic authentication into header. but this is being done for each requests. I know in Spring Boot we can achieve that by using RestTemplateBuilder. IS there a way we can maintain a single authentication for all the requests by adding authentication only once?
TIA
You can define a RestTemplate bean with authentication details like this:
#Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.basicAuthorization("user", "secret").build();
}
Updated
An alternative way to do this is by implementing an interceptor. RestTemplate extends the InterceptingHttpAccessor interface which has a setInterceptors() method. You can use it to inject an interceptor to set request headers as needed.
public class MyInterceptor implements ClientHttpRequestInterceptor {
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// ... set headers on the request ...
return execution.execute(request, body);
}
}
#Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(Collections.singletonList(new MyInterceptor()))
return restTemplate
}
Note: untested code, but should work. You can check the docs for more details.
https://docs.spring.io/autorepo/docs/spring-framework/3.2.0.RELEASE/javadoc-api/index.html?org/springframework/web/client/RestTemplate.html
The HttpClient can be built from the HttpClientBuilder.
This builder has setDefaultHeaders(Collection<? extends org.apache.http.Header> defaultHeaders) method which you can set the headers you want. Then when you want an HttpClient, call httpClientBuilder.build().

Create route in Spring Cloud Gateway with OAuth2 Resource Owner Password grant type

How to configure a route in Spring Cloud Gateway to use an OAuth2 client with authorization-grant-type: password? In other words, how to add the Authorization header with the token in the requests to an API? Because I'm integrating with a legacy application, I must use the grant type password.
I have this application:
#SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
#Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("route_path", r -> r.path("/**")
.filters(f -> f.addRequestHeader("Authorization", "bearer <token>"))
.uri("http://localhost:8092/messages"))
.build();
}
}
Replacing the <token> with an actual token, everything just works fine.
I found this project that does something similar: https://github.com/jgrandja/spring-security-oauth-5-2-migrate. It has a client (messaging-client-password) that is used to configure the WebClient to add OAuth2 support to make requests (i.e. by adding the Authorization header).
We can't use this sample project right away because Spring Cloud Gateway is reactive and the way we configure things changes significantly. I think to solve this problem is mostly about converting the WebClientConfig class.
UPDATE
I kinda make it work, but it is in very bad shape.
First, I found how to convert WebClientConfig to be reactive:
#Configuration
public class WebClientConfig {
#Bean
WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth.setDefaultOAuth2AuthorizedClient(true);
oauth.setDefaultClientRegistrationId("messaging-client-password");
return WebClient.builder()
.filter(oauth)
.build();
}
#Bean
ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.refreshToken()
.password()
.build();
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
// For the `password` grant, the `username` and `password` are supplied via request parameters,
// so map it to `OAuth2AuthorizationContext.getAttributes()`.
authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());
return authorizedClientManager;
}
private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper() {
return authorizeRequest -> {
Map<String, Object> contextAttributes = Collections.emptyMap();
ServerWebExchange serverWebExchange = authorizeRequest.getAttribute(ServerWebExchange.class.getName());
String username = serverWebExchange.getRequest().getQueryParams().getFirst(OAuth2ParameterNames.USERNAME);
String password = serverWebExchange.getRequest().getQueryParams().getFirst(OAuth2ParameterNames.PASSWORD);
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
contextAttributes = new HashMap<>();
// `PasswordOAuth2AuthorizedClientProvider` requires both attributes
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
}
return Mono.just(contextAttributes);
};
}
}
With this configuration, we can use the WebClient to make a request. This somehow initializes the OAuth2 client after calling the endpoint:
#GetMapping("/explicit")
public Mono<String[]> explicit() {
return this.webClient
.get()
.uri("http://localhost:8092/messages")
.attributes(clientRegistrationId("messaging-client-password"))
.retrieve()
.bodyToMono(String[].class);
}
Then, by calling this one we are able to get the reference to the authorized client:
private OAuth2AuthorizedClient authorizedClient;
#GetMapping("/token")
public String token(#RegisteredOAuth2AuthorizedClient("messaging-client-password") OAuth2AuthorizedClient authorizedClient) {
this.authorizedClient = authorizedClient;
return authorizedClient.getAccessToken().getTokenValue();
}
And finally, by configuring a global filter, we can modify the request to include the Authorization header:
#Bean
public GlobalFilter customGlobalFilter() {
return (exchange, chain) -> {
//adds header to proxied request
exchange.getRequest().mutate().header("Authorization", authorizedClient.getAccessToken().getTokenType().getValue() + " " + authorizedClient.getAccessToken().getTokenValue()).build();
return chain.filter(exchange);
};
}
After running this three requests in order, we can use the password grant with Spring Cloud Gateway.
Of course, this process is very messy. What still needs to be done:
Get the reference for the authorized client inside the filter
Initialize the authorized client with the credentials using contextAttributesMapper
Write all of this in a filter, not in a global filter. TokenRelayGatewayFilterFactory implementation can provide a good help to do this.
I implemented authorization-grant-type: password using WebClientHttpRoutingFilter.
By default, spring cloud gateway use Netty Routing Filter but there is an alternative that not requires Netty (https://cloud.spring.io/spring-cloud-gateway/reference/html/#the-netty-routing-filter)
WebClientHttpRoutingFilter uses WebClient for route the requests.
The WebClient can be configured with a ReactiveOAuth2AuthorizedClientManager through of an ExchangeFilterFunction (https://docs.spring.io/spring-security/site/docs/current/reference/html5/#webclient). The ReactiveOAuth2AuthorizedClientManager will be responsible of management the access/refresh tokens and will do all the hard work for you
Here you can review this implementation. In addition, I implemented the client-credentials grant with this approach

Spring Cloud RestTemplate add auth token

How to correctly implement restTemplate with authorisation token?
I have a Zuul gateway which passes a JWT downstream to other services correctly, assuming I don't want to do anything on the gateway first, using a config like:
zuul:
sensitive-headers:
routes:
instance-service:
path: /instances/**
strip-prefix: false
And using a token relay filter:
#Component
public class TokenRelayFilter extends ZuulFilter {
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
Set<String> headers = (Set<String>) ctx.get("ignoredHeaders");
headers.remove("authorization");
return null;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public String filterType() {
return "pre";
}
#Override
public int filterOrder() {
return 10000;
}
}
Which just forwards everything to the instance-service, works a treat.
However if I remove the routes config from the config.yml file because I want to handle some things on the gateway before manually calling the service I loose the access token and get a 401 back from the downstream services
#ApiOperation(value = "List all instances and their properties.")
#GetMapping("/instances")
public ResponseEntity<String> instances() {
ParameterizedTypeReference<String> reference = new ParameterizedTypeReference<String>() {
};
return restTemplate.exchange("http://instance-service", HttpMethod.GET, null, reference);
}
My RestTemplate is just wired up generically
#Configuration
public class MyConfiguration {
#LoadBalanced
#Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
}
How do I correctly get the JWT back into the new RestTemplate without having to manually create and add a header in each request?
Am I supposed to be using OAuth2RestTemplate?
After some discussion, it seems like you have two options:
Implement and endpoint and dig the Auth header out via #RequestParam on request. From there, you can add it back on for the subsequent outbound request via RestTemplate to your downstream service.
Use Zuul to proxy your request (Auth header included, make sure its excluded from the sensitive-headers config) and implement a pre filter to include any additional logic you might need.
If I had to pick, it sounds like something Zuul should be doing since it's likely acting as your gateway for both your queue and other services, and it looks like you are trying to implement a proxy request, which Zuul can already do, but it's tough to say without knowing the full scope of the architecture.

Resources