A New Collection of Thoughtful Learning Apps — Now Available on iOS & Android

Image
I’m excited to share a set of mobile apps I’ve recently completed and published on both the Google Play Store and the Apple App Store. These apps are designed with a simple goal in mind: to make meaningful, structured content more accessible, whether you’re studying theology or improving your English vocabulary. 📱 Now Available on Both Platforms All apps are live and available for download: Google Play Developer Page: https://play.google.com/store/apps/dev?id=5835943159853189043 Apple App Store Developer Page: https://apps.apple.com/ca/developer/q-z-l-corp/id1888794100 📖 Theology & Confession Study Apps For those interested in Reformed theology and classical Christian teachings, I’ve developed a series of apps that present foundational texts in a clean, focused reading format: The Belgic Confession Canons of Dort Heidelberg Catechism Westminster Shorter Catechism Each app is designed to provide a distraction-free experience, making it easier to read, reflect, and revisit these im...

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

2026 Begins: Choosing to Stay on the Path as a Blogger

Health Checks and Scaling Strategies for Next.js in Kubernetes