Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide support for OAuth2 in Spring Config Client. #2348

Open
cobar79 opened this issue Nov 7, 2023 · 22 comments
Open

Provide support for OAuth2 in Spring Config Client. #2348

cobar79 opened this issue Nov 7, 2023 · 22 comments

Comments

@cobar79
Copy link

cobar79 commented Nov 7, 2023

I would like to implement OAuth2 resource server in the Spring Config Server and require JWT for all configuration requests.

Is your feature request related to a problem? Please describe.

I can't seem to find any method to implement a Spring Security OAuth2 Client Provider to the Spring Cloud Client call.
Overriding ConfigServicePropertySourceLocator
#177 and Adding Generic "Authorization" support for Config Client do not appear to work with current import connection framework "optional:configserver:http://${env.config.hostname}:${env.config.port}/config-server"
The call to the Configuration Server is made before the ConfigServicePropertySourceLocator is instantiated.

Describe the solution you'd like
I would like to use the same implementation used for Machine to Machine OAuth2 communication where by the Spring Security handles obtaining and refreshing the Bearer Token to be passed to the Spring Config Server calls on startup. Preferably a WebClient over RestTemplate solution.

    @Bean
    ReactiveClientRegistrationRepository clientRegistrations(
        @Value("${spring.security.oauth2.client.provider.keycloak-client.token-uri}") String tokenUri,
        @Value("${spring.security.oauth2.client.registration.keycloak-client.client-id}") String clientId,
        @Value("${spring.security.oauth2.client.registration.keycloak-client.client-secret}") String clientSecret,
        @Value("${spring.security.oauth2.client.registration.keycloak-client.scope}") String scope,
        @Value("${spring.security.oauth2.client.registration.keycloak-client.authorization-grant-type}") String authorizationGrantType
    ) {

        Collection<String> scopeList = Arrays.asList(scope.split(","));

        ClientRegistration registration = ClientRegistration
            .withRegistrationId(registrationId)
            .tokenUri(tokenUri)
            .clientId(clientId)
            .clientSecret(clientSecret)
            .scope(scopeList)
            .authorizationGrantType(new AuthorizationGrantType(authorizationGrantType))
            .build();
        return new InMemoryReactiveClientRegistrationRepository(registration);
    }

    @Bean(value = "authWebClient")
    WebClient authWebClient(ReactiveClientRegistrationRepository clientRegistrations) {
        InMemoryReactiveOAuth2AuthorizedClientService clientService = new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrations);
        AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, clientService);
        ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        oauth.setDefaultClientRegistrationId(registrationId);
        return WebClient.builder()
            .filter(oauth)
            .baseUrl(baseUrl)
            .build();
    }

Describe alternatives you've considered
A method to intercept the Spring Config Server rest call, call the IDP and include the Bearer Token manually, there by bypassing the current RestTemplate and any Basic Authentication.

Additional context
Spring Boot 3.1.2, Spring Security 6.1.2
Keycloak OAuth2 implementation

Configuration Server Security

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
        MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
        http.anonymous(AbstractHttpConfigurer::disable);
        http.csrf(AbstractHttpConfigurer::disable);
        http.authorizeHttpRequests(authorize -> authorize
            .requestMatchers(mvcMatcherBuilder.pattern("/" + appName + "/actuator/**")).hasRole("actuator")
            .requestMatchers(mvcMatcherBuilder.pattern("/" + appName + "/**")).hasRole("m2m")
            //Permit webhook refresh access without authorization
            .requestMatchers(mvcMatcherBuilder.pattern("/" + appName + "/monitor")).permitAll()
            .anyRequest()
            .authenticated()
        );

        http.oauth2ResourceServer(authorize ->
            authorize.jwt(jwt -> jwt.jwtAuthenticationConverter(keycloakJwtTokenConverter))
                .authenticationEntryPoint(customAuthenticationEntryPoint)
        );

        http.sessionManagement(session -> session
            .sessionCreationPolicy(stateless? SessionCreationPolicy.STATELESS: SessionCreationPolicy.IF_REQUIRED)
        );
        return http.build();
    }

@ryanjbaxter
Copy link
Contributor

The commercial offer of Spring Cloud, Spring Cloud Services, uses OAuth2 to authorize the client to use the config server.
You can find the code for how they do that here
spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client

I suppose you could do something similar.

@cobar79
Copy link
Author

cobar79 commented Nov 8, 2023

spring-cloud-services-starters looks to be a heavy port.

I was looking for a 3-5 story point way of overwriting the current RestTemplate logic.

Unfortunately, attempts of old school filter/interceptors did not work as the call was made before the filter/interceptors where instantiated. I guess I will look at extending the existing framework to override the basic authorization.

@cobar79
Copy link
Author

cobar79 commented Nov 14, 2023

@ryanjbaxter Should I just close this? It was meant as a feature request. However, it doesn't look it is being considered.

