71

In spring-security-oauth2:2.4.0.RELEASE classes such as OAuth2RestTemplate, OAuth2ProtectedResourceDetails and ClientCredentialsAccessTokenProvider have all been marked as deprecated.

From the javadoc on these classes it points to a spring security migration guide that insinuates that people should migrate to the core spring-security 5 project. However I'm having trouble finding how I would implement my use case in this project.

All of the documentation and examples talk about integrating with a 3rd part OAuth provider if you want incoming requests to your application to be authenticated and you want to use the 3rd party OAuth provider to verify the identity.

In my use case all I want to do is make a request with a RestTemplate to an external service that is protected by OAuth. Currently I create an OAuth2ProtectedResourceDetails with my client id and secret which I pass into an OAuth2RestTemplate. I also have a custom ClientCredentialsAccessTokenProvider added to the OAuth2ResTemplate that just adds some extra headers to the token request that are required by the OAuth provider I'm using.

In the spring-security 5 documentation I've found a section that mentions customising the token request, but again that looks to be in the context of authenticating an incoming request with a 3rd party OAuth provider. It is not clear how you would use this in combination with something like a ClientHttpRequestInterceptor to ensure that each outgoing request to an external service first gets a token and then gets that added to the request.

Also in the migration guide linked above there is reference to a OAuth2AuthorizedClientService which it says is useful for using in interceptors, but again this looks like it relies on things like the ClientRegistrationRepository which seems to be where it maintains registrations for third party providers if you want to use that provide to ensure an incoming request is authenticated.

Is there any way I can make use of the new functionality in spring-security 5 for registering OAuth providers in order to get a token to add to outgoing requests from my application?

Matt Williams
  • 1,198
  • 1
  • 10
  • 27

5 Answers5

62

OAuth 2.0 Client features of Spring Security 5.2.x do not support RestTemplate, but only WebClient. See Spring Security Reference:

HTTP Client support

  • WebClient integration for Servlet Environments (for requesting protected resources)

In addition, RestTemplate will be deprecated in a future version. See RestTemplate javadoc:

NOTE: As of 5.0, the non-blocking, reactive org.springframework.web.reactive.client.WebClient offers a modern alternative to the RestTemplate with efficient support for both sync and async, as well as streaming scenarios. The RestTemplate will be deprecated in a future version and will not have major new features added going forward. See the WebClient section of the Spring Framework reference documentation for more details and example code.

Therefore, the best solution would be to abandon RestTemplate in favor of WebClient.


Using WebClient for Client Credentials Flow

Configure client registration and provider either programmatically or using Spring Boot auto-configuration:

spring:
  security:
    oauth2:
      client:
        registration:
          custom:
            client-id: clientId
            client-secret: clientSecret
            authorization-grant-type: client_credentials
        provider:
          custom:
            token-uri: http://localhost:8081/oauth/token

…​and the OAuth2AuthorizedClientManager @Bean:

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {

    OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                    .clientCredentials()
                    .build();

    DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientRepository);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    return authorizedClientManager;
}

Configure the WebClient instance to use ServerOAuth2AuthorizedClientExchangeFilterFunction with the provided OAuth2AuthorizedClientManager:

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
            new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    oauth2Client.setDefaultClientRegistrationId("custom");
    return WebClient.builder()
            .apply(oauth2Client.oauth2Configuration())
            .build();
}

Now, if you try to make a request using this WebClient instance, it will first request a token from the authorization server and include it in the request.

Anar Sultanov
  • 3,016
  • 2
  • 17
  • 27
  • 1
    That's deprecated now too lol... at least UnAuthenticatedServerOAuth2AuthorizedClientRepository is... – SledgeHammer Mar 13 '20 at 22:06
  • 3
    @AnarSultanov "Therefore, the best solution would be to abandon RestTemplate in favor of WebClient" What about places where this is not an option? For example, Spring Cloud Discovery, Configuration, and Feign clients still rely RestTemplate and documentation states to provide a custom RestTemplate if you plan to add security like OAuth to those services. – loesak Apr 04 '20 at 03:38
  • @AnarSultanov I've tried that exact example that you gave and I get a 401 error. It seems that it isn't authenticating while trying to perform requests. Any tips on that? – rafael.braga Apr 24 '20 at 01:08
  • @rafael.braga I cannot recommend anything without seeing all the code and configuration. You can try the example from the official repository and adapt it to your needs: https://github.com/spring-projects/spring-security/tree/master/samples/boot/oauth2webclient – Anar Sultanov Apr 24 '20 at 08:06
  • 1
    Here's the relevant Spring Security doco. Provides a bit more detail and explanation of the various ways you can configure WebClient: https://docs.spring.io/spring-security/site/docs/5.2.1.RELEASE/reference/htmlsingle/#oauth2Client-webclient-servlet – Crafton Jul 16 '21 at 00:33
  • Nice. When it comes to the OAuth2 client_credentials flow I would use AuthorizedClientServiceOAuth2AuthorizedClientManager instead of the DefaultOAuth2AuthorizedClientManager. The former is ' capable of operating outside of the context of a HttpServletRequest' which means it is not bound to the incoming request. – egelev Mar 14 '22 at 17:43
