Search code examples
springspring-bootauthenticationbasic-authenticationresttemplate

Spring Boot: "relaying" basic auth from REST controller to RestTemplate


I'm working with two Spring Boot applications, let's call them ServiceA and ServiceB, both exposing a REST API.

ServiceA is called by end users from the browser via a frontend app (we use @RestController classes). On some calls, ServiceA has to call ServiceB (using RestTemplate). We've got authentication and authorization sorted out for our target environment, but for testing locally we are relying on Basic Auth instead, and that's where we're hitting a snag: we would like ServiceA to re-use the Basic Auth credentials the user provided when calling Service B.

Is there an easy way to pass the Basic Auth credentials used on the call to our REST controller to the RestTemplate call?


Solution

  • Quick and dirty solution

    The easiest way to do this would be:

    import org.springframework.http.HttpEntity;
    import org.springframework.http.HttpHeaders;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestHeader;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.client.RestTemplate;
    
    @RestController
    class SomeController {
        private final RestTemplate restTemplate = new RestTemplate();
    
        @PostMapping("/delegate/call")
        public void callOtherService(@RequestHeader(HttpHeaders.AUTHORIZATION) String authorization) {
            HttpHeaders headers = new HttpHeaders();
            headers.add(HttpHeaders.AUTHORIZATION, authorization);
            
            restTemplate.postForEntity("other-service.com/actual/call", new HttpEntity<Void>(null, headers), Void.class);
            // handling the response etc...
        }
    }
    

    Using interceptors and RestTemplateCustomizer

    I didn't want to change to add an extra parameter on each controller method, and I wanted a way to enable or disable this behavior depending on the environment, so here is a slightly more complicated solution that can be enabled using Spring profiles, and doesn't touch the controllers:

    import org.springframework.http.HttpHeaders;
    import org.springframework.http.HttpRequest;
    import org.springframework.http.client.ClientHttpRequestExecution;
    import org.springframework.http.client.ClientHttpRequestInterceptor;
    import org.springframework.http.client.ClientHttpResponse;
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    public class BasicAuthPropagationInterceptor implements HandlerInterceptor, ClientHttpRequestInterceptor {
    
        private final ThreadLocal<String> cachedHeader = new ThreadLocal<>();
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
            cachedHeader.set(header);
            return true;
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            cachedHeader.remove();
        }
    
        @Override
        public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
            String ch = cachedHeader.get();
            if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION) && ch != null) {
                request.getHeaders().add(HttpHeaders.AUTHORIZATION, ch);
            }
            return execution.execute(request, body);
        }
    }
    

    This stores the received header in a ThreadLocal and adds it with an interceptor for RestTemplate.

    This can then be configured as such:

    import org.springframework.boot.web.client.RestTemplateCustomizer;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Profile;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    @Profile("LOCAL")
    class LocalConfiguration implements WebMvcConfigurer {
    
        private final BasicAuthPropagationInterceptor basicAuthPropagationInterceptor
                = new BasicAuthPropagationInterceptor();
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(basicAuthPropagationInterceptor);
        }
    
        @Bean
        RestTemplateCustomizer restTemplateCustomizer() {
            return restTemplate -> restTemplate.getInterceptors().add(basicAuthPropagationInterceptor);
        }
    }
    

    RestTemplate obtained by using the default RestTemplateBuilder bean will then automatically set the Authorization HTTP header if it's available in the current thread.