spring-security icon indicating copy to clipboard operation
spring-security copied to clipboard

Customize RestOperations / WebClient for OAuth 2.0

Open jgrandja opened this issue 4 years ago • 23 comments

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:

  1. By type
  2. 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:

  1. oauth2ClientRestOperations - oauth2-client Servlet
  2. oauth2ClientWebClient - oauth2-client WebFlux
  3. oauth2ResourceServerRestOperations - oauth2-resource-server Servlet
  4. oauth2ResourceServerWebClient - 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:

  1. DefaultAuthorizationCodeTokenResponseClient
  2. DefaultRefreshTokenTokenResponseClient
  3. DefaultClientCredentialsTokenResponseClient
  4. DefaultPasswordTokenResponseClient
  5. 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:

  1. WebClientReactiveAuthorizationCodeTokenResponseClient
  2. WebClientReactiveRefreshTokenTokenResponseClient
  3. WebClientReactiveClientCredentialsTokenResponseClient
  4. WebClientReactivePasswordTokenResponseClient
  5. 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:

  1. NimbusJwtDecoder
  2. 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:

  1. NimbusReactiveJwtDecoder
  2. 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 #

Related gh-7027 gh-5543

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 #

Related gh-8365 gh-5543

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.

jgrandja avatar Jul 28 '20 01:07 jgrandja

Are there any examples on how to configure a JWTDecoder to use a custom HTTP Client?

georgejdli avatar Feb 01 '21 15:02 georgejdli

@georgejdli Please see the reference.

jgrandja avatar Feb 01 '21 15:02 jgrandja

@jgrandja Thanks! In case anyone else was wondering this is what worked for me

import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.web.client.RestTemplate;