34

Hi maybe it's too late however RestTemplate is still supported in Spring Security 5, to non-reactive app RestTemplate is still used what you have to do is only configure spring security properly and create an interceptor as mentioned on migration guide

Use the following configuration to use client_credentials flow

application.yml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: ${okta.oauth2.issuer}/v1/keys
      client:
        registration:
          okta:
            client-id: ${okta.oauth2.clientId}
            client-secret: ${okta.oauth2.clientSecret}
            scope: "custom-scope"
            authorization-grant-type: client_credentials
            provider: okta
        provider:
          okta:
            authorization-uri: ${okta.oauth2.issuer}/v1/authorize
            token-uri: ${okta.oauth2.issuer}/v1/token

Configuration to OauthResTemplate

@Configuration
@RequiredArgsConstructor
public class OAuthRestTemplateConfig {

    public static final String OAUTH_WEBCLIENT = "OAUTH_WEBCLIENT";

    private final RestTemplateBuilder restTemplateBuilder;
    private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService;
    private final ClientRegistrationRepository clientRegistrationRepository;

    @Bean(OAUTH_WEBCLIENT)
    RestTemplate oAuthRestTemplate() {
        var clientRegistration = clientRegistrationRepository.findByRegistrationId(Constants.OKTA_AUTH_SERVER_ID);

        return restTemplateBuilder
                .additionalInterceptors(new OAuthClientCredentialsRestTemplateInterceptorConfig(authorizedClientManager(), clientRegistration))
                .setReadTimeout(Duration.ofSeconds(5))
                .setConnectTimeout(Duration.ofSeconds(1))
                .build();
    }

    @Bean
    OAuth2AuthorizedClientManager authorizedClientManager() {
        var authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
                .clientCredentials()
                .build();

        var authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, oAuth2AuthorizedClientService);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }

}

Interceptor

public class OAuthClientCredentialsRestTemplateInterceptor implements ClientHttpRequestInterceptor {

    private final OAuth2AuthorizedClientManager manager;
    private final Authentication principal;
    private final ClientRegistration clientRegistration;

    public OAuthClientCredentialsRestTemplateInterceptor(OAuth2AuthorizedClientManager manager, ClientRegistration clientRegistration) {
        this.manager = manager;
        this.clientRegistration = clientRegistration;
        this.principal = createPrincipal();
    }

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        OAuth2AuthorizeRequest oAuth2AuthorizeRequest = OAuth2AuthorizeRequest
                .withClientRegistrationId(clientRegistration.getRegistrationId())
                .principal(principal)
                .build();
        OAuth2AuthorizedClient client = manager.authorize(oAuth2AuthorizeRequest);
        if (isNull(client)) {
            throw new IllegalStateException("client credentials flow on " + clientRegistration.getRegistrationId() + " failed, client is null");
        }

        request.getHeaders().add(HttpHeaders.AUTHORIZATION, BEARER_PREFIX + client.getAccessToken().getTokenValue());
        return execution.execute(request, body);
    }

    private Authentication createPrincipal() {
        return new Authentication() {
            @Override
            public Collection<? extends GrantedAuthority> getAuthorities() {
                return Collections.emptySet();
            }

            @Override
            public Object getCredentials() {
                return null;
            }

            @Override
            public Object getDetails() {
                return null;
            }

            @Override
            public Object getPrincipal() {
                return this;
            }

            @Override
            public boolean isAuthenticated() {
                return false;
            }

            @Override
            public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
            }

            @Override
            public String getName() {
                return clientRegistration.getClientId();
            }
        };
    }
}

This will generate access_token in the first call and whenever the token is expired. OAuth2AuthorizedClientManager will manage all this to you

