How to call Oauth2 protected endpoint from a Spring Boot Java Client using WebClient "serverWebExchange cannot be null" - spring-boot

The requirement is to call an OAuth Protected endpoint from a java client program using WebClient. I'm using Password Grant authorization type using Reactive objects. Please note I'm very novice to reactive programming mode.
When I make the webclient call I get the following error ** serverWebExchange cannot be null **
Please let me know if the below configuration and usage is correct and/or how do I initialize the serverWebExchange object.
String data = webClient
.post().uri(endPoint)
// This will add the Authorization header with the bearer token.
.attributes(ServerOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId("custom"))
.body(Mono.just(alert), Alert.class)
.retrieve()
.bodyToMono(String.class)
// Block until we receive a response for a non-reactive client.
.block();
java.lang.IllegalArgumentException: serverWebExchange cannot be null
at org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager.lambda$loadAuthorizedClient$9(DefaultReactiveOAuth2AuthorizedClientManager.java:102)
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ Request to POST http://<hostname>/<protected_endpoint> [DefaultWebClient]
Stack trace:
at org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager.lambda$loadAuthorizedClient$9(DefaultReactiveOAuth2AuthorizedClientManager.java:102)
at reactor.core.publisher.MonoErrorSupplied.subscribe(MonoErrorSupplied.java:70)
at reactor.core.publisher.Mono.subscribe(Mono.java:4210)
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onComplete(FluxSwitchIfEmpty.java:75)
.....
at reactor.core.publisher.Mono.block(Mono.java:1665)
at com.xxxxx.oauthclient.ApplicationTests.testWebClient(ApplicationTests.java:41)
....
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:191)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:128)
at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:69)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
Suppressed: java.lang.Exception: #block terminated with an error
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:99)
at reactor.core.publisher.Mono.block(Mono.java:1666)
at com.xxxx.oauthclient.ApplicationTests.testWebClient(ApplicationTests.java:41)
....
I have used the following configuration
#Configuration
public class ReactiveOAuthConfig {
#Value("${oauth.username}") String username;
#Value("${oauth.password}") String password;
#Bean
WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.filter(oauth)
.build();
}
#Bean
ReactiveOAuth2AuthorizedClientManager reactiveOAuth2AuthorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.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();
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);
};
}
application properties look like this
spring.security.oauth2.client.provider.custom.token-uri=https://endpoint_to_get_token
spring.security.oauth2.client.registration.custom.client-id=<clientid>
spring.security.oauth2.client.registration.custom.client-secret=<client-secret>
spring.security.oauth2.client.registration.custom.client-authentication-method=post
spring.security.oauth2.client.registration.custom.authorization-grant-type=password
spring.security.oauth2.client.registration.custom.scope=AppIdClaimsTrust
Versions:
spring boot version: 2.2.6.RELEASE
<spring-security.version>5.2.2.RELEASE</spring-security.version>
Note: I was able to validate calling Oauth Protected endpoint from a web client as documented in this https://github.com/jgrandja/spring-security-oauth-5-2-migrate/tree/master/client-app

While building ReactiveOAuth2AuthorizedClientProvider, you should not use authorizationCode . And refresh token should never be used for machine-to-machine communication.
And most importantly dont use the DefaultReactiveOAuth2AuthorizedClientManager. Instead use
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

Related

Pact consumer test does not successfully mock the spring webclient request using the created pact