import java.net.InetSocketAddress;
import java.net.Proxy;
@Bean
    public JwtDecoder jwtDecoder() {
        Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy.example.com", 8080));
        SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
        requestFactory.setProxy(proxy);

        return NimbusJwtDecoder
                .withJwkSetUri(jwkSetURI)
                .restOperations(new RestTemplate(requestFactory)).build();
    }
    ```

georgejdli avatar Feb 01 '21 16:02 georgejdli

Edited by removing parts not relevant to this issue Would it also be good to allow customization that enables injecting own JWKSource <C extends SecurityContext>? This would allow injecting keys from any source, or implementing more elaborate caching strategies.

piotrplazienski avatar Apr 02 '21 10:04 piotrplazienski

@piotrplazienski There are a couple of different issues/enhancements you mentioned that are not directly related to this issue.

Please log questions on StackOverflow and new issue(s) for each new feature or enhancement here.

jgrandja avatar Apr 05 '21 11:04 jgrandja

Hello,

We have an issue with the lack of customization for JwtDecoderProviderConfigurationUtils.

As far as I can tell, it's currently the only way to deal with OpenID Provider Configuration (spring.security.oauth2.resourceserver.jwt.issuer-uri) which is currently the only way to validate tokens from a big name identity provider who doesn't feel like implementing an introspection endpoint

NimbusJwtDecoder only offer introspection and JWK set support which are not implemented in that Identity provider.

I haven't found anything related to an extension for issuer-uri in nimbus.

So our only solution seem to be to completely rewrite the JwtDecoders to be able to access our issuer-uri through a proxy, customizing timeouts, etc.

Choobz avatar Jul 16 '21 15:07 Choobz

@Choobz There are a few different options available for configuring a JwtDecoder using issuer-uri. Please review the reference documentation for Resource Server.

Here are a couple of references to jump to:

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
        JwtDecoders.fromIssuerLocation(issuerUri);

    OAuth2TokenValidator<Jwt> audienceValidator = audienceValidator();
    OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
    OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

    jwtDecoder.setJwtValidator(withAudience);

    return jwtDecoder;
}

jgrandja avatar Jul 19 '21 12:07 jgrandja

@jgrandja thanks a lot for your answer.

Although, our problem still stand unfortunatly : JwtDecoders.fromIssuerLocation(issuerUri); calls JwtDecoderProviderConfigurationUtils .getConfigurationForIssuerLocation(issuer) which uses ResponseEntity<Map<String, Object>> response = rest.exchange(request, STRING_OBJECT_MAP);

rest being a non modifiable RestTemplate.

So in order to implement our own JwtDecoders.fromIssuerLocation that does strictly the same thing but only with a configurable RestOperation (proxy, timeout...) we need to copy/paste 99% of the code of JwtDecoderProviderConfigurationUtils and JwtDecoders just to be able to do : JwtDecoders.fromIssuerLocation(issuer, restOperation) (or probably better, do as the NimbusJwtDecoder that allow for a customizable restOperation by pushing the call into the build function and not immediatly like JwtDecoders.fromIssuerLocation(issuerUri); does it)

From the list of enhancement, it seems you want to get rid/limit the dependency of spring oauth on nimbus. Which is great but why not implement JwtDecoders.fromIssuerLocation(issuerUri); like NimbusJwtDecoder ?

Pseudo-code :

class OAuth2ResourceServerJwtConfiguration {
    @Bean
    @Conditional(IssuerUriCondition.class)
    JwtDecoder jwtDecoderByIssuerUri() {
        // Default to virgin RestTemplate
        return JwtDecoders.fromIssuerLocation(this.properties.getIssuerUri()).restOperation(new RestTemplate()).build();
    }
...
public final class JwtDecoders {
    public IssuerBuilder fromIssuerLocation(String issuer) {
        return new IssuerBuilder().fromIssuerLocation(issuer);
    }

    public final class IssuerBuilder {
        private RestOperation rest;
        private String issuer; 
    
        public JwtDecoderBuilder fromIssuerLocation(String issuer) {
                Assert.hasText(issuer, "issuer cannot be empty");
		this.issuer=issuer;
		return this;
	}

        public JwtDecoderBuilder restOperations(RestOperation rest) {
                Assert.notNull(rest, "restcannot be null");
		this.rest=rest;
		return this;
	}

        public <T extends JwtDecoder> build(){
                Map<String, Object> configuration = JwtDecoderProviderConfigurationUtils
				.getConfigurationForIssuerLocation(issuer, rest);
		return (T) withProviderConfiguration(configuration, issuer);
        }
    }
}
...

(oc its only a quick hacky way of doing it, there's a lot to improve)

So that from a client perspective it becomes quite easy to use :

@Bean
public JwtDecoder jwtDecoder(RestTemplate whatever) {
    return JwtDecoders 
        .fromIssuerLocation(issuerUri)
        .restOperations(whatever)
        .build();
}

I'm probably missing something obvious because it's real shame to set aside all the work on the inbuilt issuerDecoder and reinvent the wheel in our codebases just to be able to parametrized the RestTemplate used to retrieve the configuration in the .well-known endpoint :(.

Choobz avatar Jul 19 '21 16:07 Choobz

Is there any update on this issue?

In our project we need to configure a proxy for the RestTemplate that is used within ClientRegistrations. That's why we stumbled across https://github.com/spring-projects/spring-security/issues/7027

Our current workaround is to change the underlying RestTemplate with Reflection.

dkroehan avatar Apr 29 '22 11:04 dkroehan

This has come up again in a Spring Boot issue. The example in the documentation works well for creating a decoder from a JWK set URI but, as @Choobz described above, things are rather cumbersome when working with an issuer URI. I think that this suggestion from @jzheaux would address this quite nicely.

wilkinsona avatar May 09 '22 09:05 wilkinsona

@dkroehan do you, by any chance, have your reflection code and would you share it with me?

essmuc avatar May 25 '22 08:05 essmuc

@wilkinsona Apologies for the delay. I'm assuming you saw gh-10309?

Regarding your comment:

This has come up again in a Spring Boot issue

Can you provide a link to the Spring Boot issue so I can review the comments?

Do you feel we should prioritize gh-10309 based on the feedback you've been getting on the Spring Boot side of things?

jgrandja avatar May 26 '22 19:05 jgrandja

@dkroehan

Is there any update on this issue?

There are no plans to enhance ClientRegistrations - please review comments above in ClientRegistrations.

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.

jgrandja avatar May 26 '22 19:05 jgrandja

I'm assuming you saw https://github.com/spring-projects/spring-security/issues/10309?

Indeed I did.

Can you provide a link to the Spring Boot issue so I can review the comments?

Sorry, I messed up the link in my earlier comment. It's https://github.com/spring-projects/spring-boot/issues/30891.

Do you feel we should prioritize https://github.com/spring-projects/spring-security/issues/10309 based on the feedback you've been getting on the Spring Boot side of things?

Yes, please. It feels like there's some low-hanging fruit here for Spring Security to provide some convenience APIs to make this easier and avoid people having to copy-paste lots of boilerplate when they want to customize things a little.

wilkinsona avatar May 26 '22 19:05 wilkinsona

@wilkinsona ok, we'll prioritize this. I'll get back to you shortly on a proposed solution so you can confirm it's flexible enough for Boot. We will consider NimbusJwtDecoder#withIssuerLocation but it might be a higher-level (more flexible) abstraction like NimbusJwtDecoder#withJwkSource (or combination of both).

jgrandja avatar May 27 '22 10:05 jgrandja

@jgrandja Thank you for the hint on the above comments for the ClientRegistrations, will try that out!

@essmuc Below is our code that modifies the private static final RestTemplate inside ClientRegistrations. Maybe you should check out the hint from @jgrandja before going that way.

// TODO remove after https://github.com/spring-projects/spring-security/issues/8882 expectedly introduces support for configuring a proxy
    static {
        String httpsProxy = System.getenv("HTTPS_PROXY");
        if (httpsProxy != null) {
            URI uri = URI.create(httpsProxy);
            Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(uri.getHost(), uri.getPort()));
            SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
            requestFactory.setProxy(proxy);

            try {
                RestTemplate restTemplate = new RestTemplate(requestFactory);
                updateFinalStaticField(ClientRegistrations.class.getDeclaredField("rest"), restTemplate);
            } catch (Exception e) {
                LOGGER.error("Error when setting proxy for RestTemplate in Spring Security ClientRegistrations", e);
            }
        }
    }

    private static void updateFinalStaticField(Field field, Object newValue) throws Exception {
        field.setAccessible(true);

        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

        field.set(null, newValue);
    }

dkroehan avatar May 27 '22 14:05 dkroehan

As stated in ClientRegistrations:

NOTE: The underlying HTTP Client used in ClientRegistrations was purposely encapsulated and there is no plan to expose it.

However, there is a clear need to be able to customize the underlying HTTP client (e.g. proxy settings) so we're considering the following enhancement that will allow customization to override the default underlying HTTP client and provide your own HTTP client configured with whatever settings are required.

The following is the proposed enhancement for ClientRegistrations:

private static final Function<URI, Map<String, Object>> DEFAULT_METADATA_RESOLVER = (metadataEndpoint) -> {
	RequestEntity<Void> request = RequestEntity.get(metadataEndpoint).build();
	return rest.exchange(request, typeReference).getBody();
};

public static ClientRegistration.Builder fromOidcIssuerMetadata(String issuer, Function<URI, Map<String, Object>> metadataResolver) {
	URI issuerUri = URI.create(issuer);

	// @formatter:off
	URI issuerEndpointUri = UriComponentsBuilder.fromUri(issuerUri)
			.replacePath(issuerUri.getPath() + OIDC_METADATA_PATH)
			.build(Collections.emptyMap());
	// @formatter:on

	return getBuilder(issuer, () -> {
		// metadataResolver is either DEFAULT_METADATA_RESOLVER or a custom (provided) resolver
		Map<String, Object> configuration = metadataResolver.apply(issuerEndpointUri);

		OIDCProviderMetadata metadata = parse(configuration, OIDCProviderMetadata::parse);
		ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuerUri.toASCIIString())
				.jwkSetUri(metadata.getJWKSetURI().toASCIIString());
		if (metadata.getUserInfoEndpointURI() != null) {
			builder.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString());
		}
		return builder;
	});
}

We are also considering the same enhancement for JwtDecoders \ ReactiveJwtDecoders.

jgrandja avatar May 27 '22 15:05 jgrandja

It would also be nice if this was easily configurable for AbstractWebClientReactiveOAuth2AccessTokenResponseClient - in particular, there's no easy way to configure timeouts so they're picked up without needing to override the logic or implement a decorator pattern to the webclient.

motinis avatar Jun 02 '22 08:06 motinis

@motinis Your comment is not really related to the main issue here.

In case you missed it, you can supply AbstractWebClientReactiveOAuth2AccessTokenResponseClient.setWebClient() with a WebClient configured with the timeouts you require.

jgrandja avatar Jun 02 '22 09:06 jgrandja

@dkroehan thank you for your code. I just went forward by copying a lot of code. from spring security into our code base. I ran into other problems and reading you code I am not sure how you handled the issue. getSignatureAlgorithms uses RemoteJWKSet<>(url(jwkSetUri)) which does not even use a RestTemplate I added another proxy using DefaultResourceRetriever? But it feels like I am going from problem to problem.

essmuc avatar Jun 02 '22 10:06 essmuc

@wilkinsona Just a heads up that we've scheduled gh-10309 for 5.8.0-M2.

jgrandja avatar Jul 18 '22 20:07 jgrandja

Thanks, Joe!

wilkinsona avatar Jul 19 '22 11:07 wilkinsona

Due to a recent change in the nimbus jose jwt library 1, this issue can become critical:

  • the default read/write timeouts for the http clients used by a 'new RestTemplate()' ist 'infitite'
  • if a request is blocked for any reason, all other requests processed by the same DefaultJWTProcessor are blocked as well.

In our case, this completely blocked all request processing threads and the service hat to be restartet.

At the very least, the connect/read/write timeouts should be set to sensible defaults for versions of spring-security-oauth2-jose that pull in the affected version of nimbus-jose-jwt (>= 9.16.1).

altery avatar Sep 09 '22 11:09 altery

@jgrandja Thanks! In case anyone else was wondering this is what worked for me

import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.web.client.RestTemplate;

import java.net.InetSocketAddress;
import java.net.Proxy;
@Bean
    public JwtDecoder jwtDecoder() {
        Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy.example.com", 8080));
        SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
        requestFactory.setProxy(proxy);

        return NimbusJwtDecoder
                .withJwkSetUri(jwkSetURI)
                .restOperations(new RestTemplate(requestFactory)).build();
    }
    ```

