Search code examples
spring-securityoauth-2.0

How to customize the Authorization header of the OAuth2 token request


I am using Spring Security 5.5 to perform an access token request and recently upgraded to 5.5.1, and now my client secret is rejected by my OAuth 2.0 provider. This was caused by a bug fix to URL encode client credentials per RFC 6749 Section 2.3.1.

Since my OAuth 2.0 provider is non-compliant, I would like to revert to the old behavior in Spring Security 5.5.0, and send my client credentials without URL encoding.

From the reference documentation, if I define a @Bean of type OAuth2AuthorizedClientManager:

    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager(
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientRepository authorizedClientRepository) {
        // @formatter:off
        OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                .clientCredentials()
                .build();

        DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(
                clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
        // @formatter:on

        return authorizedClientManager;
    }

How do I configure it to use a custom converter to set the credentials?

Note: Relates to this question but addresses Servlet support instead of Reactive support with WebClient.


Solution

  • Suppose you have the following configuration:

    spring:
      security:
        oauth2:
          client:
            registration:
              test-client:
                provider: spring
                client-id: aladdin
                client-secret: "open sesame"
                authorization-grant-type: client_credentials
                redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
                scope: resource:read
            provider:
              spring:
                authorization-uri: http://auth-server:9000/oauth/authorize
                token-uri: http://auth-server:9000/oauth/token
    

    In the example from the OP, the OAuth2AuthorizedClientManager supports client_credentials grant to make an access token request. This would be useful for ex. if we want to implement the following (fictional) endpoint:

    @RestController
    public class TokenController {
        @GetMapping("/token")
        public OAuth2AccessToken token(@RegisteredOAuth2AuthorizedClient("test-client") OAuth2AuthorizedClient testClient) {
            return testClient.getAccessToken();
        }
    }
    

    The OAuth2AuthorizedClientManager provides the OAuth2AuthorizedClient that is injected in this example. It can be configured with a custom converter as follows:

    Note: The following is an expanded example adapted from the Client Credentials section of the reference documentation.

        @Bean
        public OAuth2AuthorizedClientManager authorizedClientManager(
                ClientRegistrationRepository clientRegistrationRepository,
                OAuth2AuthorizedClientRepository authorizedClientRepository) {
            // @formatter:off
            OAuth2AuthorizedClientProvider authorizedClientProvider =
                OAuth2AuthorizedClientProviderBuilder.builder()
                    .clientCredentials((builder) ->
                        builder.accessTokenResponseClient(clientCredentialsAccessTokenResponseClient())
                            .build())
                    .build();
    
            DefaultOAuth2AuthorizedClientManager authorizedClientManager =
                new DefaultOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientRepository);
            authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
            // @formatter:on
    
            return authorizedClientManager;
        }
    
        @Bean
        public OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
            // @formatter:off
            OAuth2ClientCredentialsGrantRequestEntityConverter requestEntityConverter =
                new OAuth2ClientCredentialsGrantRequestEntityConverter();
            requestEntityConverter.setHeadersConverter(headersConverter());
    
            DefaultClientCredentialsTokenResponseClient accessTokenResponseClient =
                new DefaultClientCredentialsTokenResponseClient();
            accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter);
            // @formatter:on
    
            return accessTokenResponseClient;
        }
    
        private static <T extends AbstractOAuth2AuthorizationGrantRequest> Converter<T, HttpHeaders> headersConverter() {
            // @formatter:off
            Converter<T, ClientRegistration> clientRegistrationConverter =
                AbstractOAuth2AuthorizationGrantRequest::getClientRegistration;
            return clientRegistrationConverter
                .andThen((clientRegistration) -> {
                    HttpHeaders headers = new HttpHeaders();
                    headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
                    headers.setContentType(MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
                    headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
                    return headers;
                });
            // @formatter:on
        }
    

    If we wanted to support the authorization_code grant for logging into the application, we could use the following configuration (with the above methods and @Bean definitions contained within):

    @Configuration
    @EnableWebSecurity
    public class SecurityConfiguration {
    
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            // @formatter:off
            http
                .authorizeRequests(authorizeRequests -> authorizeRequests
                    .anyRequest().authenticated())
                .oauth2Login(oauth2Login -> oauth2Login
                    .tokenEndpoint(tokenEndpoint -> tokenEndpoint
                        .accessTokenResponseClient(authorizationCodeAccessTokenResponseClient())));
            // @formatter:on
    
            return http.build();        
        }
    
        @Bean
        public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeAccessTokenResponseClient() {
            // @formatter:off
            OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter =
                new OAuth2AuthorizationCodeGrantRequestEntityConverter();
            requestEntityConverter.setHeadersConverter(headersConverter());
    
            DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
                new DefaultAuthorizationCodeTokenResponseClient();
            accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter);
            // @formatter:on
    
            return accessTokenResponseClient;
        }
    
        // ...
    
    }