I am new to Pact Contract testing and I am trying to create a Pact consumer test to validate a method that calls an api with get request. The api request is made using Spring Webclient.
I am not able to create the webclient object by just providing the Pact mockserver eg.
WebClient webClient = WebClient.builder().baseUrl(mockServer.getUrl()).build();
I am getting the exception java.lang.IllegalStateException: No suitable default ClientHttpConnector found. The explanation I get on the internet for that , is to include reactor-netty-http and I was able to get past this issue when i included that in the POM. But I don't think that is the right solution here because I need the mockserver to respond to the webclient request and it is not. Has anyone dealt with this issue before or am I doing this wrong?
Here is the code snippet:
public RequestResponsePact pactMethod(PactDslWithProvider builder) {
Map<String, String> headers = new HashMap<>();
headers.put(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
return builder.given("Consumer request")
.uponReceiving(" getResource call")
.path("/path")
.method("GET")
.willRespondWith()
.status(200)
.headers(headers)
.body(RESPONSE_JSON).toPact();
}
#Test
#PactTestFor(pactMethod = "pactMethod", port = "9999")
public void consumerTest(MockServer mockServer) {
WebClient webClient = WebClient.builder().baseUrl(mockServer.getUrl()).build();
ConsumerServiceClient consumerServiceClient = new ConsumerServiceClient(webClient);
Mono<Data> data = consumerServiceClient.getData();
StepVerifier.create(data)
.assertNext(resp -> {
try {
Value value = resp.getValue();
Assertions.assertFalse( value.isEmpty());
} catch (Exception e) {
log.error("Unable to convert response to Value", e);
Assertions.fail();
}
}).expectComplete()
.verify();
}
The webclient call:
webClient.get()
.uri("/path")
.retrieve()
.onStatus(status -> status == HttpStatus.NOT_FOUND,
res -> Mono.error(new RunTimeException()))
.bodyToMono(clazz);

Spring Webflux Webclient timesout intermittently

I am getting intermittent ReadTimeOut from netty with the below error:
The connection observed an error","logger_name":"reactor.netty.http.client.HttpClientConnect","thread_name":"reactor-http-epoll-3","level":"WARN","level_value":30000,"stack_trace":"io.netty.handler.timeout.ReadTimeoutException: null
One observation we made is this particular endpoint for which we are getting this issue is a POST with no request body. I am now sending a dummy json in body now which the downstream system ignores and now I don't see this error anymore at all.
Below is my code:
protected <T, S Mono<S sendMonoRequest (HttpMethod method,
HttpHeaders headers,
T requestBody,
URI uri, Class < S responseClass)
throws ApiException, IOException {
log.info("Calling {} {} {} {}", method.toString(), uri.toString(), headers.toString(),
mapper.writeValueAsString(requestBody));
WebClient.RequestBodySpec requestBodySpec = getWebClient().method(method).uri(uri);
headers.keySet().stream().forEach(headerKey -> headers.get(headerKey).stream().
forEach(headerValue -> requestBodySpec.header(headerKey, headerValue)));
return requestBodySpec
.body(BodyInserters.fromObject(requestBody != null ? requestBody : ""))
.retrieve()
.onStatus(HttpStatus::is4xxClientError, this::doOn4xxError)
.onStatus(HttpStatus::is5xxServerError, this::doOn5xxError)
.onStatus(HttpStatus::isError, this::doOnError)
.bodyToMono(responseClass);
}
protected WebClient getWebClient () {
HttpClient httpClient = HttpClient.create().tcpConfiguration(
client -> client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,
20000).doOnConnected(conn - conn
.addHandlerLast(new ReadTimeoutHandler(20)).addHandlerLast(new WriteTimeoutHandler(20))));
ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);
return WebClient.builder().clientConnector(connector)
.filter(logResponse())
.build();
}
To resolve the intemrittent timeouts, I have to send a dummy pojo to sendMonoRequest() for request body. Any ideas ?

Stackoverflow when retrieving jwt token in WebTestClient and seting it in ExchangeFilterFunction

