Description
We've been working on an enhancement (gh-8732) that allows an application to provide a custom RestOperations
or WebClient
@Bean
, which would be auto-wired to the related components for oauth2-client
or oauth2-resource-server
. Unfortunately, we ran into a few challenges while trying to come up with a solution for this enhancement. Below are the details outlining the issues we faced.
There are 2 options available for auto-wiring:
- By type
- By bean name
Auto-wire by type
Configuration Scenario 1
If the application context does not contain a @Bean
of type RestOperations
(or WebClient
) then this solution will work. The application would register the customized @Bean
and it will be auto-wired into the related components for oauth2-client
or oauth2-resource-server
.
Configuration Scenario 2
If the application context already contains one or more @Bean
of type RestOperations
(or WebClient
) then this solution will not work. The only way to distinguish which @Bean
to use in the related components for oauth2-client
or oauth2-resource-server
is if the @Bean
is marked as @Primary
. However, if there is already a @Bean
marked as @Primary
, then this is not a viable option either unless the application changes the existing @Primary
@Bean
.
Based on this analysis, auto-wiring by type is NOT a viable solution, since it will not work for ALL configuration scenarios.
Auto-wire by bean name
Assuming Spring Security reserves the @Bean
name oauth2ClientRestOperations
and the application registers a @Bean
with that name, then it would be auto-wired into the related oauth2-client
components. This seemed like a viable solution, however, as we investigated this further, we discovered various configuration scenarios that may become an issue if we went down this path.
The main issue with this solution, is that we would need to reserve the following @Bean
names:
oauth2ClientRestOperations
-oauth2-client
Servletoauth2ClientWebClient
-oauth2-client
WebFluxoauth2ResourceServerRestOperations
-oauth2-resource-server
Servletoauth2ResourceServerWebClient
-oauth2-resource-server
WebFlux
Reserving these 4 @Bean
names is not ideal as we foresee possible issues that may arise by using this bean name strategy. For example, if a Servlet-based application is configured as an oauth2-resource-server
and oauth2-client
(acting as a client), and it needs to customize the RestOperations
, then it would need to register the oauth2ClientRestOperations
and oauth2ResourceServerRestOperations
@Bean
. But what if the customized RestOperations
could be shared between oauth2-client
and oauth2-resource-server
? The application would have to register the RestOperations
@Bean
twice under the 2 distinct names, however, this should not be a requirement.
We did consider using a coarse grained bean naming strategy, eg. oauth2RestOperations
or springSecurityRestOperations
, but we also foresee similar issues that may arise here as well.
Based on this analysis, auto-wiring by name is NOT a viable solution either, since it may introduce issues as described above and we're not 100% confident that the bean naming strategy will work for all possible configuration scenarios.
Motivation for this enhancement
The motivation for this enhancement was initially logged in gh-5607.
Issue gh-7027 and gh-8365 are also related, as the goal is to allow for customizing the underlying HTTP client (RestOperations
or WebClient
).
Based on our analysis and the issues we discovered as described above, it looks like we will NOT be providing this enhancement after all. However, we are not closing the door on this yet, as we would like to gather feedback from the community before we make the final decision.
Having said that, there is still a need for an application to configure a custom RestOperations
or WebClient
(eg. Proxy, TLS, etc.) for the related components in oauth2-client
or oauth2-resource-server
. The following sample configurations will demonstrate how to do so.
OAuth 2.0 Client (Servlet) #
The oauth2-client
components that allow for a custom RestOperations
are:
DefaultAuthorizationCodeTokenResponseClient
DefaultRefreshTokenTokenResponseClient
DefaultClientCredentialsTokenResponseClient
DefaultPasswordTokenResponseClient
DefaultOAuth2UserService
The following configuration could be applied to HttpSecurity.oauth2Login()
that provides a custom RestOperations
:
@EnableWebSecurity
public class OAuth2LoginConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated())
.oauth2Login(oauth2Login ->
oauth2Login
.userInfoEndpoint(userInfoEndpoint ->
userInfoEndpoint
.userService(oauth2UserService())
.oidcUserService(oidcUserService()))
.tokenEndpoint(tokenEndpoint ->
tokenEndpoint
.accessTokenResponseClient(authorizationCodeTokenResponseClient())));
}
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
DefaultOAuth2UserService userService = new DefaultOAuth2UserService();
userService.setRestOperations(oauth2ClientRestOperations());
return userService;
}
@Bean
public OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
OidcUserService userService = new OidcUserService();
userService.setOauth2UserService(oauth2UserService());
return userService;
}
@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeTokenResponseClient() {
DefaultAuthorizationCodeTokenResponseClient tokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
tokenResponseClient.setRestOperations(oauth2ClientRestOperations());
return tokenResponseClient;
}
@Bean
public RestOperations oauth2ClientRestOperations() {
// Minimum required configuration
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
new FormHttpMessageConverter(),
new OAuth2AccessTokenResponseHttpMessageConverter(),
new MappingJackson2HttpMessageConverter()));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
// TODO Add custom configuration, eg. Proxy, TLS, etc
return restTemplate;
}
}
If the application also requires the use of refresh_token
, client_credentials
and password
authorization grants, then the following configuration should also be applied:
...
@Bean
public OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenResponseClient() {
DefaultRefreshTokenTokenResponseClient tokenResponseClient = new DefaultRefreshTokenTokenResponseClient();
tokenResponseClient.setRestOperations(oauth2ClientRestOperations());
return tokenResponseClient;
}
@Bean
public OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsTokenResponseClient() {
DefaultClientCredentialsTokenResponseClient tokenResponseClient = new DefaultClientCredentialsTokenResponseClient();
tokenResponseClient.setRestOperations(oauth2ClientRestOperations());
return tokenResponseClient;
}
@Bean
public OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordTokenResponseClient() {
DefaultPasswordTokenResponseClient tokenResponseClient = new DefaultPasswordTokenResponseClient();
tokenResponseClient.setRestOperations(oauth2ClientRestOperations());
return tokenResponseClient;
}
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken(refreshToken ->
refreshToken.accessTokenResponseClient(refreshTokenTokenResponseClient()))
.clientCredentials(clientCredentials ->
clientCredentials.accessTokenResponseClient(clientCredentialsTokenResponseClient()))
.password(password ->
password.accessTokenResponseClient(passwordTokenResponseClient()))
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
...
OAuth 2.0 Client (WebFlux)
The oauth2-client
reactive components that allow for a custom WebClient
are:
WebClientReactiveAuthorizationCodeTokenResponseClient
WebClientReactiveRefreshTokenTokenResponseClient
WebClientReactiveClientCredentialsTokenResponseClient
WebClientReactivePasswordTokenResponseClient
DefaultReactiveOAuth2UserService
ServerHttpSecurity.oauth2Login()
provides the same configuration options as HttpSecurity.oauth2Login()
so the same configuration could be applied as described for HttpSecurity.oauth2Login()
.
OAuth 2.0 Resource Server (Servlet) #
The oauth2-resource-server
components that allow for a custom RestOperations
are:
NimbusJwtDecoder
NimbusOpaqueTokenIntrospector
See the reference on how to configure NimbusJwtDecoder
with a custom RestOperations
.
See the reference on how to configure NimbusOpaqueTokenIntrospector
with a custom RestOperations
.
OAuth 2.0 Resource Server (WebFlux)
The oauth2-resource-server
reactive components that allow for a custom WebClient
are:
NimbusReactiveJwtDecoder
NimbusReactiveOpaqueTokenIntrospector
See the Servlet reference on how to configure NimbusReactiveJwtDecoder
with a custom WebClient
, as the configuration would be very similar.
See the Servlet reference on how to configure a NimbusReactiveOpaqueTokenIntrospector
with a custom WebClient
, as the configuration would be very similar.
ClientRegistrations #
ClientRegistrations
is intended to be used as a utility/convenience class. It was designed to fulfill most use cases, however, it may not be suitable for certain use cases. For example, if the internal network traffic must be routed through a Proxy, you can bypass discovery by configuring the authorization-uri
and token-uri
property instead of the issuer-uri
property.
NOTE: The underlying HTTP Client used in ClientRegistrations
was purposely encapsulated and there is no plan to expose it.
JwtDecoders \ ReactiveJwtDecoders #
JwtDecoders
and ReactiveJwtDecoders
are both intended to be used as a utility/convenience class. It was designed to fulfill most use cases, however, it may not be suitable for certain use cases. For example, if the underlying HTTP Client requires Proxy and/or TLS settings, you can configure a JwtDecoder
or ReactiveJwtDecoder
with the custom HTTP Client and expose it as a @Bean
.
The reference provides sample configuration on how to configure a custom JwtDecoder
or ReactiveJwtDecoder
@Bean
. See example 1 and example 2.
NOTE: The underlying HTTP Client used in JwtDecoders
and ReactiveJwtDecoders
was purposely encapsulated and there is no plan to expose it.