Spring WebClient: SSLEngine closed already - spring

We are using Spring boot version 2.3.1 also we use WebClient
My WebClient configuration:
private val client: WebClient
init {
val sslCtx = SslContextBuilder
.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.build()
val httpClient = HttpClient.create().secure { it.sslContext(sslCtx) }
val connector = ReactorClientHttpConnector(httpClient)
client = WebClient.builder()
.clientConnector(connector)
.baseUrl(URL)
.build()
}
private fun post(formData: MultiValueMap<String, String>, response: Class<out Response>, enableLog:
Boolean = true): Response? {
val inserts = BodyInserters.fromFormData(formData)
return try {
client
.post()
.body(inserts)
.retrieve()
.bodyToMono(response)
.block()
} catch (e: Exception) {
if (enableLog) {
log.error("Failed execute request: $formData", e)
}
throw e
}
}
And when I try to debug my application I have this exception:
javax.net.ssl.SSLException: SSLEngine closed already
at io.netty.handler.ssl.SslHandler.wrap(SslHandler.java:848) ~[netty-handler-4.1.50.Final.jar:4.1.50.Final]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ Request to POST null [DefaultWebClient]

Related

Spring Boot Feign client - interceptor not working

I have feign client interceptor which adds Auth header (bearer token being fetched by RestTemplate). If the server responds with 401 (expired token) I want to reauthenticate and try the request again but the interceptor is not getting triggered 2nd time.
Interceptor code:
#Override
public void apply(RequestTemplate requestTemplate) {
if (AuthenticationService.bearerToken == null)
authenticationService.authenticate();
requestTemplate.header(AUTHORIZATION, BEARER_TOKEN_PREFIX + AuthenticationService.bearerToken );
}
Error decoder:
#Override
public Exception decode(String s, Response response) {
FeignException exception = feign.FeignException.errorStatus(s, response);
switch (response.status()) {
case 401:
authenticationService.authenticate();
return new RetryableException(response.status(), exception.getMessage(), response.request().httpMethod(), exception, null, response.request());
case 500:
throw new BadActionException(s, response.reason());
default:
break;
}
return exception;
}
Client config class:
#Bean
public RequestInterceptor requestInterceptor() {
return new RequestInterceptor (authenticationService);
}
#Bean
public RestClientDecoder restClientDecoder() {
return new RestClientDecoder(authenticationService);
}
Feign client:
#FeignClient(value = "server", url = "${server.base-url}", configuration = RestClientConfig.class)
public interface RestClient {
#PostMapping("api/test/{id}/confirm")
void test(#PathVariable Long id);
}
Side note: is there built in interceptor for authentication other than oAuth and BasicAuth? The server I am communicating with has simple jwt auth with expiration.

Handling errors from Spring WebClient in another method

In a Spring Boot application, I'm using WebClient to invoke a POST request to a remote application. The method currently looks like this:
// Class A
public void sendNotification(String notification) {
final WebClient webClient = WebClient.builder()
.defaultHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
.build();
webClient.post()
.uri("http://localhost:9000/api")
.body(BodyInserters.fromValue(notification))
.retrieve()
.onStatus(HttpStatus::isError, clientResponse -> Mono.error(NotificationException::new))
.toBodilessEntity()
.block();
log.info("Notification delivered successfully");
}
// Class B
public void someOtherMethod() {
sendNotification("test");
}
The use case is: A method in another class calls sendNotification and should handle any error, i.e. any non 2xx status or if the request couldn't even be sent.
But I'm struggling with the concept of handling errors in the WebClient. As far as I understood, the following line would catch any HTTP status other than 2xx/3xx and then return a Mono.error with the NotificationException (a custom exception extending Exception).
onStatus(HttpStatus::isError, clientResponse -> Mono.error(NotificationException::new))
But how could someOtherMethod() handle this error scenario? How could it process this Mono.error? Or how does it actually catch the NotificationException if sendNotification doesn't even throw it in the signature?
Well, there are many ways to handle errors, it really depends on what you want to do in case of an error.
In your current setup, the solution is straightforward: first, NotificationException should extend RuntimeException, thus, in case of an HTTP error, .block() will throw a NotificationException. It is a good practice to add it in the signature of the method, accompanied with a Javadoc entry.
In another method, you just need to catch the exception and do what you want with it.
/**
* #param notification
* #throws NotificationException in case of a HTTP error
*/
public void sendNotification(String notification) throws NotificationException {
final WebClient webClient = WebClient.builder()
.defaultHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
.build();
webClient.post()
.uri("http://localhost:9000/api")
.body(BodyInserters.fromValue(notification))
.retrieve()
.onStatus(HttpStatus::isError, clientResponse -> Mono.error(NotificationException::new))
.toBodilessEntity()
.block();
log.info("Notification delivered successfully");
}
public void someOtherMethod() {
try {
sendNotification("test");
} catch (NotificationException e) {
// Treat exception
}
}
In a more reactive style, you could return a Mono and use onErrorResume().
public Mono<Void> sendNotification(String notification) {
final WebClient webClient = WebClient.builder()
.defaultHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
.build();
return webClient.post()
.uri("http://localhost:9000/api")
.body(BodyInserters.fromValue(notification))
.retrieve()
.onStatus(HttpStatus::isError, clientResponse -> Mono.error(NotificationException::new))
.bodyToMono(Void.class);
}
public void someOtherMethod() {
sendNotification("test")
.onErrorResume(NotificationException.class, ex -> {
log.error(ex.getMessage());
return Mono.empty();
})
.doOnSuccess(unused -> log.info("Notification delivered successfully"))
.block();
}
Using imperative/blocking style you can surround it with a try-catch:
try {
webClient.post()
.uri("http://localhost:9000/api")
.body(BodyInserters.fromValue(notification))
.retrieve()
.onStatus(HttpStatus::isError, clientResponse -> Mono.error(NotificationException::new))
.toBodilessEntity()
.block();
} catch(NotificationException e) {...}
A reactive solution would be to use the onErrorResume operator like this:
webClient.post()
.uri("http://localhost:9000/api")
.body(BodyInserters.fromValue(notification))
.retrieve()
.onErrorResume(e -> someOtherMethod())
.toBodilessEntity();
Here, the reactive method someOtherMethod() will be executed in case of any error.

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.

Spring WebClient perform https call

Does anyone know how to configure WebClient in order to make an HTTPS endpoint?
My config looks like that:
#Bean
#NonNull
public WebClient webClient() throws SSLException {
final SslContext context = SslContextBuilder.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.build();
final HttpClient httpClient = HttpClient.create().secure(t -> t.sslContext(context));
return WebClient
.builder()
.exchangeStrategies(ExchangeStrategies.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024))
.build())
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
here is the method witch hits HTTPS endpoint
#Nullable
public AccessToken getAccessToken() {
return webClient
.post()
.uri(uriBuilder -> uriBuilder.path(authUrl)
.queryParam("username", username)
.queryParam("password", password)
.queryParam("client_id", clientId)
.queryParam("client_secret", clientSecret)
.queryParam("grant_type", "password")
.build())
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.exchange()
.flatMap(response -> {
//Error handling
if (response.statusCode().isError()) {
logger.error("error occured while authentication: {}", response.statusCode());
return response.createException().flatMap(Mono::error);
}
return response.bodyToMono(AccessToken.class);
})
.subscribeOn(Schedulers.elastic())
.block();
}
and that's my reponse, unfortunately I'm not allowed to show all the details cause there are secured data.
So I've checked everything like URL, parameters, all looks fine. Also if do the same with restTemaple it works.
Caused by: java.net.UnknownHostException: https:
at java.base/java.net.InetAddress$CachedAddresses.get(InetAddress.java:798)
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ Request to POST https:/<here goes secured endpoint with query parameters>

