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.

Comment From: sjohnr

@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?

Comment From: yvasyliev

@sjohnr thanks for such a detailed explanation!

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

Comment From: yvasyliev

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);
    }
}

Comment From: xardbaiz

Thanks, @yvasyliev ! This code works excellent ! I decided to merge bests from your approach, mjeffrey's & spring OAuth2AuthorizedClientManager creation

For those who want to create a custom OAuth2AuthorizedClientManager and ClientHttpRequestInterceptor, without having the latest spring's OAuth2ClientHttpRequestInterceptor

Here is the split code ### 1. Define standard beans

@Configuration
@EnableConfigurationProperties(OAuth2ClientProperties.class)
public class OAuth2Config {

    @Bean
    @ConditionalOnMissingBean
    public ClientRegistrationRepository clientRegistrationRepository(
            OAuth2ClientProperties oAuth2ClientProperties) {
        var clientRegistrations =
                List.copyOf(
                        new OAuth2ClientPropertiesMapper(oAuth2ClientProperties)
                                .asClientRegistrations()
                                .values());
        return new InMemoryClientRegistrationRepository(clientRegistrations);
    }

    @Bean
    @ConditionalOnMissingBean
    public OAuth2AuthorizedClientService authorizedClientService(
            ClientRegistrationRepository clientRegistrationRepository) {
        return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
    }
}
### 2. Create a component for client authorization

@Component
@RequiredArgsConstructor
public class AuthorizationManagementComponent {
    @Getter
    private final ClientRegistrationRepository clientRegistrationRepository;
    private final OAuth2AuthorizedClientService authorizedClientService;
    private OAuth2AuthorizedClientManager authorizedClientManagerInstance;

    public OAuth2AuthorizedClient authorizeClient(
            ClientRegistration clientRegistration, Map<String, String> authAttrs) {

        String registrationId = clientRegistration.getRegistrationId();

        OAuth2AuthorizeRequest.Builder oAuth2AuthorizeRequestBuilder =
                OAuth2AuthorizeRequest.withClientRegistrationId(registrationId)
                        .principal(clientRegistration.getClientId());
        if (authAttrs != null && !authAttrs.isEmpty()) {
            oAuth2AuthorizeRequestBuilder.attributes(attrs -> attrs.putAll(authAttrs));
        }
        OAuth2AuthorizeRequest oAuth2AuthorizeRequest = oAuth2AuthorizeRequestBuilder.build();

        OAuth2AuthorizedClient authorizedClient =
                getAuthorizedClientManagerInstance().authorize(oAuth2AuthorizeRequest);
        Assert.notNull(
                authorizedClient,
                () ->
                        "Client authorization failed for oauth registration id: '"
                                + registrationId
                                + "', authorization result is null");
        return authorizedClient;
    }

    public OAuth2AuthorizedClientManager getAuthorizedClientManagerInstance() {
        if (authorizedClientManagerInstance == null) {
            authorizedClientManagerInstance = createAuthorizedClientManager();
        }
        return authorizedClientManagerInstance;
    }

    private OAuth2AuthorizedClientManager createAuthorizedClientManager() {
        return
                new AuthorizedClientServiceOAuth2AuthorizedClientManager(
                        clientRegistrationRepository, authorizedClientService);

    }
}
### 3. Create custom `ClientHttpRequestInterceptor`

@RequiredArgsConstructor
public class OAuth2ClientRequestInterceptor
        implements ClientHttpRequestInterceptor, ClientHttpRequestInitializer {

    private final AuthorizationManagementComponent authorizationManagement;
    private final ClientRegistration clientRegistration;
    private final Map<String, String> authAttrs;

    @Override
    public ClientHttpResponse intercept(
            HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        request.getHeaders().setBearerAuth(getBearerToken());
        return execution.execute(request, body);
    }

    @Override
    public void initialize(ClientHttpRequest request) {
        request.getHeaders().setBearerAuth(getBearerToken());
    }

    private String getBearerToken() {
        OAuth2AuthorizedClient authorizedClient =
                authorizationManagement.authorizeClient(clientRegistration, authAttrs);
        return authorizedClient.getAccessToken().getTokenValue();
    }
}
Done! Now you can use this interceptor in spring's `RestClient`, `RestTemplate` and `WebClient`. ### `RestClient` usage example:
private final AuthorizationManagementComponent authorizationManagement;

//...

@Bean
public RestClient restClient(RestClient.Builder clientBuilder) {
    return clientBuilder
            .requestInterceptor(createClientHttpRequestInterceptor())
            .build();
}

private ClientHttpRequestInterceptor createClientHttpRequestInterceptor() {
    ClientRegistrationRepository clientRegistrationRepository = authorizationManagement
            .getClientRegistrationRepository();
    ClientRegistration clientRegistration = clientRegistrationRepository
            .findByRegistrationId("<'X' from `spring.security.oauth2.client.registration.X`>");
    Map<String, String> oAuthBodyAttrs =
            Map.of(
                    OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "foobar",
                    OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "123");

    return new OAuth2ClientRequestInterceptor(
            authorizationManagement, clientRegistration, oAuthBodyAttrs);
}

Comment From: sjohnr

Thanks for being willing to provide that @xardbaiz but I think that is off-topic from this issue. I'm going to hide the comment so it doesn't interrupt the flow.

Comment From: AndreaLombardo

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.

Comment From: sjohnr

@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.

Comment From: Interessierter

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

Comment From: sjohnr

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"?

Comment From: philwebb

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

Comment From: dsyer

See also https://github.com/spring-projects/spring-boot/issues/43978 related to the same issue for resource servers

Comment From: sjohnr

@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.

Comment From: philwebb

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?

Comment From: sjohnr

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.