Regrettably, the solution provided by @jgrandja didn't work.

Everything we found online, we tried programmatically and failed epically! The documentation that we find online don't include the Proxy case which is regrettable.

We tried to use System.setProperty in many places on the code, including the main but its ignored.

However, we still get An I/O error occurred while reading from the JWK Set source: login.microsoftonline.com when trying to run it.

We found that the the only thing that works so far is to set the VM options using flags -Dhttps.proxyHost=... which is the thing that we are trying to not have to use in the first place. Then, the I/O error doesn't show up and it works.

Does anyone have any other solution that we can try to make this work behind a proxy white connected to a VPN? Thank you in advance.

acarlstein avatar Feb 21 '23 15:02 acarlstein

This is the part that fails in org.springframework.security.oauth2.provider.token.store.jwk

This is the piece that fails:
static Map<String, JwkDefinitionHolder> loadJwkDefinitions(URL jwkSetUrl) {
		InputStream jwkSetSource;
		try {
			jwkSetSource = jwkSetUrl.openStream();
		} catch (IOException ex) {
			throw new JwkException("An I/O error occurred while reading from the JWK Set source: " + ex.getMessage(), ex);
		}

Normally, to setup the proxy you would do something like this:

Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("10.0.0.1", 8080));
conn = new URL(urlString).openConnection(proxy);

