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:
- User logs in via PingFederate (SSO)
- Frontend receives an access token
- 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
Post a Comment