How do I make Spring Security 5 OAuth 2 use Apache Commons for RestTemplate

Viewed 1267

So I'm implementing SMART on FHIR for a System, which means client_credentials grant.

Here's how I've configured Spring Security OAuth

@Configuration
class OauthConfig {



    @Bean
    @Scope( BeanDefinition.SCOPE_PROTOTYPE )
    Authentication principalObjectFactory() {
        return SecurityContextHolder.getContext().getAuthentication();
    }

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

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

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

        return authorizedClientManager;
    }
}

I'm using an interceptor to try to get the token and make requests with the HAPI Fhir client.

@Configuration
class FhirConfig {

    private final FhirProperties fhir;
    private final OAuth2AuthorizedClientManager manager;

    FhirConfig( FhirProperties fhir, OAuth2AuthorizedClientManager manager ) {
        this.fhir = fhir;
        this.manager = manager;
    }

    @Bean
    HttpClientConnectionManager connectionManager() {
        return new PoolingHttpClientConnectionManager(
            5000,
            TimeUnit.MILLISECONDS
        );
    }

    @Bean
    HttpRequestInterceptor removeAcceptEncoding() {
        return ( request, ctx ) -> request.removeHeaders( "Accept-Encoding" );
    }

    private void disableGzipIfDebugging( HttpClientBuilder builder ) {
        if ( LogManager.getLogger( "org.apache.http.wire" ).isDebugEnabled() ) {
            builder.disableContentCompression().addInterceptorLast( removeAcceptEncoding() );
        }
    }

    @Bean
    CloseableHttpClient forRestTemplate() {
        HttpClientBuilder builder = HttpClients.custom()
            .setConnectionManager( connectionManager() )
            .disableCookieManagement();

        disableGzipIfDebugging( builder );
        return builder.build();
    }

    @Bean
    CloseableHttpClient forFhir(
        ObjectFactory<Authentication> authentication
    ) {

        HttpClientBuilder builder = HttpClients.custom()
            .setConnectionManager( connectionManager() )
            .disableCookieManagement();

        fhir.getOauth2().map( FhirProperties.Oauth2::getRegistration )
            .ifPresent( registration -> {
                builder.addInterceptorLast( new SmartOnFhirInterceptor( registration, authentication, manager ) );
            } );
        disableGzipIfDebugging( builder );
        return builder.build();
    }


    @Bean
    public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory( ) {
        HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
        clientHttpRequestFactory.setHttpClient(forRestTemplate());
        return clientHttpRequestFactory;
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplateBuilder()
            .requestFactory( this::clientHttpRequestFactory )
            .build();
    }

    @Bean
    @ConditionalOnProperty( name = "fhir.endpoint" )
    IGenericClient genericClient( CloseableHttpClient forFhir ) {
        IRestfulClientFactory rcf = new FhirContext( fhir.getVersion() ).getRestfulClientFactory();
        rcf.setHttpClient( forFhir );
        return rcf.newGenericClient( fhir.getEndpoint() );
    }

    static class SmartOnFhirInterceptor implements HttpRequestInterceptor {

        private final String registration;
        private final ObjectFactory<Authentication> authentication;
        private final OAuth2AuthorizedClientManager manager;
        private final AtomicBoolean isFirstRequest = new AtomicBoolean( true );

        SmartOnFhirInterceptor( String registration, ObjectFactory<Authentication> authentication, OAuth2AuthorizedClientManager manager ) {
            this.registration = Objects.requireNonNull( registration );
            this.authentication = Objects.requireNonNull( authentication );
            this.manager = Objects.requireNonNull( manager );
        }

        @Override
        public void process( HttpRequest req, HttpContext ctx ) {
            if ( ! isFirstRequest.getAndSet( false )) {
                OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId( registration )
                    .principal( authentication.getObject() )
                    .build();
                OAuth2AuthorizedClient oac = Objects.requireNonNull( this.manager.authorize( authorizeRequest ) );
                String token = oac.getAccessToken().getTokenValue();

                req.addHeader( "Authorization", "Bearer " + token );
            }
        }

    }
}