How can I use client_credentials to access another oauth2 resource from a resource server?

I want to use client_credentials to access another oauth2-protected resource from a reactive resource-server. The part where I'm accessing the resource server using an issued token is working, but not calling the other resource using webclient.
Using UnAuthenticatedServerOAuth2AuthorizedClientRepository I get serverWebExchange must be null, and using AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository I get principalName must be null.
Using https://www.baeldung.com/spring-webclient-oauth2 works as long as I call the client as a CommandLineRunner. None of the other suggestions I have found here on stackoverflow has worked.
What am I missing here? I am using Spring Security 5.2.0 and Spring Boot 2.2.0.
ClientConfig:
#Configuration
public class ClientSecurityConfig {
// UnAuthenticatedServerOAuth2AuthorizedClientRepository version
#Bean
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
return WebClient.builder()
.filter(oauth)
.build();
}
#Bean
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider(CustomClientConfig clientConfig) {
return ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials(clientCredentialsGrantBuilder ->
clientCredentialsGrantBuilder.accessTokenResponseClient(new CustomClient(clientConfig))) // Used to send extra parameters to adfs server
.build();
}
// AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository version
#Bean
WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.filter(oauth)
.build();
}
}
#Bean
ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository, CustomClientConfig clientConfig) {
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials(clientCredentialsGrantBuilder ->
clientCredentialsGrantBuilder.accessTokenResponseClient(new CustomClient(clientConfig))) // Used to send extra parameters to adfs server
.build();
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultReactiveOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}
ResourceServerConfig:
#EnableWebFluxSecurity
class ResourceServerConfig {
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges ->
exchanges
.pathMatchers("/actuators/**", "/api/v1").permitAll()
.pathMatchers("/api/v1/**").hasAuthority("SCOPE_read")
.anyExchange().authenticated()
)
.formLogin().disable()
.httpBasic().disable()
.oauth2Client(withDefaults())
.oauth2ResourceServer().jwt();
return http.build();
}
#RestController()
#RequestMapping("/api/v1")
static class Ctrl {
final static Logger logger = LoggerFactory.getLogger(Ctrl.class);
final WebClient webClient;
public Ctrl(WebClient webClient) {
this.webClient = webClient;
}
#RequestMapping("protected")
Mono<JsonNode> protected(#RequestParam String data) {
return webClient.post()
.uri("https://other-oauth2-protected-resource")
.attributes(clientRegistrationId("myclient"))
.bodyValue("{\"data\": \"" + data + "\"}")
.retrieve()
.bodyToMono(JsonNode.class);
}
}
}
application.yml:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://adfsserver.com/adfs/services/trust
jwk-set-uri: https://adfsserver.com/adfs/discovery/keys
client:
registration:
myclient:
provider: adfs
client-id: <client-id>
client-secret: <client-secret>
authorization-grant-type: client_credentials
scope: read
provider:
adfs:
token-uri: https://adfsserver.com/adfs/oauth2/token
jwk-set-uri: https://adfsserver.com/adfs/discovery/keys
This has recently been fixed by the Spring Project Contributors as part of this PR but unfortunately the official Spring doc is not yet updated.
The normal servlet approach doc is here
If you prefer to choose the "reactive" approach, then configuring a webclient requires only two beans:
the AuthorizedClientManager Bean, and
the webClient Bean
#Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ReactiveOAuth2AuthorizedClientService authorizedClientService) {
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
#Bean
public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder().filter(oauth).build();
}
You can refer to my Github Gist which has all the required configuration.

Resources