Recipe: Bring Your Own SecurityFilterChain
Problem: your app already has a SecurityFilterChain (or needs one IAM’s defaults can’t
express). Two competing chains — or a library that force-feeds you one — is not an option.
How the backoff works
Section titled “How the backoff works”IAM’s own chain is declared @ConditionalOnMissingBean(SecurityFilterChain::class). The moment
your configuration defines any SecurityFilterChain bean, IAM contributes none — reliably,
because auto-configuration is processed after host configuration. The building blocks stay
exposed as beans for you to reuse.
Define your chain, reusing IAM’s JWT filter
Section titled “Define your chain, reusing IAM’s JWT filter”@Configuration@EnableWebSecurityclass HostSecurityConfig( // IAM's filter bean — validates Bearer tokens and populates the SecurityContext, // which is what @RequiresPermission enforcement reads. private val iamJwtFilter: JwtAuthenticationFilter,) { @Bean fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http .csrf { it.disable() } .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } .authorizeHttpRequests { auth -> auth // Permit the ERROR dispatch (NOT the /error path) FIRST — otherwise a controller // throwing ResponseStatusException(404/409/…) re-enters this chain via the // container's error dispatch unauthenticated and surfaces as a misleading 401. .dispatcherTypeMatchers(DispatcherType.ERROR).permitAll() // IAM's endpoints your chain must keep reachable: .requestMatchers("/api/iam/v1/auth/**").permitAll() // login/refresh .requestMatchers(HttpMethod.GET, "/.well-known/jwks.json").permitAll() // your own rules: .requestMatchers("/public/**").permitAll() .anyRequest().authenticated() } .addFilterBefore(iamJwtFilter, UsernamePasswordAuthenticationFilter::class.java) return http.build() }}What you get
Section titled “What you get”- Full ownership of the chain — matchers, CSRF/CORS posture, extra filters, anything.
- IAM authentication still works: the reused
JwtAuthenticationFilterpopulates theSecurityContext, so@RequiresPermission,@FieldFiltered, andPermissionCheckerbehave identically. - No double-chain surprises: IAM’s chain simply never exists.
Lighter alternatives first
Section titled “Lighter alternatives first”Replacing the chain is the last resort. In order of increasing force:
| need | use |
|---|---|
| open a few extra paths | iam.security.additional-permit-all: ["/health/**", ...] |
| adjust IAM’s chain (add a filter, tweak a matcher) | IamSecurityCustomizer SPI — runs against IAM’s HttpSecurity before it builds |
| different chain entirely | this recipe |
@Componentclass MetricsExposure : IamSecurityCustomizer { override fun customize(http: HttpSecurity) { http.authorizeHttpRequests { it.requestMatchers("/actuator/prometheus").permitAll() } }}