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...

Debugging 403 Errors with Spring Cloud Gateway, WebFlux, PingFederate, and CORS

Debugging 403 Errors with Spring Cloud Gateway, WebFlux, PingFederate, and CORS



A real-world postmortem on a confusing 403 that turned out not to be OAuth, JWT, or Spring Security — but CORS.


Background

We recently ran into a frustrating issue while integrating:

  • Spring Cloud Gateway
  • Spring Boot WebFlux backend
  • PingFederate (OIDC / OAuth2)
  • A React frontend running locally (http://localhost:4200)

The request flow looked like this:

  1. User logs in via PingFederate (SSO)
  2. Frontend receives an access token
  3. Frontend sends POST requests → Gateway → Backend service

Everything worked perfectly when:

  • Calling the backend directly via Postman
  • Calling the backend directly without the gateway
  • Running everything locally

But once the request went through Spring Cloud Gateway in OpenShift, all POST requests failed with:

403 Forbidden

Why This Was Confusing

  • The same access token worked in Postman
  • GET requests succeeded, POST requests failed
  • No role or scope checks were configured
  • 401 vs 403 behavior differed across environments
  • JWT issuer, audience, and signature were correct

Everything initially pointed to an OAuth or PingFederate issue — but that assumption was wrong.


Key Observations That Cracked the Case

1. Postman Works, Browser Fails

The same request behaved differently depending on the client:

  • Postman → success
  • Browser (via gateway) → 403

This strongly suggested the problem was not token-related.


2. Adding an Origin Header Breaks Postman

When we manually added this header in Postman:

Origin: http://localhost:4200

The request immediately failed with:

403 Forbidden

This confirmed the issue was tied to CORS handling.


The Real Root Cause

CORS Enforcement by Spring Security

The backend is a Spring Boot WebFlux application configured as an OAuth2 Resource Server:

oauth2ResourceServer(oauth2 -> oauth2.jwt())

Once issuer-uri (PingFederate) is enabled, Spring Security applies stricter request validation. When an Origin header is present:

  • The request is treated as a CORS request
  • If no explicit CORS configuration exists, Spring Security rejects it
  • The result is a 403 Forbidden response

This explains why:

  • Postman works (no Origin header)
  • Browser + Gateway fails (Origin forwarded)
  • GET often works (no preflight)
  • POST fails (CORS enforced)

Why It Didn’t Happen with public-key-location

Using a legacy setup:

jwt:
  public-key-location: classpath:public.key

Spring Security behaved more permissively, and the missing CORS configuration went unnoticed.

Switching to:

jwt:
  issuer-uri: https://fed.test.example.com

activated proper OAuth2 behavior — including CORS enforcement.


The Final Fix (Local Development)

For local debugging only, we removed the Origin header in the Node gateway proxy:

if (process.env.NODE_ENV === "development" && headers.origin) {
  // Avoid Spring Security CORS rejection during local debugging
  delete headers.origin;
}

This allowed local development to proceed without weakening production security.


Recommended Production Fix

The correct long-term solution is to explicitly configure CORS in the WebFlux backend, allowing only trusted origins.


Takeaways

  • 403 does not always mean authorization failure
  • If Postman works and browsers fail, suspect CORS first
  • issuer-uri enables stricter (and correct) security behavior
  • Gateways often forward Origin headers — know how your backend handles them

This issue turned out to be a great reminder: security bugs often hide at the boundaries between systems.

❤️ Support This Blog


If this post helped you, you can support my writing with a small donation. Thank you for reading.


Comments

Popular Posts

Next.js + NextAuth.js — Keycloak SSO Integration

QZL Compare:免费在线文件与文件夹对比工具,无需安装,隐私安全