How to log Spring Security authorization decisions properly (without noisy logs

How to log Spring Security authorization decisions properly (without noisy logs)

One of the most frustrating things about Spring Security is this:

“I know I got a 403 — but I have no idea why.”

The problem is usually not Spring Security itself.

The problem is that authorization decisions are rarely logged in a useful way.

In this post, I’ll show how to log:

  • Which authorization rule was evaluated
  • What authorities were present
  • Why access was denied

All without turning your logs into unreadable noise.


Why default logs are not enough

Most teams start with:

logging.level.org.springframework.security=DEBUG

This helps — but only to a point.

In real systems, DEBUG logs often:

  • Flood your logs
  • Hide the actual decision
  • Are too late in the request lifecycle

What we really want is focused, decision-level logging.


What we actually want to know

When access is denied, these four answers matter:

  1. Who is the user?
  2. What authorities do they have?
  3. Which rule was evaluated?
  4. Why did it fail?

Everything else is secondary.


Approach 1: Log authorities right before authorization

The simplest and most effective technique is to log the authentication object just before authorization happens.

For example, inside a custom filter or authorization manager:



Authentication auth = SecurityContextHolder.getContext().getAuthentication();

if (auth != null) {

    log.info("User={}, authorities={}",

        auth.getName(),

        auth.getAuthorities());

}

This immediately answers:

  • Was authentication successful?
  • Were any authorities extracted?

If authorities are empty, you already know the problem.


Approach 2: Custom AccessDeniedHandler (Servlet)

For Spring MVC (Servlet-based apps), a custom AccessDeniedHandler gives you a clean logging point.



public class LoggingAccessDeniedHandler implements AccessDeniedHandler {

    @Override

    public void handle(

        HttpServletRequest request,

        HttpServletResponse response,

        AccessDeniedException ex

    ) {

        Authentication auth =

            SecurityContextHolder.getContext().getAuthentication();

        log.warn(

            "Access denied. path={}, user={}, authorities={}",

            request.getRequestURI(),

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

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

        );

        response.setStatus(HttpServletResponse.SC_FORBIDDEN);

    }

}

This produces one clean log line per denial.

No noise. No guessing.


Approach 3: Authorization logging in WebFlux

Reactive security is different.

There is:

  • No thread-local context
  • No global handler you can tap into easily

The correct place to log is inside the reactive chain.

For example, in a custom ReactiveAuthorizationManager:



return Mono.deferContextual(ctx -> {

    Authentication auth = ctx.get(Authentication.class);

    log.info(

        "Authorizing user={}, authorities={}",

        auth.getName(),

        auth.getAuthorities()

    );

    return decision;

});

Logging outside the reactive pipeline often logs nothing.


Approach 4: Log matcher evaluation (the missing piece)

Many 403 issues are caused by the wrong matcher being applied.

Enable this temporarily:

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

This shows:

  • Which matcher matched the request
  • Which rule was evaluated

Once the issue is resolved, turn it back off.


What NOT to log

Avoid logging:

  • Entire JWT tokens
  • Raw request headers
  • Every filter in the chain

This adds risk and noise without improving clarity.


A practical logging strategy

In real systems, I recommend:

  • INFO logs for access denied events
  • DEBUG logs only when actively debugging
  • One log line per authorization decision

This keeps logs:

  • Readable
  • Actionable
  • Safe

Final thoughts

Spring Security does not hide decisions — it just doesn’t log them by default.

Once you log the right decision points, 403 errors stop being mysterious.

If you’re guessing, you’re logging in the wrong place.


Next in the series:
How to debug Spring Security filter chains step by step.

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