1
0
Files
spring-security-demo/cors-csrf
2026-08-28 10:16:21 +05:30
..
2026-08-28 10:16:21 +05:30
2026-08-28 10:16:21 +05:30
2026-08-28 10:16:21 +05:30
2026-08-28 10:16:21 +05:30
2026-08-28 10:16:21 +05:30

cors-csrf — CORS, CSRF and SameSite, reproduced state by state

Companion project for CORS, CSRF and SameSite in Spring Boot 4: The Three Settings Everyone Gets Wrong on ankurm.com.

Every broken state the article describes is a Spring profile on this one application, and every transcript under docs/output/ was produced by running it. No browser is required: a CORS preflight is an OPTIONS carrying two headers, and curl sends those.

Versions

Version Notes
JDK 25 (Temurin 25.0.4.1+1) current LTS
Spring Boot 4.1.1 inherited as parent, so everything below is Boot-managed
Spring Framework 7.0.9 CorsFilter, DefaultCorsProcessor, CorsConfiguration
Spring Security 7.1.1 CorsConfigurer, CsrfConfigurer.spa()
Tomcat 11.0.24
JUnit Jupiter / AssertJ Boot-managed 23 assertions

Versions were read from repo1.maven.org/.../maven-metadata.xml, not from release announcements. Note that maven-metadata.xml's own <release> element pointed at 4.2.0-M1 while this was written; a milestone is not a release.

Quickstart

./scripts/run.sh securitysource        # the configuration that works
./scripts/preflight.sh                 # one preflight, headers printed

./scripts/run.sh mvconly               # the same app with CORS on the MVC layer only
./scripts/preflight.sh                 # → 401

./scripts/run-all.sh                   # every scenario, regenerating docs/output/
mvn test                               # just the 23 assertions
./scripts/stop.sh

The user is alice / password.

Profiles

Profile Configuration Shows
securitysource (default) SecuritySourceConfig A bean named corsConfigurationSource. This one works
mvconly MvcOnlySecurityConfig addCorsMappings and nothing else — preflight answered 401
mvcbridge MvcBridgeSecurityConfig The same MVC config plus .cors(withDefaults()) — and MVC's max-age default
misnamed MisnamedSourceConfig Right type, wrong bean name — preflight answered 200 with no CORS headers
twosources TwoSourcesConfig Two sources. The docs say CORS is not configured; it is, and the name decides
wildcard WildcardCredentialsConfig allowedOrigins("*") with credentials — fails on the request, surfaces as 401
csrfnaive CsrfNaiveConfig The pre-6.0 SPA recipe: no cookie on the GET, 403 on the POST
csrfspa CsrfSpaConfig csrf.spa(), and why the cookie now arrives on the bootstrap GET
spaorder CsrfSpaOrderConfig csrfTokenRepository(..) before spa() — silently discarded
crosssite CsrfSpaCrossSiteConfig SameSite=None; Secure on the CSRF cookie
errorpermit ErrorDispatchConfig Add-on. Combine with any other profile to see the status code the /error dispatch was hiding

Add-on profiles combine: ./scripts/run.sh csrfnaive,errorpermit.

Three environment settings change behaviour rather than configuration:

Setting Effect
SESSION_SAME_SITE / SESSION_SECURE The session cookie's attributes, written straight through by Boot
JVM_ARGS=-DOMIT_SECURE=true Under crosssite, emit SameSite=None without Secure
CORS_LOG_LEVEL / CSRF_LOG_LEVEL DEBUG turns on the two log categories that answer almost every question here

Endpoints

Endpoint Purpose
GET/POST /api/data The API the imaginary SPA calls
GET /api/whoami Who the request authenticated as
GET /api/boom Throws, so you can watch the error dispatch
GET /diag/chain The filters FilterChainProxy actually holds
GET /diag/cors-sources Every CorsConfigurationSource bean, by name
GET /diag/cookie-spec?h=…&secure=… Real Set-Cookie headers run through the RFC 6265bis rules

The /diag/** endpoints are permitted without authentication so the scripts can read them. Delete them before shipping.

Documentation

Chapter
01 Two layers, one word — why MVC CORS does not fix a security-layer rejection
02 Who resolves the CorsConfigurationSource — by type, then by name
03 The three identical 403s, and reading a status code as a diagnosis
04 PreFlightRequestHandler, and the wildcard that is not allowed
05 The /error dispatch, or why your 403 arrives as a 401
06 CSRF for SPAs, and what spa() actually assigns
07 SameSite, Secure, and the cookie that is never stored
08 Debugging recipes

Captured output

File
01-mvc-only.txt An 11-filter chain with no CorsFilter, and a 401 preflight
02-mvc-bridge.txt The same app plus one line — 200, and Access-Control-Max-Age: 1800
03-security-source.txt A corsConfigurationSource bean — 200, and no max-age
04-three-identical-403s.txt Three rejections, one response, three DEBUG lines
05-misnamed-bean.txt 200 with no CORS headers, and Skip: no CORS configuration has been provided
06-two-sources.txt Two sources, CORS configured anyway, the named bean winning
07-wildcard-credentials.txt The IllegalArgumentException, arriving as a 401
08-csrf-naive.txt No cookie on the GET, then two rejected POSTs
09-error-dispatch.txt The same failure with /error permitted — the 403 reappears
10-csrf-spa.txt csrf.spa(): cookie on the GET, raw value accepted in the header
11-spa-ordering.txt A custom repository silently discarded by spa()
12-samesite.txt Four sets of real Set-Cookie headers, run through the RFC rules
13-tests.txt mvn test