I have set up Spring-Security for my webclient application for grant-type as password.
I am receiving the token successfully which is getting attached to my API call request. But on enabling debug logs i could find that there are total 3 oauth2 calls to my oauth endpoint before it actually proceeds to my actual API endpoint. Tried debugging but unable to find what could be causing the issue.
Attaching my config setup here.
Configuration:
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange(exchanges -> exchanges
.pathMatchers("/services/oauth2/token").authenticated()
.anyExchange().permitAll())
.oauth2Client(Customizer.withDefaults())
;
return http.build();
}
@Bean
public ServerSecurityContextRepository securityContextRepository() {
return new WebSessionServerSecurityContextRepository();
}
@Bean(name = "oauth2ClientManager")
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository,
@Qualifier("authorizationServerAuthorizationSuccessHandler") ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler,
@Qualifier("authorizationServerAuthorizationFailureHandler") ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler)
{
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.password()
.build();
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());
authorizedClientManager.setAuthorizationSuccessHandler(authorizationSuccessHandler);
authorizedClientManager.setAuthorizationFailureHandler(authorizationFailureHandler);
return authorizedClientManager;
}
private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper() {
return authorizeRequest -> {
Map<String, Object> contextAttributes = new HashMap<>();
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username");
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password");
return Mono.just(contextAttributes);
};
}
@Bean("authorizationServerAuthorizationSuccessHandler")
ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler(
InMemoryReactiveOAuth2AuthorizedClientService authorizedClientService) {
return new ReactiveOAuth2AuthorizationSuccessHandler() {
@Override
public Mono<Void> onAuthorizationSuccess(OAuth2AuthorizedClient authorizedClient, Authentication principal,
Map<String, Object> attributes) {
LOG.info("Authorization successful for clientRegistrationId={} tokenUri={}", authorizedClient.getClientRegistration().getRegistrationId(), authorizedClient.getClientRegistration().getProviderDetails().getTokenUri());
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
Authentication authentication = new CustomOAuth2AuthenticationToken(
principal,
accessToken,
authorizedClient.getClientRegistration().getClientName()
);
SecurityContext context = new SecurityContextImpl(authentication);
SecurityContextHolder.setContext(context);
LOG.info("SecurityContext set for client: {}", authorizedClient.getClientRegistration().getClientId());
authorizedClientService.saveAuthorizedClient(authorizedClient, principal);
return Mono.empty();
}
};
}
@Bean("authorizationServerAuthorizationFailureHandler")
ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler(
InMemoryReactiveOAuth2AuthorizedClientService authorizedClientService) {
return new RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler(
(clientRegistrationId, principal, attributes) -> {
LOG.info("Authorization failed for clientRegistrationId={}", clientRegistrationId);
authorizedClientService.removeAuthorizedClient(clientRegistrationId, principal.getName());
return Mono.empty();
});
}
@Bean(name = "sslWebClient")
public WebClient sslWebClient(@Qualifier("oauth2ClientManager") ReactiveOAuth2AuthorizedClientManager authorizedClientManager) throws ClientAuthorizationException,Exception {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2.setDefaultClientRegistrationId("common");
oauth2.setDefaultOAuth2AuthorizedClient(true);
HttpClient httpClient = getHttpClient();
httpClient.start();
return WebClient.builder()
.clientConnector(new JettyClientHttpConnector(httpClient))
.filter(oauth2)
.build();
}
Controller class:
@Autowired
@Qualifier("sslWebClient")
private WebClient sslWebClient;
@GetMapping("/sftest")
public Mono<ResponseEntity<String>> sslTest(@RegisteredOAuth2AuthorizedClient("common") OAuth2AuthorizedClient authorizedClient, ServerWebExchange serverWebExchange) throws Throwable {
URI uri = URI.create("https://my.salesforce.com/customers/12456");
return sslWebClient.get()
.uri(uri)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.exchangeToMono(clientResponse -> {
LOG.info("Response received ");
return clientResponse.toEntity(String.class);
}).map(entity -> ResponseEntity.ok(entity.getBody()))
.onErrorResume(CustomApiException.class, e -> {
return Mono.just(ResponseEntity.status(e.getStatus()).body("API error: " + e.getMessage()));
})
.onErrorResume(Exception.class, e -> {
// Handle any other exceptions
return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An unexpected error occurred: " + e.getMessage()));
})
.contextWrite(Context.of(ServerWebExchange.class, serverWebExchange)) // Ensure context is propagated
.retry(0);
}
Dependencies in gradle:
dependencies {
compileOnly("org.projectlombok:lombok:1.18.32")
annotationProcessor("org.projectlombok:lombok:1.18.32")
implementation(group = "org.springframework.boot", name = "spring-boot-starter-webflux", version = springBootVersion)
implementation(group = "org.springframework.boot", name = "spring-boot-starter-jetty", version = springBootVersion)
implementation(group = "org.springframework.boot", name = "spring-boot-starter-oauth2-client", version = springBootVersion)
implementation(group = "org.springframework.security", name = "spring-security-oauth2-client", version = "6.1.6")
implementation("org.eclipse.jetty:jetty-reactive-httpclient:4.0.2")
testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito:mockito-core")
}
application.yml
server:
port: 8087
logging:
level:
org.springframework.security: TRACE
org.springframework.security.oauth2.client: TRACE
org.springframework.security.oauth2.core: TRACE
org.springframework.security.oauth2.client.endpoint: TRACE
org.springframework.security.oauth2.client.web: TRACE
org.springframework.web.reactive.function.client: TRACE
org.springframework.web: TRACE
org.springframework.http: TRACE
spring:
web:
session:
enabled: true
application:
name: webclient-v1
security:
oauth2:
client:
registration:
common:
client-id: xxx
client-secret: xxx
authorization-grant-type: password
client-authentication-method: client_secret_post
provider:
common:
token-uri: https://xxx/services/oauth2/token
Can anybody help me out ?
Instead of allowing spring-security to automatically attach my bearer token to my API call header , I tried to comment out the .filter(oauth2) in webclient builder and added a header in controller class,, which worked but that isnt the ideal way as the filter is supposed to handle token management automatically. So is there a workaround to find why the actual setup isnt working
2