Debugging Spring Boot Security: Why You Keep Getting 401 or 403 (and How to Find the Real Cause)

Debugging Spring Security — Why You Keep Getting 401 or 403

If you’ve ever enabled Spring Boot Security and suddenly every request starts returning 401 Unauthorized or 403 Forbidden, you’re not alone.

What makes this frustrating is not the error itself — it’s that Spring Security often hides the real reason:

  • Is the token expired?
  • Is the token malformed?
  • Is a required claim missing?
  • Does the user lack the correct role?
  • Did the request even reach your controller?

This post walks through how to debug Spring Security properly, covering both:

  • Spring MVC (Servlet / Web)
  • Spring WebFlux (Reactive)

First: Understand 401 vs 403

Before debugging, it’s critical to understand what these errors actually mean in Spring Security.

401 Unauthorized

Usually means:

  • No token provided
  • Token is invalid or expired
  • Authentication failed

👉 Authentication problem

403 Forbidden

Usually means:

  • Authentication succeeded
  • User is logged in
  • But does not have permission to access the resource

👉 Authorization problem

Knowing which one you’re getting already narrows the search by half.


Step 1: Turn on Spring Security Debug Logs

Spring Security is very quiet by default.

Add this to application.yml:


logging:
  level:
    org.springframework.security: DEBUG

Or even more detailed:


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

This immediately shows:

  • Which filter rejected the request
  • Whether authentication was attempted
  • Whether authorization failed later

📌 This is the single most important step.


Step 2: Debug JWT Authentication (Servlet / Web)

Common Issue: Token Expired or Invalid

If you use JWT, authentication usually fails inside:

  • BearerTokenAuthenticationFilter
  • JwtAuthenticationProvider

Enable logs and look for messages like:

  • JWT expired
  • Invalid signature
  • Failed to decode JWT

If you have a custom decoder:


@Bean
JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder
        .withJwkSetUri(jwkSetUri)
        .build();
}

Wrap decoding with logging:


try {
    return decoder.decode(token);
} catch (JwtException ex) {
    log.error("JWT decode failed", ex);
    throw ex;
}

This exposes the real root cause instead of just a blind 401.


Step 3: Missing or Incorrect Claims

One of the most common silent failures in Spring Security is missing or misconfigured claims.

Example:


.authorizeHttpRequests(auth -> auth
    .requestMatchers("/admin/**").hasRole("ADMIN")
)

But your token contains:


{
  "roles": ["admin"]
}

Spring expects authorities like:


ROLE_ADMIN

Fix it explicitly using a JwtGrantedAuthoritiesConverter:


JwtGrantedAuthoritiesConverter converter =
    new JwtGrantedAuthoritiesConverter();

converter.setAuthoritiesClaimName("roles");
converter.setAuthorityPrefix("ROLE_");

Then wire it:


JwtAuthenticationConverter authConverter =
    new JwtAuthenticationConverter();

authConverter.setJwtGrantedAuthoritiesConverter(converter);

Without this, you’ll get a 403 with no clear explanation.


Step 4: Add a Custom AuthenticationEntryPoint (Servlet)

To expose authentication failures:


@Bean
AuthenticationEntryPoint authenticationEntryPoint() {
    return (request, response, authException) -> {
        log.error("Authentication failed", authException);
        response.sendError(
            HttpServletResponse.SC_UNAUTHORIZED,
            authException.getMessage()
        );
    };
}

This turns a silent 401 into a diagnosable error.


Reactive (WebFlux): Debugging Is Different

In Spring WebFlux, authentication failures happen inside the reactive chain — often without obvious errors.

Enable Reactive Security Logs


logging:
  level:
    org.springframework.security.web.server: DEBUG

Add a Reactive Authentication Entry Point


@Bean
ServerAuthenticationEntryPoint entryPoint() {
    return (exchange, ex) -> {
        log.error("Reactive auth failed", ex);
        exchange.getResponse()
                .setStatusCode(HttpStatus.UNAUTHORIZED);
        return exchange.getResponse().setComplete();
    };
}

Inspect the Reactive Security Context

Remember: in WebFlux, the security context is not thread-local.


ReactiveSecurityContextHolder.getContext()
    .doOnNext(ctx ->
        log.info("User: {}", ctx.getAuthentication())
    )

If this never runs — authentication never succeeded.


Step 5: Check Filter Order (Servlet & Reactive)

Custom filters in the wrong order cause mysterious failures.

Servlet


.addFilterBefore(customFilter,
    UsernamePasswordAuthenticationFilter.class)

Reactive


http.addFilterAt(customFilter,
    SecurityWebFiltersOrder.AUTHENTICATION)

Wrong order = token never processed = endless 401s.


A Simple Debug Checklist

When stuck with 401 / 403:

  1. Enable Spring Security DEBUG logs
  2. Confirm token is received by the server
  3. Confirm JWT decoding succeeds
  4. Log claims and authorities
  5. Verify role prefixes (ROLE_)
  6. Check filter order
  7. Add custom entry points for visibility
  8. In WebFlux, verify reactive security context

Final Thoughts

Spring Security is powerful — but not transparent by default.

Most 401 / 403 issues are not security bugs, but missing visibility.

Once you:

  • Turn on logs
  • Add explicit handlers
  • Inspect claims and authorities

Security becomes predictable instead of painful.

If this post saved you time, feel free to support the blog — otherwise, happy debugging 🙂

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