Why Spring Security returns 401 or 403 — and how to know the real reason (Web + Reactive)
Why Spring Security returns 401 or 403 — and how to know the real reason (Web + Reactive)
If you’ve worked with Spring Security long enough, you’ve probably seen this situation:
“Why am I getting 401 or 403, and how do I know what actually failed?”
The HTTP status code is technically correct — but the real root cause is hidden.
In this post, I’ll show how to systematically debug 401 / 403 issues in both Spring MVC (Servlet) and Spring WebFlux (Reactive) applications.
1. 401 vs 403 — what Spring Security really means
Before debugging, it’s important to understand how Spring Security interprets these responses:
| Status | Meaning (Security perspective) |
|---|---|
| 401 | Authentication failed or missing |
| 403 | Authentication succeeded, but access was denied |
Important:
Spring Security makes this decision before your controller is ever called.
If you don’t know which step rejected the request, debugging quickly becomes guesswork.
2. The real decision flow (simplified)
A request goes through Spring Security in this order:
- Security filter chain
- Authentication
- Authorization
- Controller
A failure at any of these steps results in either 401 or 403.
So the real question is:
Which step rejected my request?
3. Enable the ONE log that actually helps
Spring MVC (Servlet)
In most cases, this single line already tells you what went wrong:
logging.level.org.springframework.security=DEBUG
With this enabled, you’ll see:
- Which filter handled the request
- Whether authentication was attempted
- Why authorization failed
Typical messages look like:
Denied access because no granted authority matched
or
Failed to authenticate since token was expired
Spring WebFlux (Reactive)
Reactive security is much quieter by default.
Enable both of these:
logging.level.org.springframework.security=DEBUG
logging.level.org.springframework.security.web.server=DEBUG
Without them, you often only see:
403 Forbidden
with no explanation at all.
4. Common 401 root causes (not the obvious ones)
Token exists, but is rejected
- Token expired
- Signature mismatch
- Wrong issuer or audience
- Clock skew issues
Debug tip:
Log the decoded JWT before authentication fails.
Authentication filter never runs
This is more common than people expect:
- Wrong request matcher
- Security chain order issue
- Missing
BearerTokenAuthenticationFilter
Result: Spring treats the request as anonymous → 401
5. Common 403 root causes (the tricky ones)
Role exists, but doesn’t match
Example:
.hasRole("ADMIN")
But the token contains:
ROLE_ADMIN
or:
admin
Spring Security is very strict about role and authority matching.
Authorities not mapped correctly (JWT)
If you don’t explicitly map JWT claims to authorities:
- Authentication succeeds
- Authorization silently fails
Result:
- ✅ authenticated
- ❌ authorized → 403
6. Why Reactive (WebFlux) feels harder to debug
Two main reasons:
- No thread-local security context
- Errors happen inside reactive operators
By the time you see the response, the original cause is often already swallowed.
You must log earlier, inside:
- Authentication converter
- Authorization manager
7. My personal debugging checklist
When I see 401 or 403, I always ask:
- Did authentication run at all?
- Was the token parsed successfully?
- What authorities were extracted?
- Which rule denied access?
If I can’t answer any of these, the system is under-logged.
Final thoughts
401 and 403 are symptoms, not explanations.
Spring Security is deterministic — once you see where the decision was made, the fix is usually obvious.
If you’re debugging blind, the problem is almost always insufficient logging.
What’s next:
In upcoming posts, I’ll cover:
- Logging the exact reason Spring denied access
- JWT role-mapping mistakes that cause 403
- Security differences between MVC and WebFlux
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