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

Fix “A problem occurred starting process 'command node'” in Android Studio for React Native

Fix up watchman issue with Ghost

Using Mutual TLS (mTLS) in Next.js (Server-Side Only)