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