Skip to the content.
package com.example.restclient;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
import java.time.LocalDateTime;

import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.commons.lang3.StringUtils;

@ApplicationScoped
@Provider
public class ClientCredentialFilter implements ClientRequestFilter {

    private static final Logger logger = LoggerFactory.getLogger(ClientCredentialFilter.class);

    @Inject
    @ConfigProperty(name = "clientId")
    private javax.inject.Provider<String> clientId;

    @Inject
    @ConfigProperty(name = "clientSecret")
    private javax.inject.Provider<String> clientSecret;

    @Inject
    TokenExchangeClientFactory tokenClientFactory;

    private static String outboundToken = null;
    private static LocalDateTime outboundTokenExpirationTime;

    private boolean filterRequest() {
        boolean filterRequest = false;
        if (this.clientId != null && this.clientSecret != null) {
            filterRequest = true;
        } else {
            logger.error("Filter initialization failed. The config providers were not injected.");
        }
        return filterRequest;
    }

    @Override
    public void filter(ClientRequestContext reqContext) throws IOException {
        if (this.filterRequest() && StringUtils.isNoneBlank(clientId.get(), clientSecret.get())) {
            if (StringUtils.isNotBlank(reqContext.getHeaderString("Authorization"))) {
                logger.warn("The token provided by this filter will replace the existing token. URI: {}", reqContext.getUri());
            }

            logger.debug("Requesting token for service call");

            if (outboundToken == null || (outboundTokenExpirationTime != null && LocalDateTime.now().isAfter(outboundTokenExpirationTime))) {
                TokenExchangeResponse tokenResponse = this.tokenClientFactory.newJaxRsClient()
                    .exchangeClientAccessToken(clientId.get(), clientSecret.get());

                if (tokenResponse != null) {
                    outboundTokenExpirationTime = LocalDateTime.now().plusSeconds(tokenResponse.getExpiresIn());
                    outboundToken = tokenResponse.getAccessToken();
                }
            }

            if (StringUtils.isBlank(outboundToken)) {
                logger.warn("Could not provide a bearer token. No outbound token was specified.");
            } else {
                reqContext.getHeaders().add("Authorization", "Bearer " + outboundToken);
            }
        }
    }
}

// Sample service using the filter
@ApplicationScoped
class MyService {

    @Inject
    ClientCredentialFilter clientCredentialFilter;

    public String callOtherService(String url, String requestPayload) {
        Client client = ClientBuilder.newClient().register(clientCredentialFilter);

        Response response = client.target(url)
            .request(MediaType.APPLICATION_JSON)
            .post(Entity.entity(requestPayload, MediaType.APPLICATION_JSON));

        String result = response.readEntity(String.class);
        response.close();
        return result;
    }
}



Absolutely โ€” let's go through this code step by step so you fully understand how service-to-service REST API communication is handled here, including the security aspects.

---

## ๐Ÿ”น Overview of What's Happening

You're using **JAX-RS** (Java API for RESTful Web Services) with **MicroProfile Config** and a **custom request filter** to securely call another service.
The filter injects a **Bearer token** (OAuth2 Client Credentials Flow) into the request automatically.

---

## ๐Ÿ”น Code Breakdown

### ### 1. `ClientCredentialFilter` class

This is a `@Provider`-annotated **JAX-RS ClientRequestFilter** that adds an `Authorization: Bearer <token>` header to outgoing HTTP requests.

