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:
BearerTokenAuthenticationFilterJwtAuthenticationProvider
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:
- Enable Spring Security DEBUG logs
- Confirm token is received by the server
- Confirm JWT decoding succeeds
- Log claims and authorities
- Verify role prefixes (
ROLE_) - Check filter order
- Add custom entry points for visibility
- 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
Post a Comment