The latest Spring Boot 2.3.1.RELEASE, Java 11.
private ExchangeFilterFunction userJwtAuthentication() {
return ExchangeFilterFunction.ofRequestProcessor(
request -> generateToken("user")
.map(jwt -> ClientRequest.from(request)
.headers(headers -> headers.setBearerAuth(jwt))
.build()
)
);
}
private Mono<String> generateToken(String username) {
return this.client
.post().uri("/auth/login")
.bodyValue(AuthenticationRequest.builder().username(username).password("password").build())
.exchange()
.returnResult(new ParameterizedTypeReference<Map<String, String>>() {
})
.getResponseBody()
.last()
.map(d -> d.get("access_token"))
.doOnSubscribe(
jwt -> log.debug("generated jwt token::" + jwt)
);
}
And use it in tests
client.mutate().filter(userJwtAuthentication()).build()
When testing my APIs, it returns.
ava.lang.StackOverflowError
at java.base/java.util.regex.Pattern$Branch.match(Pattern.java:4749)
at java.base/java.util.regex.Pattern$GroupTail.match(Pattern.java:4863)
at java.base/java.util.regex.Pattern$CharPropertyGreedy.match(Pattern.java:4306)
at java.base/java.util.regex.Pattern$GroupHead.match(Pattern.java:4804)
at java.base/java.util.regex.Pattern$Branch.match(Pattern.java:4747)
at java.base/java.util.regex.Pattern$Branch.match(Pattern.java:4747)
at java.base/java.util.regex.Pattern$Begin.match(Pattern.java:3683)
at java.base/java.util.regex.Matcher.match(Matcher.java:1756)
at java.base/java.util.regex.Matcher.matches(Matcher.java:713)
at org.springframework.web.util.UriComponentsBuilder.fromUriString(UriComponentsBuilder.java:215)
at org.springframework.web.util.DefaultUriBuilderFactory$DefaultUriBuilder.initUriComponentsBuilder(DefaultUriBuilderFactory.java:242)
at org.springframework.web.util.DefaultUriBuilderFactory$DefaultUriBuilder.<init>(DefaultUriBuilderFactory.java:233)
at org.springframework.web.util.DefaultUriBuilderFactory.uriString(DefaultUriBuilderFactory.java:160)
at org.springframework.web.util.DefaultUriBuilderFactory.expand(DefaultUriBuilderFactory.java:153)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.uri(DefaultWebClient.java:176)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.uri(DefaultWebClient.java:151)
at org.springframework.test.web.reactive.server.DefaultWebTestClient$DefaultRequestBodyUriSpec.uri(DefaultWebTestClient.java:163)
at org.springframework.test.web.reactive.server.DefaultWebTestClient$DefaultRequestBodyUriSpec.uri(DefaultWebTestClient.java:146)
at com.example.demo.IntegrationTests.generateToken(IntegrationTests.java:246)
at com.example.demo.IntegrationTests.lambda$adminJwtAuthentication$5(IntegrationTests.java:236)
at org.springframework.web.reactive.function.client.ExchangeFilterFunction.lambda$ofRequestProcessor$3(ExchangeFilterFunction.java:79)
at org.springframework.web.reactive.function.client.ExchangeFilterFunction.lambda$apply$2(ExchangeFilterFunction.java:68)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.lambda$exchange$0(DefaultWebClient.java:338)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:44)
at reactor.core.publisher.Mono.subscribe(Mono.java:4219)
at reactor.core.publisher.Mono.block(Mono.java:1702)
at org.springframework.test.web.reactive.server.DefaultWebTestClient$DefaultRequestBodyUriSpec.exchange(DefaultWebTestClient.java:307)
...
If I used blockLast to retrieve token firstly, it worked., my question is how to use Reactor API to do the same work.
The complete codes is here.

Spring Security 5.2 Password Flow