However, the spring-security-oauth2 dependency, doesn't have a way to do this; therefore, the only way is to add to the main:

    System.setProperty("https.proxyHost", "internet.ford.com")
    System.setProperty("https.proxyPort", "83")
    System.setProperty("http.proxyHost", "internet.ford.com")
    System.setProperty("http.proxyPort", "83")
    System.setProperty("http.nonProxyHosts", "*.ford.com|localhost")
    System.setProperty("https.nonProxyHosts", "*.ford.com|localhost")

or from the command line with the -D syntax

Which is super painful!!!

acarlstein avatar Feb 21 '23 19:02 acarlstein

Getting this running behind a proxy took a lot of effort and I'm still not 100% sure exactly why it now works - we are not using a vpn but it is worth a try

We ended up registering a JwtDecoderFactory rather than a JwtDecoder itself

(Kotlin)

    @Bean
    fun jwtDecoderFactory(restOperations: RestOperations): JwtDecoderFactory<ClientRegistration> {
        val jwtDecoder: JwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
            .restOperations(restOperations).build()
        return JwtDecoderFactory { jwtDecoder }
    }

where restOperations is

    @Bean
    fun restOperations(): RestOperations {
        val requestFactory = SimpleClientHttpRequestFactory()
        requestFactory.setProxy(httpProxy())

        return builder.additionalInterceptors(RequestLoggingInterceptor)
            .requestFactory { requestFactory }
            .messageConverters(
                ByteArrayHttpMessageConverter(),
                StringHttpMessageConverter(),
                ResourceHttpMessageConverter(),
                FormHttpMessageConverter(),
                OAuth2AccessTokenResponseHttpMessageConverter(),
                MappingJackson2HttpMessageConverter()
            )
            .errorHandler(OAuth2ErrorResponseErrorHandler())
            .build()
    }