Leandro Assis
  • 349
  • 3
  • 5
  • I like this approach because it's consistent with Spring Security's `DefaultClientCredentialsTokenResponseClient` which uses `RestTemplate` internally. That means my project doesn't need a dependency on spring-webflux. – Nathan Jul 21 '22 at 02:38
  • 1
    best solution. Concise and works like a charm. Even the spring docs didn't provide such a clear how-to guide. – Walnussbär Aug 17 '22 at 10:04
  • 1
    Just a small note, based on the source code, it seems to me that `AuthorizedClientServiceOAuth2AuthorizedClientManager` is not strictly thread-safe. Not in a way that will crash the application, but multiple calls to fetch new tokens will be made if several requests are being handled concurrently and they all use a token that's about to expire/expired. – Alexis Sep 16 '22 at 07:25
  • @Alexis how we can avoid that? I mean not using the expired ones? – John Roshan Dec 22 '22 at 16:48
  • @JohnRoshan it will automatically update tokens when needed, it's just that in some edge cases it might make multiple updates in parallel, some of which would be redundant. To avoid that, you'd have to do the synchronization yourself. – Alexis Dec 22 '22 at 17:36
  • @Alexis I implemented regular spring boot OAuthRestTemplates to call Service B,C,D from service A. Service A takes heavy load and it’s facing 401 from service B,C,D intermittently. – John Roshan Dec 23 '22 at 18:47
7

I found @matt Williams answer quite helpful. Though I would like add in case someone would like to programatically pass clientId and secret for WebClient configuration. Here is how it can be Done.

 @Configuration
    public class WebClientConfig {

    public static final String TEST_REGISTRATION_ID = "test-client";

    @Bean
    public ReactiveClientRegistrationRepository clientRegistrationRepository() {
        var clientRegistration = ClientRegistration.withRegistrationId(TEST_REGISTRATION_ID)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .clientId("<client_id>")
                .clientSecret("<client_secret>")
                .tokenUri("<token_uri>")
                .build();
        return new InMemoryReactiveClientRegistrationRepository(clientRegistration);
    }

    @Bean
    public WebClient testWebClient(ReactiveClientRegistrationRepository clientRegistrationRepo) {

        var oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepo,  new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
        oauth.setDefaultClientRegistrationId(TEST_REGISTRATION_ID);

        return WebClient.builder()
                .baseUrl("https://.test.com")
                .filter(oauth)
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
    }
}
Jogger
  • 397
  • 5
  • 15
  • is there any sample code which can be tested for the above code snippet ? – Sagar Pilkhwal Jun 15 '20 at 14:41
  • @SagarPilkhwal You can create a simple spring-security based sample spring boot application (which you can easily find online). Set the client_credentials based access there and expose one test API. Then you can create WebClient using above code and try call that API. – Jogger Jun 15 '20 at 18:26
5

The above answer from @Anar Sultanov helped me get to this point, but as I had to add some additional headers to my OAuth token request I thought I would provide a full answer for how I solved the issue for my use case.

Configure provider details

Add the following to application.properties

spring.security.oauth2.client.registration.uaa.client-id=${CLIENT_ID:}
spring.security.oauth2.client.registration.uaa.client-secret=${CLIENT_SECRET:}
spring.security.oauth2.client.registration.uaa.scope=${SCOPE:}
spring.security.oauth2.client.registration.uaa.authorization-grant-type=client_credentials
spring.security.oauth2.client.provider.uaa.token-uri=${UAA_URL:}

Implement custom ReactiveOAuth2AccessTokenResponseClient

As this is server-to-server communication we need to use the ServerOAuth2AuthorizedClientExchangeFilterFunction. This only accepts a ReactiveOAuth2AuthorizedClientManager, not the non-reactive OAuth2AuthorizedClientManager. Therefore when we use ReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider() (to give it the provider to use to make the OAuth2 request) we have to give it a ReactiveOAuth2AuthorizedClientProvider instead of the non-reactive OAuth2AuthorizedClientProvider. As per the spring-security reference documentation if you use a non-reactive DefaultClientCredentialsTokenResponseClient you can use the .setRequestEntityConverter() method to alter the OAuth2 token request, but the reactive equivalent WebClientReactiveClientCredentialsTokenResponseClient does not provide this facility, so we have to implement our own (we can make use of the existing WebClientReactiveClientCredentialsTokenResponseClient logic).

My implementation was called UaaWebClientReactiveClientCredentialsTokenResponseClient (implementation omitted as it only very slightly alters the headers() and body() methods from the default WebClientReactiveClientCredentialsTokenResponseClient to add some extra headers/body fields, it does not change the underlying auth flow).

Configure WebClient

The ServerOAuth2AuthorizedClientExchangeFilterFunction.setClientCredentialsTokenResponseClient() method has been deprecated, so following the deprecation advice from that method:

