Add the cors-csrf module
This commit is contained in:
117
cors-csrf/README.md
Normal file
117
cors-csrf/README.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# `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**](https://ankurm.com/spring-boot-4-cors-csrf-samesite/)
|
||||
on ankurm.com.
|
||||
|
||||
Every broken state the article describes is a Spring profile on this one application, and every
|
||||
transcript under [`docs/output/`](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
|
||||
|
||||
```bash
|
||||
./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`](src/main/java/com/ankurm/cors/config/SecuritySourceConfig.java) | A bean named `corsConfigurationSource`. This one works |
|
||||
| `mvconly` | [`MvcOnlySecurityConfig`](src/main/java/com/ankurm/cors/config/MvcOnlySecurityConfig.java) | `addCorsMappings` and nothing else — preflight answered `401` |
|
||||
| `mvcbridge` | [`MvcBridgeSecurityConfig`](src/main/java/com/ankurm/cors/config/MvcBridgeSecurityConfig.java) | The same MVC config plus `.cors(withDefaults())` — and MVC's `max-age` default |
|
||||
| `misnamed` | [`MisnamedSourceConfig`](src/main/java/com/ankurm/cors/config/MisnamedSourceConfig.java) | Right type, wrong bean name — preflight answered `200` with no CORS headers |
|
||||
| `twosources` | [`TwoSourcesConfig`](src/main/java/com/ankurm/cors/config/TwoSourcesConfig.java) | Two sources. The docs say CORS is not configured; it is, and the name decides |
|
||||
| `wildcard` | [`WildcardCredentialsConfig`](src/main/java/com/ankurm/cors/config/WildcardCredentialsConfig.java) | `allowedOrigins("*")` with credentials — fails on the request, surfaces as `401` |
|
||||
| `csrfnaive` | [`CsrfNaiveConfig`](src/main/java/com/ankurm/cors/config/CsrfNaiveConfig.java) | The pre-6.0 SPA recipe: no cookie on the GET, 403 on the POST |
|
||||
| `csrfspa` | [`CsrfSpaConfig`](src/main/java/com/ankurm/cors/config/CsrfSpaConfig.java) | `csrf.spa()`, and why the cookie now arrives on the bootstrap GET |
|
||||
| `spaorder` | [`CsrfSpaOrderConfig`](src/main/java/com/ankurm/cors/config/CsrfSpaOrderConfig.java) | `csrfTokenRepository(..)` before `spa()` — silently discarded |
|
||||
| `crosssite` | [`CsrfSpaCrossSiteConfig`](src/main/java/com/ankurm/cors/config/CsrfSpaCrossSiteConfig.java) | `SameSite=None; Secure` on the CSRF cookie |
|
||||
| `errorpermit` | [`ErrorDispatchConfig`](src/main/java/com/ankurm/cors/config/ErrorDispatchConfig.java) | 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](docs/01-two-layers.md) | Two layers, one word — why MVC CORS does not fix a security-layer rejection |
|
||||
| [02](docs/02-who-resolves-the-source.md) | Who resolves the `CorsConfigurationSource` — by type, then by **name** |
|
||||
| [03](docs/03-three-identical-403s.md) | The three identical 403s, and reading a status code as a diagnosis |
|
||||
| [04](docs/04-preflight-handlers.md) | `PreFlightRequestHandler`, and the wildcard that is not allowed |
|
||||
| [05](docs/05-the-error-dispatch.md) | The `/error` dispatch, or why your 403 arrives as a 401 |
|
||||
| [06](docs/06-csrf-for-spas.md) | CSRF for SPAs, and what `spa()` actually assigns |
|
||||
| [07](docs/07-samesite.md) | SameSite, `Secure`, and the cookie that is never stored |
|
||||
| [08](docs/08-debugging-recipes.md) | Debugging recipes |
|
||||
|
||||
## Captured output
|
||||
|
||||
| File | |
|
||||
|---|---|
|
||||
| [01-mvc-only.txt](docs/output/01-mvc-only.txt) | An 11-filter chain with no `CorsFilter`, and a `401` preflight |
|
||||
| [02-mvc-bridge.txt](docs/output/02-mvc-bridge.txt) | The same app plus one line — `200`, and `Access-Control-Max-Age: 1800` |
|
||||
| [03-security-source.txt](docs/output/03-security-source.txt) | A `corsConfigurationSource` bean — `200`, and **no** max-age |
|
||||
| [04-three-identical-403s.txt](docs/output/04-three-identical-403s.txt) | Three rejections, one response, three DEBUG lines |
|
||||
| [05-misnamed-bean.txt](docs/output/05-misnamed-bean.txt) | `200` with no CORS headers, and `Skip: no CORS configuration has been provided` |
|
||||
| [06-two-sources.txt](docs/output/06-two-sources.txt) | Two sources, CORS configured anyway, the named bean winning |
|
||||
| [07-wildcard-credentials.txt](docs/output/07-wildcard-credentials.txt) | The `IllegalArgumentException`, arriving as a `401` |
|
||||
| [08-csrf-naive.txt](docs/output/08-csrf-naive.txt) | No cookie on the GET, then two rejected POSTs |
|
||||
| [09-error-dispatch.txt](docs/output/09-error-dispatch.txt) | The same failure with `/error` permitted — the `403` reappears |
|
||||
| [10-csrf-spa.txt](docs/output/10-csrf-spa.txt) | `csrf.spa()`: cookie on the GET, raw value accepted in the header |
|
||||
| [11-spa-ordering.txt](docs/output/11-spa-ordering.txt) | A custom repository silently discarded by `spa()` |
|
||||
| [12-samesite.txt](docs/output/12-samesite.txt) | Four sets of real `Set-Cookie` headers, run through the RFC rules |
|
||||
| [13-tests.txt](docs/output/13-tests.txt) | `mvn test` |
|
||||
|
||||
## Related modules
|
||||
|
||||
- [`filter-chain/`](../filter-chain/README.md) — the order numbers this module keeps citing, and the `/error` dispatch in full
|
||||
- [`context-propagation/`](../context-propagation/README.md) — whether the `SecurityContext` survives leaving the request thread
|
||||
- [`method-security/`](../method-security/README.md) — reading that context back
|
||||
Reference in New Issue
Block a user