I have logging.level.org.apache.http.wire=debug set, and I see logs for the first request to the server for the metadata, which should be unauthenticated (and it's skipping it). The second request is failing with this, and I see no debug logs.

Caused by: org.springframework.http.converter.HttpMessageNotReadableException: An error occurred reading the OAuth 2.0 Error: Could not read JSON: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $; nested exception is com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $; nested exception is org.springframework.http.converter.HttpMessageNotReadableException: Could not read JSON: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $; nested exception is com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $
    at org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter.readInternal(OAuth2ErrorHttpMessageConverter.java:78)
    at org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter.readInternal(OAuth2ErrorHttpMessageConverter.java:46)
    at org.springframework.http.converter.AbstractHttpMessageConverter.read(AbstractHttpMessageConverter.java:199)
    at org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler.handleError(OAuth2ErrorResponseErrorHandler.java:59)
    at org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63)
    at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:785)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:743)
    at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:644)
    at org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient.getTokenResponse(DefaultClientCredentialsTokenResponseClient.java:75)
    at org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient.getTokenResponse(DefaultClientCredentialsTokenResponseClient.java:52)
    at org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider.authorize(ClientCredentialsOAuth2AuthorizedClientProvider.java:83)
    at org.springframework.security.oauth2.client.DelegatingOAuth2AuthorizedClientProvider.authorize(DelegatingOAuth2AuthorizedClientProvider.java:67)
    at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager.authorize(DefaultOAuth2AuthorizedClientManager.java:107)
    at com.potreromed.phg.emr.fhir.FhirConfig$SmartOnFhirInterceptor.process(FhirConfig.java:130)
    at org.apache.http.protocol.ImmutableHttpProcessor.process(ImmutableHttpProcessor.java:133)
    at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:184)
    at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89)
    at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110)
    at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185)
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83)
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108)
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:56)
    at ca.uhn.fhir.rest.client.apache.ApacheHttpRequest.execute(ApacheHttpRequest.java:64)
    at ca.uhn.fhir.rest.client.impl.BaseClient.invokeClient(BaseClient.java:304)
    at ca.uhn.fhir.rest.client.impl.GenericClient$BaseClientExecutable.invoke(GenericClient.java:431)
    at ca.uhn.fhir.rest.client.impl.GenericClient$SearchInternal.execute(GenericClient.java:1830)
    at com.potreromed.phg.emr.fhir.OAuthTest.lambda$client$0(OAuthTest.java:40)
    at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:55)
    ... 91 more
Caused by: org.springframework.http.converter.HttpMessageNotReadableException: Could not read JSON: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $; nested exception is com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $
    at org.springframework.http.converter.json.AbstractJsonHttpMessageConverter.readResolved(AbstractJsonHttpMessageConverter.java:112)
    at org.springframework.http.converter.json.AbstractJsonHttpMessageConverter.read(AbstractJsonHttpMessageConverter.java:94)
    at org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter.readInternal(OAuth2ErrorHttpMessageConverter.java:73)
    ... 118 more
Caused by: com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $
    at com.google.gson.Gson.fromJson(Gson.java:944)
    at com.google.gson.Gson.fromJson(Gson.java:897)
    at org.springframework.http.converter.json.GsonHttpMessageConverter.readInternal(GsonHttpMessageConverter.java:92)
    at org.springframework.http.converter.json.AbstractJsonHttpMessageConverter.readResolved(AbstractJsonHttpMessageConverter.java:109)
    ... 120 more
Caused by: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $
    at com.google.gson.stream.JsonReader.beginObject(JsonReader.java:386)
    at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:183)
    at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:145)
    at com.google.gson.Gson.fromJson(Gson.java:932)
    ... 123 more

This message isn't particularly useful, but I get the feeling that Spring 5 isn't using my RestTemplate instance using http commons. I found an older blog, probably for 2.x of the old library, for an OAuth2RestTemplate but that class seems gone.

How can I configure Spring Security 5.2 Oauth2 to use my RestTemplate so I can get the apache wire debug logs? alternatively.. is there another way to get the wire level debug logs from this?

0 Answers
Related