@ryanjbaxter
Copy link
Contributor

I am not saying you need to port exactly what spring-cloud-services did I am just showing that it is possible to authorize the client before the request to the server is made, specifically I think this class is where that is done.

https://github.com/pivotal-cf/spring-cloud-services-starters/blob/29b7961ec57ec918110b669fb2409d55aacc8974/spring-cloud-services-config-client-autoconfigure/src/main/java/io/pivotal/spring/cloud/config/client/ConfigResourceClientAutoConfiguration.java#L39

@kvmw am I correct?

@kvmw
Copy link
Contributor

kvmw commented Nov 15, 2023

@ryanjbaxter @cobar79 ,
You need to configure the RestTemplate in 2 different places :

  1. Firstly, you need a BootstrapRegistryInitializer. Since the call to config-server is done during the application startup you need to configure the RestTemplate before startup, during the bootstrapping using a BootstrapRegistryInitializer. check ConfigClientOAuth2BootstrapRegistryInitializer in SCS Starters, for example. This has been mentioned in the Spring Cloud Config Docs too.

  2. Also, if your client application consumes plain text or binary resources, you need to configure a ConfigResouceClient as well. check ConfigResourceClientAutoConfiguration in SCS Starters, for example. You can ignore this configuration if you don't need to consume such resources.

The ConfigResourceClientAutoConfiguration which was mentioned above by @ryanjbaxter, is for the client applications with legacy processing enabled. This is the alternative for BootstrapRegistryInitializer and you can ignore it if your application is developed recently and does not need legacy processing.

@ryanjbaxter ,
It would be nice if ConfigServerConfigDataLoader in Spring Cloud Config Client would provide an easier way to inject the RestTemplate. I don't know if alternative approach is even possible but the current one (using BootstrapRegistryInitializer) is not the most straightforward and intuitive one.

@ryanjbaxter
Copy link
Contributor

I am not sure there would be an easier way because we are so early in the startup of the Boot application at the point we are loading configuration.

@spring-cloud-issues
Copy link

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

@cobar79
Copy link
Author

cobar79 commented Nov 22, 2023

@ryanjbaxter @kvmw

Great discussion. However, this was just meant to be a feature request. As a Spring Developer, I have a requirement to lock down the config server, thus making it an OAuth2 resource server. Therefore, this requires the config server clients to authenticate with a Bearer token from the IDP. I would think this would be very common occurrence in the Spring Community?

As a proof of concept I cloned this project and with small changes to ConfigClientRequestTemplateFactory and ConfigClientProperties I was able to obtain a Bearer token from IDP and append it to the config server endpoint call.

We are investigating the commercial Cloud-Services-for-VMware-Tanzu product cost and portability to AWS. However, it would be more convenient and easier to maintain the additional feature to this project than it will be to implement the commercial version.

@kvmw
Copy link
Contributor

kvmw commented Nov 22, 2023

@cobar79
As I mentioned above the call to Config Server happens during bootstrap before any of those beans you mentioned above are even initialised. So changing those beans doesn't help.

Have a look at this code once again:
https://github.com/spring-cloud/spring-cloud-config/blob/main/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java#L262

@cobar79
Copy link
Author

cobar79 commented Nov 22, 2023

@kvmw
Again, just proof of concept but it works:

The method below creates a Rest Template. I simply detect an OAuth2 property and use the Rest Template to get the Token from IDP and add it to the headers map which is added to the GenericRequestHeaderInterceptor. Later when the Config Server call is made, it uses the GenericRequestHeaderInterceptor adds the Bearer Token header in included in the call.

ConfigClientRequestTemplateFactory.create

                String tokenUri = properties.getTokenUri();
		if (tokenUri != null) {
			String token = getOAuthToken(template, tokenUri);
			headers.put(AUTHORIZATION, "Bearer " + token);
			properties.setHeaders(headers);
		}

injected just before adding the inteceptor

		if (!headers.isEmpty()) {
			template.setInterceptors(Arrays.asList(new GenericRequestHeaderInterceptor(headers)));
		}

@kvmw
Copy link
Contributor

kvmw commented Nov 23, 2023

@cobar79
I see what you mean now. I was thinking your poc work is in your code rather than spring-cloud-config code.
I think that would work but not sure if the spring-config team would want that.

@ryanjbaxter
would you consider a pull-request to support this requirement?

@ryanjbaxter
Copy link
Contributor

Absolutely!

@cobar79
Copy link
Author

cobar79 commented Nov 29, 2023

Thanks @ryanjbaxter
I will look over the guidelines and supporting documentation. I may need guidance moving from POC to PR since this will be my first open source contribution. Here are some areas or questions I have.

  1. I replicated the spring.security.oauth2.client properties to fit into the spring.cloud.config properties. I wasn't crazy about duplicating the properties but given the impact of loading multiple properties and complexity of OAuth2 properties I think I would stick with updating ConfigClientProperties with OAuth2 properties. Even the commercial version flattened the OAuth2 properties into a single client registration.
