Search code examples
springspring-bootspring-securityspring-security-oauth2

Deprecate spring-security-oauth as a client


I want to move from spring-security-oauth to spring-security now, but I cannot find any way of doing so. I searched a lot and all I could find are examples about providing an OAuth Endpoint.

My current OAuth2RestTemplate is a bit complicated, as the oauth server is not using the standard way for identification, inspired by the post here.

This is my OAuth2RestTemplate:

fun createOAuthRestTemplate(resourceDetails: OAuth2ProtectedResourceDetails): OAuth2RestTemplate {

    val clientCredentialsAccessTokenProvider = ClientCredentialsAccessTokenProvider()
    clientCredentialsAccessTokenProvider.setAuthenticationHandler(<myClientAuthenticationHandler extends ClientAuthenticationHandler>))
    clientCredentialsAccessTokenProvider.setRequestFactory(requestFactory)

    val oauthTemplate = OAuth2RestTemplate(resourceDetails)
    oauthTemplate.setAccessTokenProvider(clientCredentialsAccessTokenProvider)
    return oauthTemplate
  }

The spring migration guide unfortunately didn't help me much as it just mentions the RestTemplate, but doesn't go into detail.

[...] 
A Simplified RestTemplate and WebClient
Spring Security OAuth extends RestTemplate, introducing OAuth2RestTemplate. 
This class needs to be instantiated and exposed as a @Bean.

Spring Security chooses to favor composition and instead exposes an 
OAuth2AuthorizedClientService, which is useful for creating RestTemplate interceptors 
[...]

My question is now:
How do I get the same functionality in a rest template using spring-security?


Solution

  • OAuth 2.0 with Spring Security 5 and RestTemplate

    Spring Security 5.2 does not have direct support for RestTemplate, it has beans that simplify the work, though. The recommendation is to use WebClient, if you can, instead of RestTemplate.

    However, if you need to use RestTemplate, then you first want to create an OAuth2AuthorizedClientManager:

    @Bean
    OAuth2AuthorizedClientManager authorizeClientManager(
        ClientRegistrationRepository clients,
        OAuth2AuthorizedClientRepository authorizedClients) {
    
        DefaultOAuth2AuthorizedClientManager manager =
            new DefaultOAuth2AuthorizedClientManager(clients, authorizedClients);
    
        OAuth2AuthorizedClientProvider authorizedClientProvider =
                OAuth2AuthorizedClientProviderBuilder.builder()
                    .clientCredentials()
                    .build();
        manager.setAuthorizedClientProvider(authorizedClientProvider);
        return manager;
    }
    

    This manager bean is where you make decisions on the kinds of grant types that you want your interceptor to negotiate for you. It is similar to the ClientCredentialsAccessTokenProvider in your post.

    Second, you'll want to create a RestTemplate interceptor. It will ask the manager to get it a token and then add that token to the Authorization header:

    @Component
    public class OAuth2AuthorizedClientInterceptor implements ClientHttpRequestInterceptor {
        OAuth2AuthorizedClientManager manager;
    
        public OAuth2AuthorizedClientInterceptor(OAuth2AuthorizedClientManager manager) {
            this.manager = manager;
        }
    
        public ClientHttpResponse intercept(
            HttpRequest request, byte[] body, ClientHttpRequestExecution execution) 
            throws IOException {
    
            Authentication principal = // ...
    
            OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
                 .withClientRegistrationId("foo-client")
                 .principal(principal)
                 .build();
            OAuth2AuthorizedClient authorizedClient = 
                this.manager.authorize(authorizedRequest);
    
            HttpHeaders headers = httpRequest.getHeaders();
            headers.setBearerAuth(authorizedClient.getAccessToken().getValue());
    
            return execution.execute(request, body);
        }
    }
    

    In Spring Security 5, each client is represented by a registration id. The registration id is what goes in place of foo-client.

    The principal will depend a bit on your situation, but it is typically sufficient to use SecurityContextHolder.getContext().getAuthentication(). If there is no user in context, you might consider AnonymousAuthenticationToken, e.g.

    Authentication principal = new AnonymousAuthenticationToken
        ("key", "anonymous", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
    

    Finally, you can add your interceptor to your RestTemplate:

    @Bean
    public RestTemplate rest(OAuth2AuthorizedClientInterceptor interceptor) {
        RestTemplate rest = new RestTemplate();
        rest.getInterceptors().add(interceptor);
        return rest;
    }
    

    JWT Client Authentication

    As for the post you referred to, it is doing a JWT for client authentication. For this support, you'll want to look at configuring a DefaultClientCredentialsTokenResponseClient. This would be part of the manager that you built in the first step:

    @Bean
    OAuth2AuthorizedClientManager authorizeClientManager(
        ClientRegistrationRepository clients,
        OAuth2AuthorizedClientRepository authorizedClients) {
    
        DefaultOAuth2AuthorizedClientManager manager =
            new DefaultOAuth2AuthorizedClientManager(clients, authorizedClients);
    
        DefaultClientCredentialsTokenResponseClient tokenClient =
                new DefaultClientCredentialsTokenResponseClient();
        tokenClient.setRequestEntityConverter(fooConverter);
    
        OAuth2AuthorizedClientProvider authorizedClientProvider =
                OAuth2AuthorizedClientProviderBuilder.builder()
                    .clientCredentials(cc -> cc
                            .accessTokenResponseClient(tokenClient))
                    .build();
        manager.setAuthorizedClientProvider(authorizedClientProvider);
        return manager;
    }
    

    fooConverter is any converter that you specify for creating the appropriate RequestEntity instance. Something like:

    // ...
    tokenClient.setRequestEntityConverter(grantRequest -> {
        ClientRegistration client = grantRequest.getClientRegistration();
        // ... formulate JWT
        // ... create `RequestEntity`, including `Authorization` header
        // that includes JWT as the bearer token
    });
    

    This setter, setRequestEntityConverter is the functional equivalent of ClientAuthenticationHandler in the legacy project.