Deprecated. Use ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientManager) instead. Create an instance of ClientCredentialsReactiveOAuth2AuthorizedClientProvider configured with a WebClientReactiveClientCredentialsTokenResponseClient (or a custom one) and than supply it to DefaultReactiveOAuth2AuthorizedClientManager.

This ends up with configuration looking something like:

@Bean("oAuth2WebClient")
public WebClient oauthFilteredWebClient(final ReactiveClientRegistrationRepository 
    clientRegistrationRepository)
{
    final ClientCredentialsReactiveOAuth2AuthorizedClientProvider
        clientCredentialsReactiveOAuth2AuthorizedClientProvider =
            new ClientCredentialsReactiveOAuth2AuthorizedClientProvider();
    clientCredentialsReactiveOAuth2AuthorizedClientProvider.setAccessTokenResponseClient(
        new UaaWebClientReactiveClientCredentialsTokenResponseClient());

    final DefaultReactiveOAuth2AuthorizedClientManager defaultReactiveOAuth2AuthorizedClientManager =
        new DefaultReactiveOAuth2AuthorizedClientManager(clientRegistrationRepository,
            new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
    defaultReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider(
        clientCredentialsReactiveOAuth2AuthorizedClientProvider);

    final ServerOAuth2AuthorizedClientExchangeFilterFunction oAuthFilter =
        new ServerOAuth2AuthorizedClientExchangeFilterFunction(defaultReactiveOAuth2AuthorizedClientManager);
    oAuthFilter.setDefaultClientRegistrationId("uaa");

    return WebClient.builder()
        .filter(oAuthFilter)
        .build();
}

Use WebClient as normal

The oAuth2WebClient bean is now ready to be used to access resources protected by our configured OAuth2 provider in the way you would make any other request using a WebClient.

Matt Williams
  • 1,198
  • 1
  • 10
  • 27
  • How to I pass a client-id, client-secret and an oauth endpoint programatically? – monti Dec 13 '19 at 06:42
  • 1
    I haven't tried this, but it looks like you could create instances of `ClientRegistration`s with the required details and pass those into the constructor for `InMemoryReactiveClientRegistrationRepository` (the default implementation of `ReactiveClientRegistrationRepository`). You then use that newly created`InMemoryReactiveClientRegistrationRepository` bean in place of my autowired `clientRegistrationRepository` that is passed into the `oauthFilteredWebClient` method – Matt Williams Dec 20 '19 at 08:25
  • Mh, but I'm not able to register different `ClientRegistration` at runtime, am I? As far as I understood I need to create a bean of `ClientRegistration` at startup. – monti Dec 20 '19 at 13:26
  • Ah ok, I thought you just wanted to not declare them in the `application.properties` file. Implementing your own `ReactiveOAuth2AccessTokenResponseClient` allows you to make whatever request you want to get an OAuth2 token, but I do not know how you could provide a dynamic "context' to it per request. The same goes for if you implemented your own entire filter. All this would give you access to is the outgoing request, so unless you could infer what you need from there I'm not sure what your options are. What's your use case? Why would you not know the possible registrations at startup? – Matt Williams Dec 20 '19 at 14:41
2

This is a simple alternative to OAuth2RestTemplate. The following snippet has been tested using Spring Boot 3.0.0-M4 and there is no application.yml configuration is needed.

SecurityConfig.java

    @Bean
    public ReactiveClientRegistrationRepository getRegistration() {
        ClientRegistration registration = ClientRegistration
                .withRegistrationId("custom")
                .tokenUri("<token_URI>")
                .clientId("<client_id>")
                .clientSecret("<secret>")
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .build();
        return new InMemoryReactiveClientRegistrationRepository(registration);
    }

    @Bean
    public WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations) {
        InMemoryReactiveOAuth2AuthorizedClientService clientService = new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrations);
        AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, clientService);
        ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        oauth.setDefaultClientRegistrationId("custom");
        return WebClient.builder()
                .filter(oauth)
                .filter(errorHandler()) // This is an optional
                .build();

    }

    public static ExchangeFilterFunction errorHandler() {
        return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {

            if (clientResponse.statusCode().is5xxServerError() || clientResponse.statusCode().is4xxClientError()) {
                return clientResponse.bodyToMono(String.class)
                        .flatMap(errorBody -> Mono.error(new IllegalAccessException(errorBody)));
            } else {
                return Mono.just(clientResponse);
            }
        });
    }

pom.xml

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.0-M4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</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-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-jose</artifactId>
        </dependency>
    <dependencies>