Skip to content

Add OAuth2AuthorizedClientManager autoconfiguration without spring-boot-starter-web dependency #15877

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

Open
yvasyliev opened this issue Oct 4, 2024 · 14 comments
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) type: enhancement A general enhancement

Comments

@yvasyliev
Copy link

yvasyliev commented Oct 4, 2024

Expected Behavior

I would like org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager to be an autoconfigured bean based on application.yml properties, and without having spring-boot-starter-web dependency.

My desirable state would be the following:

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.0-M3</version>
    </parent>

    <groupId>io.github.yvasyliev.oauth2</groupId>
    <artifactId>springboot-oauth2-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-json</artifactId>
        </dependency>
    </dependencies>
</project>

application.yml

spring:
  security:
    oauth2:
      client:
        registration:
          auth-1:
            client-id: client-id
            client-secret: client-secret
            authorization-grant-type: client_credentials
        provider:
          auth-1:
            token-uri: https://auth-1/api/v1/token

MyServiceConfig.java

@Configuration
public class MyServiceConfig {
    @Bean
    public MyService myService(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager) {
        var oAuth2ClientHttpRequestInterceptor = new OAuth2ClientHttpRequestInterceptor(
                authorizedClientManager,
                request -> "auth-1"
        );
        var restClient = RestClient.builder()
                .baseUrl("https://api.service-1.com")
                .requestInterceptor(oAuth2ClientHttpRequestInterceptor)
                .build();
        var restClientAdapter = RestClientAdapter.create(restClient);
        var httpServiceProxyFactory = HttpServiceProxyFactory.builderFor(restClientAdapter).build();
        return httpServiceProxyFactory.createClient(MyService.class);
    }
}

I would expect oAuth2AuthorizedClientManager to be an instance of org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager, because it exists outside the servlet context.

Current Behavior

The application above fails to start:

Console output

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of method myService in io.github.yvasyliev.oauth2.config.MyServiceConfig required a bean of type 'org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager' that could not be found.


Action:

Consider defining a bean of type 'org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager' in your configuration.


Process finished with exit code 1

If I add spring-boot-starter-web dependency to the project, the oAuth2AuthorizedClientManager bean will be automatically created:

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.0-M3</version>
    </parent>

    <groupId>io.github.yvasyliev.oauth2</groupId>
    <artifactId>springboot-oauth2-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>

But at the same time I'm having:

  1. A web server up and running.
  2. oAuth2AuthorizedClientManager is instance of org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager.
  3. spring.main.web-application=none property will disable server startup and OAuth2AuthorizedClientManager autoconfiguration.

Context

I'm building a Spring Boot (not web!) application that communicates with external REST services. I want to utilize HTTP Interface based on RestClient with OAuth interceptor. And I don't really want to add spring-boot-starter-web to my project, because it includes HTTP server that I won't use.

It would be awesome if OAuth2AuthorizedClientManager bean was automatically created in case of spring.security.oauth2.client.* properties existence in application.yml just like spring-boot-starter-web does.

I can achieve the desired outcome by manual OAuth2AuthorizedClientManager configuration:

MyServiceConfig.java

@Configuration
public class MyServiceConfig {
    @Bean
    public MyService myService(
            @Value("${spring.security.oauth2.client.registration.auth-1.client-id}") String clientId,
            @Value("${spring.security.oauth2.client.registration.auth-1.client-secret}") String clientSecret,
            @Value("${spring.security.oauth2.client.registration.auth-1.authorization-grant-type}") AuthorizationGrantType authorizationGrantType,
            @Value("${spring.security.oauth2.client.provider.auth-1.token-uri}") String tokenUri) {
        var clientRegistration = ClientRegistration.withRegistrationId("auth-1")
                .clientId(clientId)
                .clientSecret(clientSecret)
                .authorizationGrantType(authorizationGrantType)
                .tokenUri(tokenUri)
                .build();
        var clientRegistrationRepository = new InMemoryClientRegistrationRepository(clientRegistration);
        var authorizedClientService = new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
        var authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(
                clientRegistrationRepository,
                authorizedClientService
        );
        var oAuth2ClientHttpRequestInterceptor = new OAuth2ClientHttpRequestInterceptor(
                authorizedClientManager,
                request -> "auth-1"
        );
        var restClient = RestClient.builder()
                .baseUrl("https://api.service-1.com")
                .requestInterceptor(oAuth2ClientHttpRequestInterceptor)
                .build();
        var restClientAdapter = RestClientAdapter.create(restClient);
        var httpServiceProxyFactory = HttpServiceProxyFactory.builderFor(restClientAdapter).build();
        return httpServiceProxyFactory.createClient(MyService.class);
    }
}

But there's too much boilerplate code.

