How to debug Spring Security filter chains step by step

How to debug Spring Security filter chains step by step

When Spring Security blocks a request, it usually happens long before your controller is reached.

At that point, many developers are stuck asking:

“Which filter rejected my request — and why?”

This post shows a practical, step-by-step approach to debug Spring Security filter chains, without reading the entire framework source code.


Why filter chains are so hard to debug

Spring Security uses a chain of filters, each responsible for one concern:

  • Request matching
  • Authentication
  • Authorization
  • Exception handling

Once a filter rejects a request, the chain stops.

If you don’t know which filter made the decision, debugging becomes guesswork.


Step 1: Confirm which SecurityFilterChain is used

The first thing to verify is whether the request is handled by the expected security chain.

Enable this log:

logging.level.org.springframework.security.web.FilterChainProxy=DEBUG

You will see output like:



Securing GET /api/admin/users

Using SecurityFilterChain [RequestMatcher=any request]

If the wrong chain is selected, everything that follows is irrelevant.

Many 401 / 403 issues start here.


Step 2: Identify the filter where the chain stops

Next, enable:

logging.level.org.springframework.security=DEBUG

Now each filter will log when it processes the request.

Look for the last filter that logs before the response is returned.

That filter is where the request was rejected.


Step 3: Know the most common “stop filters”

In real-world systems, most failures happen in just a few filters:

  • BearerTokenAuthenticationFilter — JWT not parsed or rejected
  • AnonymousAuthenticationFilter — request treated as anonymous
  • ExceptionTranslationFilter — converts exceptions to 401 / 403
  • AuthorizationFilter — access denied

Once you know which filter stopped the chain, the cause is usually obvious.


Step 4: Distinguish authentication vs authorization failures

This distinction is critical:

  • If the chain stops before authentication → 401
  • If it stops after authentication → 403

Ask this question:

Did Spring ever create an Authentication object?

If not, focus on token parsing and filters.

If yes, focus on roles, authorities, and matchers.


Step 5: Log SecurityContext at key points

Strategically logging the security context saves hours of debugging.

Right after authentication:



Authentication auth =

    SecurityContextHolder.getContext().getAuthentication();

log.info("Authenticated user={}, authorities={}",

    auth != null ? auth.getName() : "none",

    auth != null ? auth.getAuthorities() : "none");

If this log never appears, authentication never succeeded.


Step 6: Watch for matcher-related surprises

Security rules are evaluated in order.

A common mistake:



.requestMatchers("/api/**").authenticated()

.requestMatchers("/api/admin/**").hasRole("ADMIN")

The first rule matches everything — the second rule is never reached.

Debug logs will reveal this immediately.


Step 7: Reactive (WebFlux) filter chains behave differently

In WebFlux:

  • There is no thread-local context
  • Logging outside the reactive chain often shows nothing

Filters may appear to run, but authentication context is lost downstream.

Always log inside the reactive pipeline.


A repeatable debugging checklist

When a request fails, walk through this checklist:

  1. Which SecurityFilterChain handled the request?
  2. Which filter was last executed?
  3. Was authentication created?
  4. Which matcher and rule were applied?

If you can answer all four, the bug is usually trivial.


Final thoughts

Spring Security is complex — but it is not random.

Every 401 or 403 comes from a specific filter, in a specific chain, for a specific reason.

Once you learn how to observe the filter chain, debugging becomes systematic instead of frustrating.


Series complete:
This post concludes the Spring Security 401 / 403 debugging series.

Part of the Spring Boot SSO Series

❤️ Support This Blog


If this post helped you, you can support my writing with a small donation. Thank you for reading.


Comments

Popular posts from this blog

fixed: embedded-redis: Unable to run on macOS Sonoma

Copying MDC Context Map in Web Clients: A Comprehensive Guide

Reset user password for your own Ghost blog