Alternatively using a ProxySelector worked but also affects any other apps etc in the same JVM - Tomcat in our case

class StaticProxySelector(address: InetSocketAddress) : ProxySelector() {
    private val noProxyList: List<Proxy> = listOf(Proxy.NO_PROXY)
    private val list: List<Proxy>

    init {
        list = run {
            val p: Proxy = Proxy(Proxy.Type.HTTP, address)
            listOf(p)
        }
    }

    override fun connectFailed(uri: URI?, sa: SocketAddress?, e: IOException?) {
    }

    @Synchronized
    override fun select(uri: URI): List<Proxy> {
        val scheme = uri.scheme.lowercase(Locale.getDefault())
        return if (scheme == "http" || scheme == "https") {
            list
        } else {
            noProxyList
        }
    }

and initialising it early in app startup

   private val proxyAddress = InetSocketAddress(proxyHostname, proxyPort)

   init {
        ProxySelector.setDefault(StaticProxySelector(proxyAddress))
    }

mallen avatar Feb 22 '23 07:02 mallen

Hey @mallen, at least you provide me with some code that I can play and experiment with and I appreciate that. Thank you.

acarlstein avatar Feb 24 '23 16:02 acarlstein

Thanks everyone for so much valuable input on this issue!

We have introduced simplified configuration via gh-11783 that allows for configuring a RestTemplate with proxy settings for OAuth2 Client components. See this example in the docs. Servlet support will be released in 6.2.0, with reactive support for WebClient coming in a future release.

  • [x] gh-11783
  • [ ] gh-13763

Note also the related issue for adding general support for RestClient via gh-13588. See the issue description for other related issues which have largely been addressed.

With that in mind, I'm finally going to close this issue. If anyone has additional requests, please open a new enhancement.

sjohnr avatar Sep 20 '23 15:09 sjohnr