@yvasyliev yvasyliev added status: waiting-for-triage An issue we've not yet triaged type: enhancement A general enhancement labels Oct 4, 2024
@sjohnr sjohnr self-assigned this Oct 4, 2024
@sjohnr sjohnr added in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) and removed status: waiting-for-triage An issue we've not yet triaged labels Oct 4, 2024
@sjohnr
Copy link
Contributor

sjohnr commented Oct 4, 2024

@yvasyliev thanks for reaching out!

I think there might be some overlapping concepts regarding Spring Boot outlined in this issue, that should be clarified before we can discuss your use case.

  1. Spring Boot is a separate project from Spring Security, and actually depends on Spring Security.
  2. Auto-configuration is a feature of Spring Boot and cannot be used in Spring Security.
  3. Spring Boot starters are also a feature of Spring Boot and not part of Spring Security.

Because of the above, some of what you ask in the issue isn't quite accurate in context. Also, I don't think this request could simply be moved to Spring Boot because much of what you're asking for here is still specific to Spring Security, but would not be implemented the way you mention above (using Spring Boot features).

Regarding your use case:

I would like org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager to be an autoconfigured bean based on application.yml properties, and without having spring-boot-starter-web dependency.

When Spring Security automatically configures an OAuth2AuthorizedClientManager, this is not part of Spring Boot's auto-configuration but performed by a BeanDefinitionRegistryPostProcessor in Spring Security that registers the bean. This post-processor is registered whenever spring-security-oauth2-client is on the classpath. Note that this is a Spring Security jar, not the Spring Boot starter.

It would be awesome if OAuth2AuthorizedClientManager bean was automatically created in case of spring.security.oauth2.client.* properties existence in application.yml just like spring-boot-starter-web does.

The presence of spring-boot-starter-web does cause Spring Security to be configured by default. However, it's not strictly related to OAuth2 Client, though I understand why it appears that way since the interactions between various components being switched on and configured with Spring Boot seems a bit magical.

Spring Security itself is only automatically set up in Spring Boot web applications (e.g. when spring-boot-starter-web is present). As you have noticed, nothing will initialize OAuth2 Client features for a non-web application and even Spring Security itself is not set up in that case since there would be nothing to protect by default. So when you use spring-boot-starter by itself, you are responsible for setting up OAuth2 Client.

But there's too much boilerplate code.

I'm sorry you feel that it is too much boilerplate code. However, I think the bean configuration in your example is fairly reasonable and minimal given that what you're requesting isn't supported out of the box.


Considering that the above is context for how things work now, what I think this request ends up asking is whether Spring Security can provide some kind of feature for initializing a non-web application with OAuth2 Client features, specifically using client_credentials with the AuthorizedClientServiceOAuth2AuthorizedClientManager.

This is an interesting request and could be a compelling use case. For requests like this, we typically want to see how many users in the community are asking for this before deciding to tackle it. We do that by tracking upvotes on open issues over time and if quite a lot of community interest is demonstrated, we would decide to prioritize it at that point.

Make sense?

@sjohnr sjohnr added the status: waiting-for-feedback We need additional information before we can continue label Oct 4, 2024
@yvasyliev
Copy link
Author

@sjohnr thanks for such a detailed explanation!

I 100% agree. Let's see if anyone else needs this feature. 😊

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Oct 5, 2024
@sjohnr sjohnr removed the status: feedback-provided Feedback has been provided label Oct 15, 2024
@sjohnr sjohnr removed their assignment Oct 15, 2024
@yvasyliev
Copy link
Author

Just in case if anyone is looking into this topic, I found a more concise configuration approach:

@Configuration
@EnableConfigurationProperties(OAuth2ClientProperties.class)
public class MyServiceConfig {
    @Bean
    public MyService myService(OAuth2ClientProperties oAuth2ClientProperties) {
        var clientRegistrations = List.copyOf(new OAuth2ClientPropertiesMapper(oAuth2ClientProperties)
                .asClientRegistrations()
                .values()
        );
        var clientRegistrationRepository = new InMemoryClientRegistrationRepository(clientRegistrations);
        var authorizedClientService = new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
        var authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(
                clientRegistrationRepository,
                authorizedClientService
        );
        var oAuth2ClientHttpRequestInterceptor = new OAuth2ClientHttpRequestInterceptor(
                authorizedClientManager,
                request -> "auth-1"
        );
        var restClient = RestClient.builder()
                .baseUrl("https://api.service-1.com")
                .requestInterceptor(oAuth2ClientHttpRequestInterceptor)
                .build();
        var restClientAdapter = RestClientAdapter.create(restClient);
        var httpServiceProxyFactory = HttpServiceProxyFactory.builderFor(restClientAdapter).build();
        return httpServiceProxyFactory.createClient(MyService.class);
    }
}

@xardbaiz

This comment was marked as off-topic.

@sjohnr

This comment was marked as off-topic.

@AndreaLombardo
Copy link

I’m experiencing a similar issue with a Feign client.

As mentioned in the Spring Cloud OpenFeign documentation, the solution is to enable the flag spring.cloud.openfeign.oauth2.enabled=true in the application.yml or application.properties. However, when running the application, I see the following log message in the console (debug mode enabled):