I am trying to authenticate the user using the password flow in the latest version of Spring Security - 5.2.
The docs seem to suggest how to do that.
#Bean
public OAuth2AuthorizedClientManager passwordFlowAuthorizedClientManager(
HttpClient httpClient,
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpClient(httpClient);
DefaultPasswordTokenResponseClient c = new DefaultPasswordTokenResponseClient();
RestTemplate client = new RestTemplate(requestFactory);
client.setMessageConverters(Arrays.asList(
new FormHttpMessageConverter(),
new OAuth2AccessTokenResponseHttpMessageConverter()));
client.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
c.setRestOperations(client);
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.password(configurer -> configurer.accessTokenResponseClient(c))
.refreshToken()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
authorizedClientManager.setContextAttributesMapper(authorizeRequest -> {
Map<String, Object> contextAttributes = new HashMap<>();
String username = authorizeRequest.getAttribute(OAuth2ParameterNames.USERNAME);
String password = authorizeRequest.getAttribute(OAuth2ParameterNames.PASSWORD);
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
return contextAttributes;
});
return authorizedClientManager;
}
I execute the request, I can see the access token returned in HTTP header but the SecurityContext is not populated and the session user remains anonymous.
String username = "joe";
String password = "joe";
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
ClientRegistration r = clientRegistrationRepository.findByRegistrationId("keycloak");
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(r.getRegistrationId())
.principal(authentication)
.attributes(attrs -> {
attrs.put(OAuth2ParameterNames.USERNAME, username);
attrs.put(OAuth2ParameterNames.PASSWORD, password);
})
.build();
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
Any ideas?
After reading into the documentation a bit more I do not think that Oauth 2 password flow in Spring Security 5.2 is supported the same way authorisation flow is. Spring Security 5.2 has password flow support for the http client which can cache the authorization request and refresh the token before it expires - but there is no end user password flow support in which the client proxies the credentials to the authorization server.
Of course, it is entirely possible to authenticate the end user by harvesting the credentials, implementing a custom AuthenticationProvider that swaps the credentials for a token with the authorization server and returns an OAuth2AuthenticationToken that is persisted to the context.

Spring REST template - 401 Unauthorized error

I am using Spring Rest Template inside a Spring Boot Application.
I always get 401 Unauthorized error even though I am passing the credentials.
I am able to access this service by Chrome REST Web Service Client.
Is there a simplified way to access the REST template in SpringBoot.
Below is the code snippet done so far which results in 401 error
private DetailsBean invokeDetailsRestService(UserParam userParam){
ResponseEntity<DetailsBean> responseEntity = null;
String url = "https://dev.com/app/identifyuser/";
RestClientConfig restClientConfig =new RestClientConfig("user123","pass123");
responseEntity= restClientConfig.postForEntity(url, userParam, DetailsBean.class);
log.debug("User Details : {} ", responseEntity.getBody());
return responseEntity.getBody();
}
public ClientHttpRequestFactory getRequestFactory(String userName,String password){
CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials( new AuthScope(null, -1), new UsernamePasswordCredentials(userName,password) );
HttpClient httpClient = HttpClients.custom().setDefaultCredentialsProvider(credsProvider).build();
return new HttpComponentsClientHttpRequestFactory(httpClient);
}
RestClientConfig class
public RestClientConfig(String username, String password) {
CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(
new AuthScope(null, -1),
new UsernamePasswordCredentials(username, password));
HttpClient httpClient = HttpClients.custom().setDefaultCredentialsProvider(credsProvider).build();
setRequestFactory(new HttpComponentsClientHttpRequestFactory(httpClient));
}
Error:
WARN c7af55b5-1cac-4db6-a202-202416c27ba4
12612 --- [apr-8082-exec-8] o.a.http.impl.auth.HttpAuthenticator
: NEGOTIATE authentication error:
No valid credentials provided (Mechanism level:
No valid credentials provided (Mechanism level:
Failed to find any Kerberos tgt))
The authorization issue was fixed with the below code..
Credentials should be passed to a Spring REST Template with the below code:
String userAndPass = "Test:Test123";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
headers.add("Authorization", "Basic " + Base64Utility.encode(userAndPass.getBytes()));
I faced similar issue when i'm trying to make call to webservice, this solved my issue:
restTemplate.getInterceptors().add(new BasicAuthorizationInterceptor("userName", "password"));
restTemplate.postForObject('','',''');
Pass the credentials like this, it should solve the issue.
I used spring boot 2.2.4.RELEASE version. then I work below way.
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(username, password));
RequestDto requestDto = new RequestDto();
// set parameter
ResponseDto response = restTemplate.postForObject(URL, requestDto, ResponseDto.class);
Or
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth(username, password);
RequestDto requestDto = new RequestDto();
// set parameter
HttpEntity<RequestDto> request = new HttpEntity<>(requestDto, headers);
ResponseDto response = restTemplate.postForObject(URL, request, ResponseDto.class);

Resources