Spring Boot WebClient with OAuth2 and use InsecureTrustManagerFactory - spring-boot

I have successfully implemented WebClient with oAuth2. Facing problem with oAuth2 when the Authentication Server (Keycloak) is having SSL (https). Though I am passing InsecureTrustManagerFactory while defining WebClient, this oAuth is called before the builder is complete as it is there in the filter, it uses default implementation of WebClient and throws certification error.
Is there a way we can configure oAuth2 client also to use InsecureTrustManagerFactory?
pom.xml part
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
Bean Configuration
#Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
final ReactiveClientRegistrationRepository clientRegistrationRepository,
final ReactiveOAuth2AuthorizedClientService authorizedClientService) {
logger.info("ReactiveOAuth2AuthorizedClientManager Bean Method");
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder
.builder().password().build();
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
authorizedClientManager.setContextAttributesMapper(oAuth2AuthorizeRequest -> Mono
.just(Map.of(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, System.getProperty("user"),
OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, System.getProperty("pass"))));
return authorizedClientManager;
}
/**
* The Oauth2 based WebClient bean for the web service
*
* #throws SSLException
*/
#Bean
public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) throws SSLException {
String registrationId = "bael";
SslContext sslContext = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE)
.build();
SslProvider sslProvider = SslProvider.builder().sslContext(sslContext).build();
HttpClient httpClient = HttpClient.create().secure(sslProvider)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000).responseTimeout(Duration.ofMillis(5000))
.doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
.addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS)));
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
authorizedClientManager);
oauth.setDefaultClientRegistrationId(registrationId);
logger.info("WebClient Bean Method");
return WebClient.builder()
// base path of the client, this way we need to set the complete url again
.baseUrl("BASE_URL")
.clientConnector(new ReactorClientHttpConnector(httpClient))
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).filter(logRequest())
.filter(oauth).filter(logResponse()).build();
}

So you have to make new WebClient for OAuth2 too.
In your authorizedClientManager definition add some strings(It's better to have HttpClient bean, so you won't define it all the time)
SslContext sslContext = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE)
.build();
SslProvider sslProvider = SslProvider.builder().sslContext(sslContext).build();
HttpClient httpClient = HttpClient.create().secure(sslProvider)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000).responseTimeout(Duration.ofMillis(5000))
.doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
.addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS)));
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
WebClientReactiveClientCredentialsTokenResponseClient clientCredentialsTokenResponseClient =
new WebClientReactiveClientCredentialsTokenResponseClient();
clientCredentialsTokenResponseClient.setWebClient(webClient);
and add in your authorizedClientProvider ->
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder
.builder().password(builder -> builder.accessTokenResponseClient(clientCredentialsTokenResponseClient)).build();

Related

Spring Security OAuth2 token refreshed by WebClient but stored as "anonymousUser"

I'm trying to implement Spring OAuth2 with WebClient saving refresh tokens for auto renewal in a JdbcOAuth2AuthorizedClientService.
When I first grant access with the Google prompt the token is stored perfectly as a row in the oauth2_authorized_client table for the principal_name that granted access, let's say john#gmail.com.
Later, when I want to use that stored access_token in a scheduled task (no user interaction) I can fetch it like this and inject it in my webClient:
private final OAuth2AuthorizedClientService authorizedClientService;
// ...
OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient("google", "john#gmail.com");
return webClient.get()
.uri(CALENDAR_URL, uriBuilder ->
uriBuilder.path("/calendars/primary/events")
.queryParam("key", API_KEY)
.build())
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String.class);
This time the expired token gets automatically renewed (hurray!) but for some reason it is stored in the database table as a new row with principal_name as anonymousUser instead of john#gmail.com. Why is that?
This is my WebClient configuration:
#Bean
public OAuth2AuthorizedClientService oAuth2AuthorizedClientService(JdbcOperations jdbcOperations,
ClientRegistrationRepository clientRegistrationRepository) {
return new JdbcOAuth2AuthorizedClientService(jdbcOperations, clientRegistrationRepository);
}
#Bean
public WebClient webClient(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientService authorizedClientService) {
AuthorizedClientServiceOAuth2AuthorizedClientManager manager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
manager.setAuthorizedClientProvider(new DelegatingOAuth2AuthorizedClientProvider(
new RefreshTokenOAuth2AuthorizedClientProvider(),
new ClientCredentialsOAuth2AuthorizedClientProvider()));
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
manager);
oauth2.setDefaultClientRegistrationId("google");
return WebClient.builder()
.filter(oauth2)
.apply(oauth2.oauth2Configuration())
.build();
}

How to validate OAuth token in Spring Cloud Gateway

I have a Spring Cloud gateway application which currently supports "HTTP basic auth". I'm planning to use OAuth 2.0 with Keycloak as the provider. I have started by adding required packages (listed below).
implementation "org.springframework.boot:spring-boot-starter-security"
implementation "org.springframework.boot:spring-boot-starter-oauth2-client"
I'm new to this and want to support the following things.
Register clients dynamically to Keycloak
Validate the current request token with Keycloak to allow/ deny the request
My SecurityConfig.java is as given below.
#Configuration
#EnableWebFluxSecurity
public class SecurityConfig {
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http.csrf().disable()
.formLogin().disable()
.logout().disable()
.authorizeExchange(exchanges -> exchanges.anyExchange().permitAll())
.oauth2Client();
return http.build();
}
#Bean
public WebClientHttpRoutingFilter webClientHttpRoutingFilter(
#Qualifier("webClient") WebClient webClient,
ObjectProvider<List<HttpHeadersFilter>> headerFilters) {
return new WebClientHttpRoutingFilter(webClient, headerFilters);
}
#Bean
public WebClient webClient(
#Qualifier("reactiveOAuth2AuthorizedClientManager")
ReactiveOAuth2AuthorizedClientManager reactiveOAuth2AuthorizedClientManager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(reactiveOAuth2AuthorizedClientManager);
oauth.setDefaultOAuth2AuthorizedClient(true);
oauth.setDefaultClientRegistrationId("keycloak");
return WebClient.builder()
.filter(oauth)
.build();
}
#Bean
public ReactiveOAuth2AuthorizedClientManager reactiveOAuth2AuthorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultReactiveOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}

NTLM Authentication using Spring WebClient

How do we achieve NTLM authentication using Spring WebClient? All I could find is only using RestTemplate in a blocking way.
CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(AuthScope.ANY, new NTCredentials(user, password, "source-host-name", "domain-name"));
CloseableHttpClient httpClient = HttpClients.custom()
.setDefaultCredentialsProvider(credsProvider)
.build();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpClient(httpClient);
RestTemplate restTemplate = new RestTemplate(requestFactory);

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