Token is valid but still 403 — 5 real reasons in Spring Security

Token is valid but still 403 — 5 real reasons in Spring Security

This is one of the most confusing Spring Security problems:

“My JWT token is valid. It’s not expired. But I still get 403 Forbidden.”

At this point, authentication has already succeeded.

A 403 means only one thing:

You are authenticated — but not authorized.

This post walks through five real-world reasons why this happens, and how to identify the exact cause quickly.


Quick refresher: where 403 comes from

Spring Security processes a request like this:

  1. Authenticate the request
  2. Extract authorities
  3. Evaluate authorization rules

If step 1 fails → 401 If step 2 or 3 fails → 403

So when you see a 403, the question is not “is my token valid?” but:

What authorities does Spring think I have?


Reason 1: Role name mismatch (the most common)

You configure:

.hasRole("ADMIN")

But your token contains:

ROLE_ADMIN

or:

admin

Spring Security does exact string matching.

There is no automatic normalization.

If the strings don’t match exactly, access is denied.

Debug tip:
Log Authentication.getAuthorities() right before authorization.


Reason 2: Authorities were never mapped from the JWT

JWT validation can succeed even if no authorities are extracted.

Result:

  • ✅ token valid
  • ✅ authenticated
  • ❌ no roles → 403

This usually happens when:

  • Roles are stored in a custom claim
  • You rely on default converters

Spring does not guess where your roles are.

Debug tip:
If getAuthorities() is empty, the problem is mapping — not authorization rules.


Reason 3: hasRole vs hasAuthority confusion

These two are not the same:

.hasRole("ADMIN")
.hasAuthority("ADMIN")

Key difference:

  • hasRole("ADMIN") checks for ROLE_ADMIN
  • hasAuthority("ADMIN") checks for ADMIN

Mixing these up guarantees a 403.

Rule of thumb:
Pick one convention and use it everywhere.


Reason 4: Wrong security matcher — rule never applies

Your rule looks correct:

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

But the actual request is:

/v1/api/admin/users

Result:

  • Rule doesn’t match
  • Fallback rule applies
  • Access denied

Debug tip:
Enable DEBUG logs and check which matcher was evaluated.


Reason 5: Reactive (WebFlux) context loss

In WebFlux, security context is not thread-local.

If you:

  • Switch schedulers
  • Manually subscribe
  • Break the reactive chain

The authentication context may be lost.

Result:

  • Token validated
  • Authorities missing downstream
  • 403 returned

Debug tip:
Log authorities inside the reactive chain, not outside.


A simple 403 debugging checklist

When you hit a 403, check these in order:

  1. Is authentication successful?
  2. What authorities were extracted?
  3. Which rule was evaluated?
  4. Does the rule match the request?

If any step is unclear, your logs are insufficient.


Final thoughts

A valid token does not guarantee access.

403 errors are almost always about:

  • Role naming
  • Authority mapping
  • Matcher logic

Once you log the right place, the fix is usually obvious.


Up next:
How to log exactly why Spring Security denied access — without flooding your logs.

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