spring:
  cloud:
    config:
        token-uri: ${oauth.scheme}://localhost:${env.idp.port}/realms/${oauth.realm}/protocol/openid-connect/token
        client-id:

I was planning on leaving the basic authentication properties as is and using username/password for both oauth2 and basic authentication. Thoughts?

spring:
  cloud:
    config:
      username:
      password:
      token:
  1. Client/User credential encryption: Most cyber teams won't allow plain text credentials in property files. That leaves environment/system properties or encrypting the credentials. I assume the Spring team would just want to go with environment/system properties for this? In the POC, I actually incorporated a Jayspt SimplePBEStringEncryptor. I passed in the algorithm and iterations via the properties and set the encryption password as an environment variable.
spring:
  cloud:
    config:
      token-uri: 
      client-id: ENC(blablabla)
      client-secret: ENC(yaddaYaddaYadda)
      username: ENC()
      password: ENC()
      encryptor-algorithm: PBEWITHHMACSHA512ANDAES_256
      encryptor-iterations: 1000
		<dependency>
			<groupId>com.github.ulisesbocchio</groupId>
			<artifactId>jasypt-spring-boot</artifactId>
			<version>3.0.5</version>
		</dependency>

@ryanjbaxter
Copy link
Contributor

We love first time contributors ❤️

  1. That sounds fine to me

  2. We have the ability to encrypt and decrypt properties as part of Spring Cloud Config. Have you looked into leveraging that functionality?

https://docs.spring.io/spring-cloud-config/docs/current/reference/html/#_encryption_and_decryption

@cobar79
Copy link
Author

cobar79 commented Nov 30, 2023

Thanks Ryan.

I have it mostly stubbed out with JCE. I am having issues with JCE cypher properties in the Config Client application. I can't seem to find any documentation on using JCE in a normal Boot application. Do you happen to know where I can find documentation?

Removed old Jasypt encryption and attempted to try simple Symmetric encryption first.

    Property: spring.cloud.config.client-secret
    Value: "{cipher}cdac3c554d3f6fbdc6f2f553a7b3b3e1b64b1fbb3b1c8fa5b2766be4dafc0b6a6ca38e13442787f5b779dbbd3634001f7a89d030ad8f0363c61a7719d425ef21"
    Origin: URL [file:config/application-local.yml] - 78:22
    Reason: java.lang.UnsupportedOperationException: No decryption for FailsafeTextEncryptor. Did you configure the keystore correctly?

This seems to say you can't do Symmetric Encryption, but thought I would ask.

@cobar79
Copy link
Author

cobar79 commented Dec 4, 2023

@ryanjbaxter
Ready for PR. How do I obtain access to push feature branch and create PR?

I went with Jasypt encryption since I couldn't find supporting documentation of JCE support for the client boot application. See comment above.

@cobar79
Copy link
Author

cobar79 commented Dec 6, 2023

@kvmw
Can you help me with getting access to push my branch and create a PR?

@kvmw
Copy link
Contributor

kvmw commented Dec 6, 2023

@cobar79 I don't have admin access to the repo. You need to fork the repo and submit the PR from your fork.

@ryanjbaxter
Copy link
Contributor

You can configure encryption using symetric or asymmetric encryption
https://docs.spring.io/spring-cloud-config/docs/4.0.4/reference/html/#_key_management

Ultimately that shouldn't really matter I believe since the user will configure encryption however they want to encrypt the secret right?

As far as the PR goes, fork the repo, create a branch from the 4.0.x branch, then push that branch to your forked repo and create a PR from that branch against the 4.0.x branch in this repo on GitHub.

@cobar79
Copy link
Author

cobar79 commented Dec 7, 2023

@ryanjbaxter
Since the config server logic is way before spring boot sequence, there is no encryption bean created yet. So the properties loaded in the ConfigClientProperties were encrypted.

I can try the JCE again, but I had issues with the cipher in the properties. It failed with the cipher unquoted for yaml syntax and failed for "No decryption for FailsafeTextEncryptor. Did you configure the keystore correctly?" when quoted.

I went with Jayspt since it is the most common practice for encrypting properties in Spring applications.

PR

@ryanjbaxter ryanjbaxter linked a pull request Dec 11, 2023 that will close this issue
cobar79 pushed a commit to cobar79/spring-cloud-config that referenced this issue Dec 22, 2023
@DasAmpharos
Copy link

@ryanjbaxter do you have any idea on the timeline for this feature? My team is really interested in this feature and we are trying to determine if we should wait for official support or implement a custom solution.

@ryanjbaxter
Copy link
Contributor

It won't be for a while since we need to wait for a major release of Spring Cloud where we can introduce major changes like this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants