Search code examples
spring-mvcspring-securityspring-oauth2

How to configure RestTemplate to use browser's session for api call?


I have an API written with Spring Boot RestControllers secured with keycloak. I am writing a Thymeleaf client in a separate Spring Boot application to use this API. The browser of the client successfully logs into keycloak and on the controller I am able to access the OAuth2AuthenticatedPrincipal via @AuthenticationPrincipal.

How can I configure a RestTemplate to use this already established trust relationship rather than establish a new one for each RestTemplate?

The code below is the code that successfully authenticates with the api using the user authenticated in the browser.

Controller

@Controller
@RequestMapping("/product")
public class ProductController {
    private final RestTemplate restTemplate;
    private final ProductService productService;
    private final ClientRegistrationRepository clientRegistrationRepository;
    // only needed to validate registration during debug
    private final InMemoryClientRegistrationRepository clientRegistrationRepository;

    public ProductController(RestTemplate restTemplate, ProductService productService,
                           ClientRegistrationRepository clientRegistrationRepository,
                           InMemoryClientRegistrationRepository clientRegistrationRepository) {
        this.restTemplate = restTemplate;
        this.productService = productService;
        this.clientRegistrationRepository = clientRegistrationRepository;
        this.clientRegistrationRepository = clientRegistrationRepository;
    }

    @GetMapping("")
    @PreAuthorize("isAuthenticated()")
    public String index(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal,
                        Authentication auth,
                        HttpServletRequest servletRequest,
                        Model model) {
        OAuth2AuthorizedClient accessToken = clientRepository.loadAuthorizedClient("keycloak-confidential-user",
                auth, servletRequest);

    log.debug("accessToken is null [{}]", accessToken == null);
    model.addAttribute("products",
            productService.getProductWithDetailsForUser(UUID.fromString(principal.getName()), accessToken));
    return "product/list";
}

Service Method

public List<ProductListInfo> getProductWithDetailsForUser(UUID userId, String token) {
    List<ProductListInfo> products = productRepository.findByUser_UniqueUserOrderByNameAsc(userId,
            Pageable.ofSize(10));

    if(token != null) {
        header.setBearerAuth(token);
        for (ProductListInfo product : products) {
            ProductPublicDto publicData = restTemplate.exchange(
                    "https://localhost:8043/product/%s/public".formatted(product.getProductId()),
                    HttpMethod.GET, new HttpEntity<>(header), ProductPublicDto.class).getBody();
            productMapper.partialUpdateDetails(publicData product);
        }
    }
    return products;
}

pom.xml snip

...
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>
...

Solution

  • If your app with Thymeleaf controllers is configured as an OAuth2 client, the other app with REST API should be configured as an OAuth2 resource server and would probably be stateless (no session).

    Requests to resource servers are authorized with Bearer access tokens, not sessions. In your client, configure your REST client (RestTemplate is not quite the trend, you might have a look at WebClient or @FeignClient) to set an Authorization header with a Bearer string containing the access token you get from the OAuth2AuthorizedClient (you can have the OAuth2AuthorizedClientRepository autowired in your OAuth2 client controllers, and query it to retrieve the OAuth2AuthorizedClient you need).

    I have a complete working sample with WebClient (not RestTemplate, sorry) there. In this tutorials, the client and resource server parts are merged in a single app, but the two parts have distinct SecurityFilterChain beans and communicate with a REST client. Internal requests are authorized with a Bearer access token, just as you probably need.