How to Separate External and Internal Authentication in Spring Cloud Microservices

This article explains why authentication loops occur when external user tokens and internal service tokens are handled together in Spring Security, and provides a step‑by‑step solution using distinct headers, custom filters, and Feign interceptors to break the cycle.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
How to Separate External and Internal Authentication in Spring Cloud Microservices

Introduction

In a microservice architecture, external requests and internal service calls usually follow different authentication logic.

External user access must validate a user_token, while service‑to‑service calls only need a client_token to indicate an internal request.

Problem: When a user sends an access_token to Service A, Spring Security’s filter triggers a Feign call to Service B with the same token, which is intercepted again, causing an authentication loop.

Requirement Analysis

1. External request

Must carry user_token When passing through Service A, Spring Security must run user authentication logic

2. Service‑to‑service call

Feign call must include client_token Service B only validates client_token, no user authentication needed

3. Problem point

When Service A’s security filter uses Feign to verify access_token, the request is intercepted again, leading to infinite recursion.

Common Pitfalls

Not distinguishing token types

User token and client token are mixed, causing all requests to follow the same interception logic.

Feign applies global interceptor

Spring Security’s OncePerRequestFilter applies to all requests, including Feign internal calls.

Risk of dead loop

Service A calls B for token verification, B intercepts as a user request, calls A again, forming a loop.

Solution Idea

1. Distinguish two request types

User request: header Authorization: Bearer user_token Internal call: header

X-Client-Token: client_token

2. Spring Security double authentication chain

User requests go through JWT validation

Internal calls only validate client_token, bypassing user authentication

3. Add client_token to Feign requests

Use RequestInterceptor to automatically add

X-Client-Token

4. Custom authentication filters

Two filters are needed: one for user token, one for client token.

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignConfig {
    @Bean
    public RequestInterceptor clientTokenInterceptor() {
        return (RequestTemplate template) -> {
            // Simulate obtaining client_token from config or vault
            template.header("X-Client-Token", "my-client-token-123");
        };
    }
}
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;

public class ClientTokenFilter extends OncePerRequestFilter {
    private final String CLIENT_TOKEN = "my-client-token-123";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String clientToken = request.getHeader("X-Client-Token");
        if (clientToken != null && clientToken.equals(CLIENT_TOKEN)) {
            // Authenticate as internal service
            UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken("internal-service", null, null);
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable());
        http.authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
        );
        // client token filter has higher priority than user token filter
        http.addFilterBefore(new ClientTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        http.addFilterBefore(new UserTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;

public class UserTokenFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String authHeader = request.getHeader("Authorization");
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            String userToken = authHeader.substring(7);
            // TODO: call Service B to verify user_token, then set user context
            // JWT verification logic omitted for brevity
        }
        filterChain.doFilter(request, response);
    }
}

The logic is simple: if the request contains a valid X-Client-Token, it is authenticated as an internal service; otherwise the request proceeds to the usual user authentication flow.

Practical Scenario

User "Xiao Ming" logs in via the app and sends a request to /api/orders with user_token.

Service A receives the request and needs to call Service B to verify the token.

The Feign call automatically adds X-Client-Token, so Service B treats it as an internal call and skips user authentication.

After Service B validates successfully, Service A continues its business logic.

Conclusion

Add client_token to all Feign calls via a request interceptor.

Insert a ClientTokenFilter before the user token filter in the Spring Security chain.

Keep user requests flowing through JWT or access_token validation.

Proper filter ordering prevents Feign calls from triggering user authentication, fully solving the dead‑loop issue.

backendJavamicroservicesfeignToken AuthenticationSpring Security
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

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.