FeignAutoConfiguration.Oauth2FeignConfiguration#defaultOAuth2AccessTokenInterceptor:
      Did not match:
         - @ConditionalOnBean (types: org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; SearchStrategy: all) did not find any beans of type org.springfram

It seems no interceptor is being invoked during Feign client requests. Additionally, my application does not and should not have any dependencies related to spring-boot-starter-web. Any tips? Thanks.

@sjohnr
Copy link
Contributor

sjohnr commented Nov 21, 2024

@AndreaLombardo I think you will need to reach out on the spring cloud project’s issue tracker if you believe it’s a bug. Spring Security does not contribute to auto-configuration since it is not built on Spring Boot.

@Interessierter
Copy link

I'm having the exact same usecase as @yvasyliev and was scratching my head a while before finding this issue which finally made things clear thanks to @sjohnr's good and detailed explanation, thanks a lot to both!

I would be very interested in the new feature as @sjohnr suggested. Thinking about it maybe it could be done a little bit broader because I also copied the following code to some of my web applications:

    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository,
                                                                 OAuth2AuthorizedClientService oAuth2AuthorizedClientService) {
        /*
         * note to self: we are creating this bean with AuthorizedClientServiceOAuth2AuthorizedClientManager because it allows
         * self-contained auth without a pre-existing http session, by default a DefaultOAuth2AuthorizedClientManager is
         * provided by spring security when asked to inject a OAuth2AuthorizedClientManager which requires a http request as input
         * (i.e. it must be called via web) which is not suitable for schedules and so on
         */
        OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
                .refreshToken()
                .clientCredentials()
                .authorizationCode()
                .build();
        AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
                new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, oAuth2AuthorizedClientService);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }

The use case would be / is "I want to make requests to an oauth2-protected external resource using my applications credentials (i.e. using the client_credentials flow), regardless if I am running in and web application or not". I may be mistaken but this isn't a very rare usecase, or is it? It would be very neat if there is a simple API in spring-security for this (IHMO a RequestInterceptor like OAuth2ClientHttpRequestInterceptor which just takes a clientRegistrationId and does setup all other required things would be good), the current API requires some "boilerplate" to make this work.

For people also stumbling over this here some details to the given explanation above (why it isnt working when not an web-app, it indeed seems quite magical to anyone who never looked into this):

  • spring-boot autoconfigures some required things in org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration (the imported configurations)
  • spring-security sets up the default OAuth2AuthorizedClientManager in org.springframework.security.config.annotation.web.configuration.OAuth2ClientConfiguration.OAuth2AuthorizedClientManagerRegistrar

@sjohnr
Copy link
Contributor

sjohnr commented Jan 27, 2025

For reference (similar to above comments), here is the most minimal configuration that I am aware of for configuring OAuth2 Client to obtain a client_credentials access token in a non-web setup. It provides the necessary beans for this use case, similar to those provided by Spring Boot + Spring Security out of the box with a web application.

Besides spring-boot-starter-oauth2-client, the application needs the com.fasterxml.jackson.core:jackson-databind dependency in order to use JSON deserialization for the Access Token Request.

It seems plausible for something like this to be provided, possibly as a separate Spring Boot starter, with some auto-configuration for this specific scenario. However, I'm not sure what @Conditional could be used to detect that special auto-configuration should be activated.

@philwebb Does Spring Boot have any scenarios for auto-configuration that's activated when an application is "not a web app"?

@philwebb
Copy link
Member

@sjohnr We have @@ConditionalOnNotWebApplication but it looks like FreeMarkerNonWebConfiguration is the only configuration that uses it.

@dsyer
Copy link
Member

dsyer commented Jan 27, 2025

See also spring-projects/spring-boot#43978 related to the same issue for resource servers

@sjohnr
Copy link
Contributor

sjohnr commented Jan 28, 2025

@sjohnr We have @@ConditionalOnNotWebApplication but it looks like FreeMarkerNonWebConfiguration is the only configuration that uses it.

Interesting, thanks!

(Thinking out loud) So I suppose a spring-boot-starter-oauth2-client + spring-boot-starter-json setup would already be possible, although it requires knowing that you need json support. I wonder if there's an intuitive name for a Spring Boot starter that combines these two things? Or if Spring Initializr can simply add the json starter when there is no web or webflux dependency? Though that might not be easy to get right even if it's possible.

@philwebb
Copy link
Member

Perhaps we can add a dependency in spring-boot-starter-oauth2-client to spring-boot-starter-json since JSON is needed most of the time?

@sjohnr
Copy link
Contributor

sjohnr commented Jan 28, 2025

Actually, that might make sense. Typically, the json support that comes with web or webflux is enough, but since OAuth2 Client is built around making OAuth 2.0 Access Token Requests which return JSON, we do need json support pretty much all the time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

8 participants