Enterprise Java applications frequently live behind a corporate Single Sign-On (SSO) gateway such as Broadcom (formerly CA) SiteMinder. SiteMinder authenticates users at the reverse-proxy layer — before any request ever reaches your application server — and then injects the verified identity into a well-known HTTP request header (typically SM_USER). Your Spring Boot application simply trusts that header and builds a security context from it, with no login form and no password handling of its own.
This pattern is called pre-authentication: the heavy lifting of credential verification is delegated to an external system, and your app only needs to map the already-authenticated identity to application-level roles. Spring Security has first-class support for this via RequestHeaderAuthenticationFilter and PreAuthenticatedAuthenticationProvider.
This tutorial walks through a complete Spring Boot 3.x / Spring Security 6.x configuration, covering Maven dependencies, the security filter chain using the modern lambda DSL, a UserDetailsService implementation, and tips for testing locally without a real SiteMinder agent.
Maven Dependencies
With Spring Boot 3.x you only need the two starters below. The Boot BOM manages the Spring Security 6.x version (currently 6.4.x) for you — no explicit version pins required on the security artifacts.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
spring-boot-starter-security transitively pulls in spring-security-web and spring-security-config at the version managed by the Spring Boot parent POM. If you are not using the Spring Boot parent, add the spring-security-bom to your <dependencyManagement> block and pin to 6.4.x.
How SiteMinder Pre-Authentication Works
The flow is straightforward:
- The user’s browser hits a URL protected by the SiteMinder policy server.
- SiteMinder intercepts the request, challenges the user (login page, Kerberos ticket, certificate, etc.), and verifies the credentials against its user store.
- On success, SiteMinder forwards the request to your application server with an injected
SM_USERheader containing the authenticated username. - Spring Security’s
RequestHeaderAuthenticationFilterreads that header, extracts the principal name, and calls theAuthenticationManager. - The
PreAuthenticatedAuthenticationProvidercalls yourUserDetailsServiceto load roles, then places anAuthenticationobject in theSecurityContext. - Your controllers see a fully populated
Authenticationas if the user had logged in normally.
Critically, your application never sees unauthenticated users in production — SiteMinder blocks them before the request reaches Tomcat. The setExceptionIfHeaderMissing(false) setting (shown below) is useful for local development where there is no SiteMinder agent present.
Spring Security Configuration
In Spring Security 6.x the WebSecurityConfigurerAdapter class is completely removed. Configuration is done by declaring a SecurityFilterChain bean. Below is a complete, self-contained configuration class.
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final UserDetailsService userDetailsService;
public SecurityConfig(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Bean
public RequestHeaderAuthenticationFilter siteminderFilter() throws Exception {
RequestHeaderAuthenticationFilter filter = new RequestHeaderAuthenticationFilter();
filter.setPrincipalRequestHeader("SM_USER"); // SiteMinder header name
filter.setAuthenticationManager(authenticationManager());
filter.setExceptionIfHeaderMissing(false); // allow unauthenticated requests through
return filter;
}
@Bean
public PreAuthenticatedAuthenticationProvider preAuthProvider() {
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(
new UserDetailsByNameServiceWrapper<>(userDetailsService)
);
return provider;
}
@Bean
public ProviderManager authenticationManager() {
return new ProviderManager(preAuthProvider());
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.addFilter(siteminderFilter())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
);
return http.build();
}
}
Key points to note:
- Jakarta EE imports — All
javax.servlet.*imports becomejakarta.servlet.*in Spring Boot 3.x. If you see compile errors mentioningjavax.servlet, you are mixing a Spring Boot 2.x dependency into a Boot 3.x project. RequestHeaderAuthenticationFilter— This built-in Spring Security filter reads the named header and hands the value to theAuthenticationManageras the pre-authenticated principal. No custom filter class is needed.setExceptionIfHeaderMissing(false)— In production SiteMinder guarantees the header is always present. Setting this tofalseprevents aPreAuthenticatedCredentialsNotFoundExceptionwhen running locally without a SiteMinder agent.- Lambda DSL —
.authorizeHttpRequests(auth -> auth...)replaces the deprecated.authorizeRequests()method from Spring Security 5.x. - No
WebSecurityConfigurerAdapter— That class was deprecated in 5.7 and removed in 6.0. UseSecurityFilterChainbeans as shown above.
UserDetailsService Implementation
The UserDetailsService receives the username that SiteMinder placed in the header and must return a UserDetails object with the appropriate roles. In a real application you would look up the user’s roles from a database, LDAP, or another authority store. The example below uses a hardcoded role for illustration.
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class SiteminderUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// In a real application, load roles from a database or LDAP here.
// The username comes directly from the SM_USER header via the pre-auth filter.
if (username == null || username.isBlank()) {
throw new UsernameNotFoundException("No username supplied by SiteMinder");
}
return User.withUsername(username)
.password("") // pre-authenticated — no password is needed or checked
.roles("USER") // replace with roles loaded from your authority store
.build();
}
}
Note that User.withUsername(...).password("").roles(...).build() uses the fluent builder introduced in Spring Security 5 and still supported in 6.x. The empty password string is intentional — Spring Security never verifies it in the pre-authentication flow.
Testing Locally Without SiteMinder
Because SiteMinder only runs in your corporate environment, local development requires a way to simulate the header. The simplest approach is curl:
curl -H "SM_USER: testuser" http://localhost:8080/your-endpoint
For a more realistic local setup you can add a development-only OncePerRequestFilter that injects the SM_USER header when a Spring profile named local is active:
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
@Component
@Profile("local")
public class LocalSiteminderStubFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) {
@Override
public String getHeader(String name) {
if ("SM_USER".equalsIgnoreCase(name)) return "dev-user";
return super.getHeader(name);
}
@Override
public Enumeration<String> getHeaders(String name) {
if ("SM_USER".equalsIgnoreCase(name))
return Collections.enumeration(Collections.singletonList("dev-user"));
return super.getHeaders(name);
}
};
chain.doFilter(wrapper, response);
}
}
Activate it with --spring.profiles.active=local when starting the app locally. This filter is completely ignored in any other environment.
Frequently Asked Questions
What header name does SiteMinder use?
The default is SM_USER, but your SiteMinder administrator may have configured a different header name in the SiteMinder policy server. Check the SiteMinder agent configuration or ask your infrastructure team. Update setPrincipalRequestHeader() in SecurityConfig to match whatever header name your installation uses.
What are the key differences between Spring Security 5.x and 6.x for pre-authentication?
Three changes affect pre-auth setups specifically:
- Jakarta EE —
javax.servlet.*becomesjakarta.servlet.*everywhere. This is a hard compile-time break if you upgrade without updating imports. - Authorisation DSL —
.authorizeRequests()is removed; use.authorizeHttpRequests()with the lambda DSL. - No
WebSecurityConfigurerAdapter— Extend-and-override is gone. All configuration must use@Bean-returning methods on a plain@Configurationclass.
Do I need to disable CSRF for SiteMinder pre-authentication?
It depends on your client type. If your application serves only REST API clients (mobile apps, SPAs that manage their own CSRF tokens, or other backend services), you can safely disable CSRF with .csrf(csrf -> csrf.disable()). If your application renders traditional browser-based HTML forms, keep CSRF protection enabled — Spring Security’s default CSRF filter works correctly alongside pre-authentication.
What if I also need to extract roles from a SiteMinder header?
SiteMinder can be configured to inject role or group membership into a second header (often SM_ROLE or a site-specific name). Inject an HttpServletRequest into your UserDetailsService via RequestContextHolder, or better, use a custom AbstractPreAuthenticatedProcessingFilter subclass that extracts credentials from both the user header and the role header and packages them together as the pre-authenticated credentials object. The PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails class in Spring Security is designed for exactly this use case.
Conclusion
Integrating SiteMinder with Spring Boot 3.x is clean and concise: declare a RequestHeaderAuthenticationFilter, wire it to a PreAuthenticatedAuthenticationProvider backed by your UserDetailsService, and register both through a SecurityFilterChain bean. The lambda DSL and Jakarta EE imports are the two most visible differences from older Spring Security 3.x/4.x XML-based configurations, but the underlying pre-authentication model is the same.
With the stub filter pattern shown above you can develop and test the security integration fully on your local machine, then deploy to an environment where SiteMinder takes over without any code changes.