```java
@ApplicationScoped
@Provider
public class ClientCredentialFilter implements ClientRequestFilter

### 2. Injected Configuration

@Inject
@ConfigProperty(name = "clientCredentialFilter.clientId")
private javax.inject.Provider<String> clientId;

### 3. Token Fetching Dependencies

@Inject
TokenExchangeClientFactory tokenClientFactory;

This factory helps you obtain a token using client ID and secret via OAuth2 token exchange.


### 4. Static Fields to Cache the Token

private static String outboundToken = null;
private static LocalDateTime outboundTokenExpirationTime;

To avoid calling the token API on every request, you cache the token and its expiration time. The token is refreshed only when itโ€™s expired.


### 5. filterRequest() โ€“ Should This Filter Apply?

private boolean filterRequest() {
    return this.clientId != null && this.clientSecret != null;
}

Basic sanity check to ensure configuration is loaded.


### 6. filter(...) โ€“ Core Logic

This is where the token is fetched and injected into the request:

a. Skip if missing config or blank strings

if (filterRequest() && StringUtils.isNoneBlank(clientId.get(), clientSecret.get()))

b. Check if Authorization already exists

if (StringUtils.isNotBlank(reqContext.getHeaderString("Authorization"))) {
    logger.warn("The token provided by this filter will replace the existing token...");
}

Warns if someone else has already set an Authorization header.

c. Token Expiration Check

if (outboundToken == null || LocalDateTime.now().isAfter(outboundTokenExpirationTime))

If the token is missing or expired, fetch a new one.

d. Fetch Token

TokenExchangeResponse tokenResponse = tokenClientFactory.newJaxRsClient().exchangeClientAccessToken(...);

Fetches a token using client credentials. Sets:

e. Set Authorization Header

reqContext.getHeaders().add("Authorization", "Bearer " + outboundToken);

This line injects the token into the actual HTTP request.


### 7. MyService โ€“ Calls Another REST API

Client client = ClientBuilder.newClient().register(clientCredentialFilter);
Response response = client.target(url)
    .request(MediaType.APPLICATION_JSON)
    .post(Entity.entity(requestPayload, MediaType.APPLICATION_JSON));
String result = response.readEntity(String.class);

๐Ÿ”น Summary

Component Purpose
ClientCredentialFilter Injects access token via filter
clientId & clientSecret Pulled from config
TokenExchangeClientFactory Gets token from identity provider
MyService.callOtherService() Makes the actual API call using JAX-RS
@Provider Enables JAX-RS to detect the filter

Great! Letโ€™s now convert the JAX-RS version into a Spring Boot version that does the same thing: securely call another service using a Bearer token obtained via OAuth2 Client Credentials Flow โ€” but this time, using Springโ€™s WebClient (not JAX-RS).


โœ… Step-by-step: Equivalent Spring Boot Version

Weโ€™ll cover:

  1. application.yml configuration
  2. A component to fetch and cache the token
  3. A service class to make secure REST API calls

๐Ÿ”น 1. application.yml

client-credentials:
  client-id: your-client-id
  client-secret: your-client-secret
  token-uri: https://auth-server.example.com/oauth/token

๐Ÿ”น 2. Token Fetching Component

@Component
public class OAuth2TokenProvider {

    @Value("${client-credentials.client-id}")
    private String clientId;

    @Value("${client-credentials.client-secret}")
    private String clientSecret;

    @Value("${client-credentials.token-uri}")
    private String tokenUri;

    private String cachedToken;
    private Instant tokenExpiryTime;

    public synchronized String getAccessToken() {
        if (cachedToken == null || Instant.now().isAfter(tokenExpiryTime)) {
            fetchNewToken();
        }
        return cachedToken;
    }

    private void fetchNewToken() {
        WebClient client = WebClient.builder().build();

        MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
        form.add("grant_type", "client_credentials");
        form.add("client_id", clientId);
        form.add("client_secret", clientSecret);

        Map<String, Object> response = client.post()
            .uri(tokenUri)
            .contentType(MediaType.APPLICATION_FORM_URLENCODED)
            .bodyValue(form)
            .retrieve()
            .bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
            .block();

        if (response != null) {
            cachedToken = (String) response.get("access_token");
            int expiresIn = (Integer) response.get("expires_in");
            tokenExpiryTime = Instant.now().plusSeconds(expiresIn - 30); // buffer
        }
    }
}

๐Ÿ”น 3. Service to Call Another API

@Service
public class MyRestClient {

    private final OAuth2TokenProvider tokenProvider;
    private final WebClient webClient;

    @Autowired
    public MyRestClient(OAuth2TokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
        this.webClient = WebClient.builder().build();
    }

    public String callOtherService(String url, Object requestBody) {
        String token = tokenProvider.getAccessToken();

        return webClient.post()
                .uri(url)
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(requestBody)
                .retrieve()
                .bodyToMono(String.class)
                .block();
    }
}

๐Ÿ” Comparison: Spring Boot vs. JAX-RS

Feature JAX-RS + ClientRequestFilter Spring Boot + WebClient
Framework JAX-RS / MicroProfile Spring Boot
Token Injection Done via ClientRequestFilter Done via WebClient with manual header
Token Caching Static fields In-memory fields with Instant check
Configuration @ConfigProperty @Value from application.yml
Request Building ClientBuilder WebClient.builder()
Flexibility Needs boilerplate for token logic Easily pluggable and reactive
Community Support Less Spring-specific Massive community + built-in support

โœ… Best Practice Notes


โœ… Step 1: Spring Bootโ€™s Built-in OAuth2 Client Credentials Support (No manual WebClient setup)

This is the cleanest and most idiomatic way in Spring Boot to call secure APIs using OAuth2 Client Credentials.


๐Ÿ”น 1. Add Dependencies

For Maven:

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

๐Ÿ”น 2. application.yml Configuration

spring:
  security:
    oauth2:
      client:
        registration:
          my-client:
            client-id: your-client-id
            client-secret: your-client-secret
            authorization-grant-type: client_credentials
            scope: read write
        provider:
          my-provider:
            token-uri: https://auth-server.example.com/oauth/token

๐Ÿ”น 3. Inject OAuth2AuthorizedClientManager (Spring Boot 2.4+)

@Configuration
public class WebClientOAuth2Config {

    @Bean
    public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
        ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
            new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        oauth2.setDefaultClientRegistrationId("my-client");
        return WebClient.builder()
            .apply(oauth2.oauth2Configuration())
            .build();
    }
}

๐Ÿ”น 4. Use WebClient to Call Secure API

@Service
public class MySecureApiClient {

    private final WebClient webClient;

    @Autowired
    public MySecureApiClient(WebClient webClient) {
        this.webClient = webClient;
    }

    public String callSecureService(String url, Object requestBody) {
        return webClient.post()
                .uri(url)
                .bodyValue(requestBody)
                .retrieve()
                .bodyToMono(String.class)
                .block();
    }
}

๐Ÿ‘‰ Spring will automatically inject the Bearer token using the client_credentials flow.


๐Ÿ”„ Alternate Approaches

๐Ÿ”ธ 1. RestTemplate (Deprecated for OAuth2)

โš ๏ธ Manual and not recommended for new projects.

Youโ€™d have to:

โš™๏ธ Verbose and hard to test.


๐Ÿ”ธ 2. OpenFeign (Spring Cloud)

If youโ€™re using Spring Cloud, Feign makes service-to-service calls declarative.

1. Dependency

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2. Enable Feign

@SpringBootApplication
@EnableFeignClients
public class YourApp {
}

3. Config (application.yml)

feign:
  client:
    config:
      default:
        request-interceptors:
          - com.example.security.OAuth2FeignRequestInterceptor

4. Feign Client Interface

@FeignClient(name = "other-service", url = "https://api.example.com")
public interface OtherServiceClient {
    @PostMapping("/data")
    String sendData(@RequestBody MyRequest req);
}

5. Add Token Interceptor Manually

@Component
public class OAuth2FeignRequestInterceptor implements RequestInterceptor {

    @Autowired
    private OAuth2TokenProvider tokenProvider;

    @Override
    public void apply(RequestTemplate template) {
        String token = tokenProvider.getAccessToken();
        template.header("Authorization", "Bearer " + token);
    }
}

โ˜‘๏ธ Declarative, clean. Ideal with Spring Cloud.


๐Ÿงพ Final Summary โ€“ What to Choose?

Approach Best For Pros Cons
๐Ÿ”น Manual WebClient + token Full control Fine-grained handling Boilerplate code
๐Ÿ”น WebClient + Spring OAuth2 โœ… Recommended No boilerplate, secure, token caching Slightly more setup
๐Ÿ”น Feign + Spring Cloud Declarative REST clients Cleanest code, testable Adds Spring Cloud
๐Ÿ”น RestTemplate Legacy apps Simpler than JAX-RS Deprecated for OAuth2