Zero Trust in Spring Boot: Token Relay vs Token Exchange for Microservices Security

This article explains why gateway-only JWT validation is insufficient in zero-trust microservices, detailing Token Relay and Token Exchange patterns with Spring Boot 3.5 code examples for secure service-to-service authentication and authorization.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Zero Trust in Spring Boot: Token Relay vs Token Exchange for Microservices Security

1. Introduction

In microservice architectures, requests typically traverse multiple services: Client → API Gateway → Service A → Service B. While the API Gateway validates the incoming JWT, zero-trust principles require each service to independently verify credentials and enforce its own authorization policies.

2. Why Gateway Validation Is Not Enough

The API Gateway performs JWT signature verification, issuer/expiration checks, authentication, routing, and access policies. However, relying solely on gateway validation creates an overly broad trust boundary. If an attacker bypasses the gateway and calls Service B directly, or if network controls fail, Service B would be vulnerable. Zero trust dictates: never trust a request merely because it originates from an internal component. Every resource server must validate its incoming token and apply its own authorization logic, typically by configuring Spring Security as an OAuth 2.0 Resource Server in each Spring Boot service.

3. Service-to-Service Identity

JWT validation answers: trusted issuer? tampered? expired? correct audience? required scopes? But a downstream call introduces three identities: User Identity (who initiated), Service Identity (which microservice calls downstream), and Authorization Context (what the caller may do). Service B may need all three. For example, a user may have order:read scope, but Service A should not automatically gain administrative privileges on Service B. This gap is addressed by Token Relay, Token Exchange, and Client Credentials.

4. Token Relay: Reusing the Original User Token

How It Works

The same user access token is forwarded from gateway → Service A → Service B. Each downstream resource server independently validates the token. Example JWT payload:

{
  "sub": "user-007",
  "iss": "https://auth.pack.com",
  "aud": ["service-a", "service-b"],
  "scope": "order:read order:write",
  "exp": 1792456921590
}

Spring Cloud Gateway configuration with TokenRelay filter:

spring:
  cloud:
    gateway:
      default-filters:
        - TokenRelay
      routes:
        - id: service-a
          uri: lb://service-a
          predicates:
            - Path=/api/v1/resource/**
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.pack.com/auth/realms/master

The TokenRelay filter propagates the authenticated user's OAuth 2.0 access token downstream. Note: Token Relay does not automatically configure Service A to forward the token to Service B; Service A must explicitly implement the downstream call and pass the credential.

Forwarding JWT from Service A

A controller can use RestClient to forward the authenticated JWT:

@RestController
public class ProxyController {
  private final RestClient restClient;
  @GetMapping("/proxy")
  public ResponseEntity<String> proxy(@AuthenticationPrincipal Jwt jwt) {
    String token = jwt.getTokenValue();
    return restClient.get()
      .headers("Authorization", "Bearer %s".formatted(token))
      .uri("http://service-b/api/resource")
      .retrieve().body(String.class);
  }
}

Advantages

Simple implementation

Preserves original user identity and delegated permissions

Avoids extra token exchange requests

Works well when downstream services accept the same token

Limitations

Token expiry in request chain: If the token expires between Service A processing and Service B call, Service B rejects the request despite gateway validation passing.

Audience mismatch: A token issued for Service A may be invalid for Service B; a valid JWT is not necessarily a valid credential for every service.

Excessive token exposure: The original token traverses multiple services, increasing blast radius if logged or leaked.

Missing service identity: The token identifies the user but not which service initiates the downstream call. Adequacy depends on authorization requirements.

Token Relay is appropriate when the original token is explicitly intended for downstream services and its lifetime, audience, and scopes align. Otherwise, Token Exchange provides a more precise security model.

5. Token Exchange: Obtaining a Token for the Next Service

Token Exchange lets Service A trade the received access token for a new token scoped to Service B. Service A requests a downstream token from the Identity Provider instead of forwarding the original. The new token can have a different audience, narrower scopes, and shorter lifetime, while preserving user identity per the IdP's token exchange profile.

Why It Improves Security

If the original token grants broad API access, Service A only needs order:read from Service B. By exchanging for a narrowly scoped token, the system follows least privilege: each component receives only the permissions required for its task. Token Exchange is especially useful when:

Different services require different token audiences

Downstream permissions must be narrower than original

A service acts on behalf of a user

Clearer authorization models are needed

Request chains are long or complex

Not all OAuth 2.0 IdPs support Token Exchange automatically; the IdP must implement the relevant flow and the client must be authorized to perform it. Token Exchange does not inherently solve expiry or service identity issues; issued tokens still need appropriate claims, permissions, and lifetimes.

Implementation with Spring WebClient and Keycloak

Simplified OAuth 2.0 Token Exchange request:

private final WebClient webClient;
Mono<TokenExchangeResponse> response =
    webClient.post()
    .uri("https://auth.pack.com/auth/realms/master/"
      + "protocol/openid-connect/token")
    .contentType(MediaType.APPLICATION_FORM_URLENCODED)
    .body(BodyInserters.fromFormData(
        "grant_type",
        "urn:ietf:params:oauth:grant-type:token-exchange")
      .with("subject_token", userToken)
      .with("subject_token_type",
        "urn:ietf:params:oauth:token-type:access_token")
      .with("client_id", "service-a")
      .with("client_secret", serviceASecret)
      .with("scope", "system:internal"))
    .retrieve()
    .bodyToMono(TokenExchangeResponse.class);

Using the exchanged token for the downstream call:

Mono<String> result = response
  .flatMap(tokenResponse -
    webClient.get()
      .uri("http://service-b/api/resource")
      .headers(headers -
        headers.setBearerAuth(
          tokenResponse.getAccessToken()))
      .retrieve()
      .bodyToMono(String.class));

6. Every Microservice Must Validate Tokens Locally

Regardless of Token Relay, Token Exchange, or Client Credentials, downstream services must enforce their own security policies. Example Service B configuration as an OAuth 2.0 Resource Server:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  http
    .authorizeHttpRequests(auth -> auth
      .requestMatchers("/public/**").permitAll()
      .requestMatchers(HttpMethod.POST, "/orders/**").hasAuthority("order:write")
      .anyRequest().authenticated())
    .oauth2ResourceServer(oauth2 ->
      oauth2.jwt());
  return http.build();
}

Resource servers must also enforce the expected audience ( "who is this for" ). Configuring only the issuer does not guarantee service-specific audience validation; Service B should reject tokens intended for Service A.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Spring BootJWTZero TrustSpring Cloud GatewayOAuth 2.0Token RelayToken ExchangeMicroservices Security
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.