mkopylec / charon-spring-boot-starter

Reverse proxy implementation in form of a Spring Boot starter.
Apache License 2.0
240 stars 54 forks source link

Provide a custom instance of RestTemplate #101

Closed bjansen closed 4 years ago

bjansen commented 4 years ago

Hi,

I would like to provide my own instance of RestTemplate to a RequestMappingConfigurer. Specifically, I would like to use a org.springframework.security.oauth2.client.OAuth2RestTemplate instead of the default RetryAwareRestTemplate. I can't find a way to do that with the current version, because of private constructors and package-private methods in RestTemplateConfigurer.

Can you think of a way to achieve this?

mkopylec commented 4 years ago

Hi, I'm afraid it is not possible to replace the RestTemplate used by Charon. This is becuase it must be a special one which makes retrying request possible. The only things you can configure are timeouts, underlying http client and rest template interceptors, see here

What grant type you need to use the OAuth2RestTemplate with? If it is a stateless grant type, like client credentials grant type, you can create a custom Charon's interceptor (not the rest template one) and it's configurer. The interceptor would acquire an access token by sending a proper request to authorization server. Next, the interceptor will modify outgoing request headers by setting the 'Authorization' header which will include the access token.

bjansen commented 4 years ago

Right, I need client credentials. I thought about using a custom interceptor, but I was hoping I could use something already existing.

What can go wrong if the RestTemplate is not retry-aware? If I extended OAuth2RestTemplate to make my own RetryAwareOAuth2RestTemplate, would you consider adding a way to replace the default template?

mkopylec commented 4 years ago

Here is an example of how to create a RestTemplate interceptor, which can acquire access tokens. Maybe you can create a similar one and set it via Charon's configration API.

As for the RetryAwareOAuth2RestTemplate you can't extend RetryAwareRestTemplate and OAuth2RestTemplate at the same time. In Charon's confgiuration API I can only expose a setter for RetryAwareRestTemplate. I can't expose setter for RestTemplate because then you could set a RestTemplate that is not aware of retrying.

mkopylec commented 4 years ago

BTW OAuth2RestTemplate is deprecated now

bjansen commented 4 years ago

Yes, I've seen that the class is deprecated, I will consider switching to WebFlux instead.

Thanks for the answers and the examples!

mkopylec commented 4 years ago

I've analyzed the code a bit and find a way to actually allow to set a custom RestTemplate :) Thus I must use reflection in the code. If you are still interested let me know.

bjansen commented 4 years ago

If it feels like a hack for you, then maybe it's better if I use an interceptor.

On the other hand, if it integrates nicely with the existing configuration DSL, and if you can release this feature "soon enough", then yes I'm definitely interested :)

mkopylec commented 4 years ago

I think I can implement this, it will nicely integrate with the DSL. Give me up to two days to release it.

bjansen commented 4 years ago

Thanks a lot!

mkopylec commented 4 years ago

Sorry, but I can't implement it like thought. I can't provide a similar functionality to webflux module (WebClient has too restricted API) and I want both modules to provide analogical features. Please try to create a custom interceptor.

bjansen commented 4 years ago

All right. For the record, here is the interceptor I made, in case someone else needs it:

import com.github.mkopylec.charon.forwarding.interceptors.RequestForwardingInterceptorConfigurer;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;

public class OAuth2HeaderConfigurer extends RequestForwardingInterceptorConfigurer<OAuth2Header> {

    private OAuth2HeaderConfigurer() {
        super(new OAuth2Header());
    }

    public static OAuth2HeaderConfigurer oauth2Header() {
        return new OAuth2HeaderConfigurer();
    }

    public OAuth2HeaderConfigurer clientCredentials(String tokenUrl, String clientId, String clientSecret) {
        ClientCredentialsResourceDetails resourceDetails = new ClientCredentialsResourceDetails();

        resourceDetails.setAccessTokenUri(tokenUrl);
        resourceDetails.setClientId(clientId);
        resourceDetails.setClientSecret(clientSecret);

        configuredObject.setResourceDetails(resourceDetails);

        return this;
    }
}
import com.github.mkopylec.charon.forwarding.interceptors.HttpRequest;
import com.github.mkopylec.charon.forwarding.interceptors.HttpRequestExecution;
import com.github.mkopylec.charon.forwarding.interceptors.HttpResponse;
import com.github.mkopylec.charon.forwarding.interceptors.RequestForwardingInterceptor;
import com.github.mkopylec.charon.forwarding.interceptors.RequestForwardingInterceptorType;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.DefaultOAuth2RequestAuthenticator;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.http.AccessTokenRequiredException;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.util.StringUtils;

import static org.springframework.util.Assert.notNull;

public class OAuth2Header implements RequestForwardingInterceptor {

    private OAuth2RestTemplate restTemplate;

    @Override
    public HttpResponse forward(HttpRequest request, HttpRequestExecution execution) {
        OAuth2AccessToken accessToken = restTemplate.getAccessToken();

        authenticate(request);

        return execution.execute(request);
    }

    /**
     * Copied from {@link DefaultOAuth2RequestAuthenticator#authenticate(org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails, org.springframework.security.oauth2.client.OAuth2ClientContext, org.springframework.http.client.ClientHttpRequest)}
     * and adapted to support {@link HttpRequest} instead of {@link ClientHttpRequest}.
     */
    private void authenticate(HttpRequest request) {
        OAuth2ClientContext clientContext = restTemplate.getOAuth2ClientContext();
        OAuth2AccessToken accessToken = clientContext.getAccessToken();
        if (accessToken == null) {
            throw new AccessTokenRequiredException(restTemplate.getResource());
        }
        String tokenType = accessToken.getTokenType();
        if (!StringUtils.hasText(tokenType)) {
            tokenType = OAuth2AccessToken.BEARER_TYPE; // we'll assume basic bearer token type if none is specified.
        } else if (tokenType.equalsIgnoreCase(OAuth2AccessToken.BEARER_TYPE)) {
            // gh-1346
            tokenType = OAuth2AccessToken.BEARER_TYPE; // Ensure we use the correct syntax for the "Bearer" authentication scheme
        }
        request.getHeaders().set("Authorization", String.format("%s %s", tokenType, accessToken.getValue()));
    }

    @Override
    public RequestForwardingInterceptorType getType() {
        return new RequestForwardingInterceptorType(550);
    }

    public void setResourceDetails(OAuth2ProtectedResourceDetails resourceDetails) {
        restTemplate = new OAuth2RestTemplate(resourceDetails, new DefaultOAuth2ClientContext());
    }

    @Override
    public void validate() {
        notNull(restTemplate, "No OAuth2ProtectedResourceDetails set");
    }
}
mkopylec commented 4 years ago

Thanks for showing the code example to others :)