Add the cors-csrf module
This commit is contained in:
82
cors-csrf/docs/01-two-layers.md
Normal file
82
cors-csrf/docs/01-two-layers.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# 1. Two layers, one word
|
||||
|
||||
*Next: [2. Who resolves the CorsConfigurationSource](02-who-resolves-the-source.md)*
|
||||
|
||||
A Spring Boot application can be told about CORS in two entirely separate places, and the two
|
||||
places do not talk to each other unless you make them.
|
||||
|
||||
**The MVC layer.** `WebMvcConfigurer.addCorsMappings(..)` and `@CrossOrigin` register a
|
||||
`CorsConfiguration` with Spring MVC's handler mappings. It is consulted inside
|
||||
`DispatcherServlet`, when the request is being matched to a handler method.
|
||||
|
||||
**The security layer.** `HttpSecurity.cors(..)` puts a `org.springframework.web.filter.CorsFilter`
|
||||
into the security filter chain. It runs at order **1000** — between `HeaderWriterFilter`
|
||||
(900) and `CsrfFilter` (1100), and a long way above `AuthorizationFilter` (4200).
|
||||
|
||||
The security filter chain runs to completion before `DispatcherServlet` is ever entered. So if
|
||||
the security chain rejects a request, MVC's CORS configuration is not merely ignored: the code
|
||||
that reads it never executes.
|
||||
|
||||
## Why that specifically breaks preflights
|
||||
|
||||
A CORS preflight is not a special protocol. It is an ordinary `OPTIONS` request carrying two
|
||||
headers:
|
||||
|
||||
```
|
||||
OPTIONS /api/data HTTP/1.1
|
||||
Origin: https://spa.example.com
|
||||
Access-Control-Request-Method: POST
|
||||
```
|
||||
|
||||
The browser sends it **without credentials** — no cookies, no `Authorization` header, by
|
||||
design. Against `anyRequest().authenticated()` that request is anonymous, and anonymous requests
|
||||
are denied. The rejection happens at order 4200, three thousand two hundred slots before
|
||||
`DispatcherServlet` and about four thousand before your `addCorsMappings` call matters.
|
||||
|
||||
[`docs/output/01-mvc-only.txt`](output/01-mvc-only.txt) is that state: an eleven-filter chain
|
||||
with no `CorsFilter` in it, and a preflight answered `401`.
|
||||
|
||||
## The fix is one line, and it is not on the MVC layer
|
||||
|
||||
```java
|
||||
http.cors(Customizer.withDefaults())
|
||||
```
|
||||
|
||||
That is [`MvcBridgeSecurityConfig`](../src/main/java/com/ankurm/cors/config/MvcBridgeSecurityConfig.java),
|
||||
and [`docs/output/02-mvc-bridge.txt`](output/02-mvc-bridge.txt) is the identical application
|
||||
answering `200`. The MVC configuration was fine all along. Nothing was reading it.
|
||||
|
||||
`CorsFilter` short-circuits every preflight it sees:
|
||||
|
||||
```java
|
||||
boolean isValid = this.processor.processRequest(corsConfiguration, request, response);
|
||||
if (!isValid || CorsUtils.isPreFlightRequest(request)) {
|
||||
return; // the chain stops here
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
```
|
||||
|
||||
Read that `if` carefully, because chapter 2 turns on it: the filter returns on **every**
|
||||
preflight, whether or not it found a configuration to apply.
|
||||
|
||||
## One thing that changes when you move the configuration
|
||||
|
||||
Moving CORS from `addCorsMappings` to a `CorsConfigurationSource` bean is not a pure
|
||||
relocation. Compare the two transcripts:
|
||||
|
||||
```
|
||||
02-mvc-bridge.txt Access-Control-Max-Age: 1800
|
||||
03-security-source.txt (nothing)
|
||||
```
|
||||
|
||||
`CorsRegistration` — the builder behind `addCorsMappings` — defaults `maxAge` to
|
||||
1800 seconds. A bare `CorsConfiguration` leaves it `null`, and a preflight response with no
|
||||
`Access-Control-Max-Age` is not cached, so the browser preflights **every single cross-origin
|
||||
call**. Two round trips instead of one, forever, with nothing in any log to suggest it.
|
||||
|
||||
```java
|
||||
configuration.setMaxAge(1800L);
|
||||
```
|
||||
|
||||
---
|
||||
*Next: [2. Who resolves the CorsConfigurationSource](02-who-resolves-the-source.md)*
|
||||
99
cors-csrf/docs/02-who-resolves-the-source.md
Normal file
99
cors-csrf/docs/02-who-resolves-the-source.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# 2. Who resolves the `CorsConfigurationSource`
|
||||
|
||||
*Prev: [1. Two layers, one word](01-two-layers.md) · Next: [3. The three identical 403s](03-three-identical-403s.md)*
|
||||
|
||||
There are two lookups involved in getting a CORS configuration into the filter chain, and they
|
||||
disagree about what they are looking for. One searches by **type**. The other searches by
|
||||
**name**.
|
||||
|
||||
## Lookup 1: should the CORS configurer run at all?
|
||||
|
||||
`HttpSecurityConfiguration.applyCorsIfAvailable(HttpSecurity)`, disassembled from
|
||||
`spring-security-config` 7.1.1:
|
||||
|
||||
```
|
||||
4: ldc // class org/springframework/web/cors/UrlBasedCorsConfigurationSource
|
||||
6: invokeinterface // ApplicationContext.getBeanNamesForType:(Ljava/lang/Class;)[Ljava/lang/String;
|
||||
11: arraylength
|
||||
12: ifle 23
|
||||
19: invokevirtual // HttpSecurity.cors:(Customizer)HttpSecurity
|
||||
23: return
|
||||
```
|
||||
|
||||
By type, and the test is `ifle` — "branch if less than or equal to zero". One bean is
|
||||
enough. So is five.
|
||||
|
||||
> The reference documentation says: *"If you have more than one `CorsConfigurationSource` bean,
|
||||
> Spring Security won't automatically configure CORS support for you, because it cannot decide
|
||||
> which one to use."* That is not what 7.1.1 does.
|
||||
> [`docs/output/06-two-sources.txt`](output/06-two-sources.txt) has two such beans, CORS
|
||||
> configured, and one of them serving traffic.
|
||||
|
||||
## Lookup 2: which source does the configurer use?
|
||||
|
||||
`CorsConfigurer.getCorsConfigurationSource(ApplicationContext)`:
|
||||
|
||||
```
|
||||
7: ldc // String corsConfigurationSource
|
||||
9: invokeinterface // ApplicationContext.containsBeanDefinition:(Ljava/lang/String;)Z
|
||||
20: ldc // String corsConfigurationSource
|
||||
24: invokeinterface // ApplicationContext.getBean:(String,Class)Object
|
||||
34: invokestatic // MvcCorsFilter.getMvcCorsConfigurationSource:(ApplicationContext)CorsConfigurationSource
|
||||
```
|
||||
|
||||
By name. The literal string `corsConfigurationSource`. If no bean definition carries that name,
|
||||
it falls through to Spring MVC's registrations. (There is a similar name check first, for a
|
||||
`CorsFilter` bean named `corsFilter`.)
|
||||
|
||||
## The gap between them
|
||||
|
||||
Name a `UrlBasedCorsConfigurationSource` bean anything other than `corsConfigurationSource` and
|
||||
you land between the two lookups: CORS is switched **on** by the type lookup, and the
|
||||
configuration you wrote is **ignored** by the name lookup.
|
||||
|
||||
That is the `misnamed` profile, and it produces the worst diagnostic in this whole subject:
|
||||
|
||||
```
|
||||
HTTP/1.1 200
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
...
|
||||
```
|
||||
|
||||
Two hundred. No `Access-Control-Allow-Origin`. The browser blocks the request and reports a CORS
|
||||
error; your access log shows a successful `OPTIONS`; nothing anywhere is red.
|
||||
|
||||
Why 200 rather than the 401 from chapter 1? Because `CorsFilter` is now in the chain, and its
|
||||
`if (!isValid || isPreFlightRequest(request)) return;` fires on the second clause. The request
|
||||
never reaches `AuthorizationFilter`. The only trace is one DEBUG line:
|
||||
|
||||
```
|
||||
o.s.web.cors.DefaultCorsProcessor : Skip: no CORS configuration has been provided
|
||||
```
|
||||
|
||||
Full transcript: [`docs/output/05-misnamed-bean.txt`](output/05-misnamed-bean.txt).
|
||||
|
||||
## What is in the context that you did not put there
|
||||
|
||||
`/diag/cors-sources` on a stock Boot web application:
|
||||
|
||||
```json
|
||||
{ "corsConfigurationSourceBeans": { "mvcHandlerMappingIntrospector": "HandlerMappingIntrospector" } }
|
||||
```
|
||||
|
||||
`HandlerMappingIntrospector` implements `CorsConfigurationSource`. It is always there, it is not
|
||||
a `UrlBasedCorsConfigurationSource`, and it is the object the MVC fallback returns. That is why
|
||||
the fallback path never throws in a normal application — and why the failure is silent
|
||||
rather than loud.
|
||||
|
||||
## Rules that follow
|
||||
|
||||
- Name the bean `corsConfigurationSource`. Exactly that.
|
||||
- If you want several, pass them per chain with `.cors(c -> c.configurationSource(..))`, which
|
||||
bypasses both lookups.
|
||||
- `NoSuchBeanDefinitionException: Failed to find a bean that implements
|
||||
\`CorsConfigurationSource\`` names three fixes and does not mention the fourth one, which is
|
||||
usually the right one: rename your bean.
|
||||
|
||||
---
|
||||
*Prev: [1. Two layers, one word](01-two-layers.md) · Next: [3. The three identical 403s](03-three-identical-403s.md)*
|
||||
79
cors-csrf/docs/03-three-identical-403s.md
Normal file
79
cors-csrf/docs/03-three-identical-403s.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# 3. The three identical 403s
|
||||
|
||||
*Prev: [2. Who resolves the CorsConfigurationSource](02-who-resolves-the-source.md) · Next: [4. Preflight handlers](04-preflight-handlers.md)*
|
||||
|
||||
`DefaultCorsProcessor` runs three checks on a preflight, in order: origin, method, request
|
||||
headers. All three failures produce the same thing.
|
||||
|
||||
```
|
||||
HTTP/1.1 403
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
|
||||
Invalid CORS request
|
||||
```
|
||||
|
||||
Same status, same body, byte for byte, no `Access-Control-*` header to distinguish them. The
|
||||
assertion in `CorsContractTests.threeRejectionsLookIdentical` compares the three bodies for
|
||||
equality, so if a future version starts distinguishing them, that test fails.
|
||||
|
||||
The only place the difference exists is a DEBUG log line, and the three are worth memorising
|
||||
because they are the fastest CORS diagnosis available:
|
||||
|
||||
```
|
||||
o.s.web.cors.DefaultCorsProcessor : Reject: 'https://evil.example.com' origin is not allowed
|
||||
o.s.web.cors.DefaultCorsProcessor : Reject: HTTP 'DELETE' is not allowed
|
||||
o.s.web.cors.DefaultCorsProcessor : Reject: headers '[authorization]' are not allowed
|
||||
```
|
||||
|
||||
Turn them on with:
|
||||
|
||||
```yaml
|
||||
logging.level.org.springframework.web.cors: DEBUG
|
||||
```
|
||||
|
||||
The complete set of messages, read out of the class's constant pool, is five:
|
||||
|
||||
| Message | Meaning |
|
||||
|---|---|
|
||||
| `Skip: no CORS configuration has been provided` | The source returned `null` for this path — chapter 2 |
|
||||
| `Skip: response already contains "Access-Control-Allow-Origin"` | Something upstream already handled it |
|
||||
| `Reject: origin is malformed` | The `Origin` header did not parse |
|
||||
| `Reject: '…' origin is not allowed` | |
|
||||
| `Reject: HTTP '…' is not allowed` | |
|
||||
| `Reject: headers '[…]' are not allowed` | |
|
||||
|
||||
## Status codes, and what each one means
|
||||
|
||||
Collecting the states this module reproduces:
|
||||
|
||||
| What you see | What it means |
|
||||
|---|---|
|
||||
| `401`/`403`, no `Access-Control-*` at all | No `CorsFilter` in the chain. The preflight was judged by authorization — chapter 1 |
|
||||
| `200`, no `Access-Control-*` | `CorsFilter` is present and found no configuration for this path — chapter 2 |
|
||||
| `403`, `Invalid CORS request` | `CorsFilter` is present and rejected origin, method or headers — this chapter |
|
||||
| `200` with `Access-Control-Allow-Origin` | It worked |
|
||||
| `404`, no `Access-Control-*` | The path is outside the pattern you registered. Common with `/api/**` versus a mis-typed URL |
|
||||
|
||||
The browser reports the same "blocked by CORS policy" for the first four rows. Two of them are
|
||||
not CORS problems.
|
||||
|
||||
## What a **simple** request does
|
||||
|
||||
Only preflighted requests get intercepted. A simple `GET` runs the whole chain, so an
|
||||
unauthenticated one returns 401 — **carrying** the CORS header, because `CorsFilter` at
|
||||
1000 already wrote it before `AuthorizationFilter` at 4200 rejected the request:
|
||||
|
||||
```
|
||||
HTTP/1.1 401
|
||||
Access-Control-Allow-Origin: https://spa.example.com
|
||||
Access-Control-Allow-Credentials: true
|
||||
```
|
||||
|
||||
That is the good case: the SPA's `fetch` resolves and the code can read `response.status`. It is
|
||||
also the reason "my POST fails but my GET returns a readable 401" is a coherent bug report and
|
||||
not a contradiction.
|
||||
|
||||
---
|
||||
*Prev: [2. Who resolves the CorsConfigurationSource](02-who-resolves-the-source.md) · Next: [4. Preflight handlers](04-preflight-handlers.md)*
|
||||
73
cors-csrf/docs/04-preflight-handlers.md
Normal file
73
cors-csrf/docs/04-preflight-handlers.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# 4. `PreFlightRequestHandler`, and the wildcard that is not allowed
|
||||
|
||||
*Prev: [3. The three identical 403s](03-three-identical-403s.md) · Next: [5. The /error dispatch](05-the-error-dispatch.md)*
|
||||
|
||||
## `preFlightRequestHandler`
|
||||
|
||||
`CorsConfigurer` in 7.1.1 has a second setter beside `configurationSource`:
|
||||
|
||||
```java
|
||||
public CorsConfigurer<H> preFlightRequestHandler(PreFlightRequestHandler handler);
|
||||
```
|
||||
|
||||
When one is selected, Spring Security registers Spring Framework's `PreFlightRequestFilter`
|
||||
**before** `CorsFilter` in the chain — `addFilterBefore(.., CorsFilter.class)`, which lands
|
||||
it at 999. It is for applications that answer preflights from their own routing rather than from
|
||||
a `CorsConfiguration`, and it is the hook `WebFlux`-style functional routing and gateway-shaped
|
||||
applications want.
|
||||
|
||||
The handler is picked up either from the `preFlightRequestHandler(..)` call or from a
|
||||
`PreFlightRequestHandler` bean, and only when no `CorsConfigurationSource` or `CorsFilter` was
|
||||
chosen for that chain. Configuring both raises, at startup:
|
||||
|
||||
```
|
||||
java.lang.IllegalStateException: Cannot configure both a CorsConfigurationSource and a
|
||||
PreFlightRequestHandler on CorsConfigurer
|
||||
```
|
||||
|
||||
That string is in `CorsConfigurer.configure`'s constant pool; it is a hard failure, not a
|
||||
warning.
|
||||
|
||||
## `allowedOrigins("*")` with `allowCredentials(true)`
|
||||
|
||||
The Fetch standard forbids answering a credentialed request with
|
||||
`Access-Control-Allow-Origin: *`. Spring enforces it — but not where you would expect.
|
||||
|
||||
The configuration builds. The context starts. The check happens on the first request, inside
|
||||
`CorsConfiguration.validateAllowCredentials`, reached from `checkOrigin`:
|
||||
|
||||
```
|
||||
java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain
|
||||
the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response
|
||||
header. To allow credentials to a set of origins, list them explicitly or consider using
|
||||
"allowedOriginPatterns" instead.
|
||||
at org.springframework.web.cors.CorsConfiguration.validateAllowCredentials(CorsConfiguration.java:552)
|
||||
at org.springframework.web.cors.CorsConfiguration.checkOrigin(CorsConfiguration.java:678)
|
||||
at org.springframework.web.cors.DefaultCorsProcessor.checkOrigin(DefaultCorsProcessor.java:193)
|
||||
```
|
||||
|
||||
And here is the part worth knowing: **the client does not get a 500.** It gets a `401`.
|
||||
[`docs/output/07-wildcard-credentials.txt`](output/07-wildcard-credentials.txt) shows a request
|
||||
with entirely correct Basic credentials answered `401 WWW-Authenticate: Basic`. Chapter 5 is why.
|
||||
|
||||
The fix is `setAllowedOriginPatterns(..)`, which echoes the request's own origin back instead of
|
||||
a literal asterisk, and is therefore legal with credentials:
|
||||
|
||||
```java
|
||||
configuration.setAllowedOriginPatterns(List.of("https://*.example.com"));
|
||||
configuration.setAllowCredentials(true);
|
||||
```
|
||||
|
||||
`allowedHeaders("*")` and `allowedMethods("*")` are unaffected — the prohibition is
|
||||
specific to the origin, because that is the one that gets reflected into a header the browser
|
||||
uses to decide whether the caller may read a credentialed response.
|
||||
|
||||
## Private Network Access
|
||||
|
||||
`DefaultCorsProcessor` in Spring Framework 7.0.9 also handles
|
||||
`Access-Control-Request-Private-Network` / `Access-Control-Allow-Private-Network` — both
|
||||
strings are in the class. If a public-origin SPA calls something on a private address, that is
|
||||
the header pair to look for, and `CorsConfiguration.setAllowPrivateNetwork(true)` is the switch.
|
||||
|
||||
---
|
||||
*Prev: [3. The three identical 403s](03-three-identical-403s.md) · Next: [5. The /error dispatch](05-the-error-dispatch.md)*
|
||||
79
cors-csrf/docs/05-the-error-dispatch.md
Normal file
79
cors-csrf/docs/05-the-error-dispatch.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# 5. The `/error` dispatch, or why your 403 arrives as a 401
|
||||
|
||||
*Prev: [4. Preflight handlers](04-preflight-handlers.md) · Next: [6. CSRF for SPAs](06-csrf-for-spas.md)*
|
||||
|
||||
Two of the failures in this module — the wildcard/credentials clash in chapter 4 and the
|
||||
CSRF rejection in chapter 6 — produce a status code that has nothing to do with what went
|
||||
wrong. Both have the same cause, and it is worth understanding once because it explains a large
|
||||
fraction of confusing Spring Security bug reports.
|
||||
|
||||
## The mechanism
|
||||
|
||||
1. Something inside the chain calls `response.sendError(403, ..)` (that is what
|
||||
`AccessDeniedHandlerImpl` does) or lets an exception escape `FilterChainProxy`.
|
||||
2. The servlet container does not write that response. It **re-dispatches** the request
|
||||
internally to `/error`, with `DispatcherType.ERROR`.
|
||||
3. Spring Boot registers `springSecurityFilterChain` for **every** dispatcher type:
|
||||
`SecurityFilterProperties.dispatcherTypes` defaults to `EnumSet.allOf(DispatcherType.class)`.
|
||||
So the whole security chain runs again on that dispatch.
|
||||
4. On the second pass, the filters that extend `OncePerRequestFilter` skip themselves —
|
||||
`shouldNotFilterErrorDispatch()` defaults to `true`. `BasicAuthenticationFilter` is one of
|
||||
them. The credential is never re-read.
|
||||
5. The filters that extend `GenericFilterBean` do run. `AuthorizationFilter` is one of them.
|
||||
6. So the second pass is **authorized but not authenticated**: `AuthorizationFilter` evaluates
|
||||
`/error` against `anyRequest().authenticated()`, finds an anonymous principal, and denies it.
|
||||
7. The 401 from step 6 is what reaches the client. The 403 from step 1 is gone.
|
||||
|
||||
The mechanism is set out in full in
|
||||
[The Spring Security Filter Chain Explained](https://ankurm.com/spring-security-filter-chain-explained/);
|
||||
this chapter is what it looks like when it lands on a CORS or CSRF problem.
|
||||
|
||||
## Proving it in one diff
|
||||
|
||||
The `errorpermit` profile adds one filter chain, `@Order(0)`, matching `/error` and permitting
|
||||
everything. Nothing else changes.
|
||||
|
||||
```
|
||||
./scripts/run.sh csrfnaive POST → 401, empty body, WWW-Authenticate: Basic
|
||||
./scripts/run.sh csrfnaive,errorpermit POST → 403, {"status":403,"error":"Forbidden", ...}
|
||||
```
|
||||
|
||||
[`docs/output/08-csrf-naive.txt`](output/08-csrf-naive.txt) against
|
||||
[`docs/output/09-error-dispatch.txt`](output/09-error-dispatch.txt).
|
||||
|
||||
## What to do about it
|
||||
|
||||
Permit `/error`. It is not a hole: the error page is generated from an attribute the container
|
||||
set, and an unauthenticated request cannot reach it except through a dispatch the container
|
||||
initiated.
|
||||
|
||||
```java
|
||||
@Bean
|
||||
@Order(0)
|
||||
SecurityFilterChain errorChain(HttpSecurity http) throws Exception {
|
||||
return http.securityMatcher("/error")
|
||||
.authorizeHttpRequests((auth) -> auth.anyRequest().permitAll())
|
||||
.csrf(CsrfConfigurer::disable)
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
The alternative is to narrow the dispatcher types so the chain does not run on the error
|
||||
dispatch at all:
|
||||
|
||||
```yaml
|
||||
spring.security.filter.dispatcher-types: request
|
||||
```
|
||||
|
||||
That one is broader in effect than it looks; prefer the `/error` chain unless you have a
|
||||
specific reason.
|
||||
|
||||
## Why this matters more for a SPA than for a server-rendered app
|
||||
|
||||
A browser will not let a SPA read a cross-origin response unless the CORS headers are present.
|
||||
When the 403 is replaced by a 401 written on a dispatch where `CorsFilter` may or may not have
|
||||
re-run, what the developer sees in the console is neither "403" nor "CSRF"; it is
|
||||
`TypeError: Failed to fetch`. Every layer of the stack has thrown away the actual cause by then.
|
||||
|
||||
---
|
||||
*Prev: [4. Preflight handlers](04-preflight-handlers.md) · Next: [6. CSRF for SPAs](06-csrf-for-spas.md)*
|
||||
114
cors-csrf/docs/06-csrf-for-spas.md
Normal file
114
cors-csrf/docs/06-csrf-for-spas.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# 6. CSRF for SPAs, and what `spa()` actually assigns
|
||||
|
||||
*Prev: [5. The /error dispatch](05-the-error-dispatch.md) · Next: [7. SameSite](07-samesite.md)*
|
||||
|
||||
## The recipe that stopped working in 6.0
|
||||
|
||||
```java
|
||||
http.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
|
||||
```
|
||||
|
||||
Every SPA tutorial written before Spring Security 6 ends with that line. Three separate things go
|
||||
wrong with it now, and [`docs/output/08-csrf-naive.txt`](output/08-csrf-naive.txt) walks through
|
||||
all three in one transcript.
|
||||
|
||||
**1. The bootstrap GET sets no cookie.** Since 6.0 the token is *deferred*: `CsrfFilter` puts a
|
||||
`Supplier<CsrfToken>` in a request attribute and only materialises it if something dereferences
|
||||
it. A `GET` on a JSON endpoint dereferences nothing. So the SPA starts up, sees no `XSRF-TOKEN`
|
||||
cookie, and its first mutating request has nothing to send.
|
||||
|
||||
**2. Sending the raw cookie value fails.** The default handler is
|
||||
`XorCsrfTokenRequestAttributeHandler`, added as a BREACH mitigation. It expects the value in the
|
||||
header to be XOR-masked. `CookieCsrfTokenRepository` writes the **raw** token into the cookie.
|
||||
So the SPA reads a raw value, sends a raw value, and the handler tries to unmask it. 403.
|
||||
|
||||
**3. The cookie is not sent cross-site anyway.** Chapter 7.
|
||||
|
||||
## What `spa()` is
|
||||
|
||||
Spring Security 7.0 added `CsrfConfigurer.spa()`. Its entire bytecode:
|
||||
|
||||
```
|
||||
0: aload_0
|
||||
1: invokestatic // CookieCsrfTokenRepository.withHttpOnlyFalse()
|
||||
4: putfield // Field csrfTokenRepository
|
||||
7: aload_0
|
||||
8: new // class CsrfConfigurer$SpaCsrfTokenRequestHandler
|
||||
12: invokespecial // <init>
|
||||
15: putfield // Field requestHandler
|
||||
18: aload_0
|
||||
19: areturn
|
||||
```
|
||||
|
||||
Two unconditional assignments. `SpaCsrfTokenRequestHandler` holds two delegates:
|
||||
|
||||
```java
|
||||
private final CsrfTokenRequestAttributeHandler plain = new CsrfTokenRequestAttributeHandler();
|
||||
private final CsrfTokenRequestAttributeHandler xor = new XorCsrfTokenRequestAttributeHandler();
|
||||
// constructor: xor.setCsrfRequestAttributeName(null);
|
||||
```
|
||||
|
||||
`handle(..)` always delegates to `xor`. `resolveCsrfTokenValue(..)` picks `plain` when the
|
||||
request carries the header and `xor` otherwise:
|
||||
|
||||
```java
|
||||
String headerValue = request.getHeader(csrfToken.getHeaderName());
|
||||
return (StringUtils.hasText(headerValue) ? this.plain : this.xor)
|
||||
.resolveCsrfTokenValue(request, csrfToken);
|
||||
```
|
||||
|
||||
So a SPA reading the cookie and echoing it in `X-XSRF-TOKEN` compares raw against raw and
|
||||
succeeds, while a `<form>` post keeps the BREACH masking on the hidden field. Both work, from
|
||||
one configuration.
|
||||
|
||||
## The part nobody documents: why the cookie now appears on the GET
|
||||
|
||||
That `xor.setCsrfRequestAttributeName(null)` looks like a detail. It is the fix for problem 1.
|
||||
|
||||
`CsrfTokenRequestAttributeHandler.handle` wraps the supplier in a `SupplierCsrfToken` and sets
|
||||
two request attributes. The key for the second one is the configured attribute name — or,
|
||||
when that is `null`, `csrfToken.getParameterName()`. Calling `getParameterName()` on a
|
||||
`SupplierCsrfToken` **dereferences the supplier**. The token is generated, the repository saves
|
||||
it, and the `Set-Cookie` header goes out.
|
||||
|
||||
The eager rendering is a side effect of needing a string for a map key. It is real, it is
|
||||
load-bearing, and [`docs/output/10-csrf-spa.txt`](output/10-csrf-spa.txt) shows the cookie
|
||||
arriving on the bootstrap `GET` where `08` showed nothing.
|
||||
|
||||
## `spa()` discards what you configured before it
|
||||
|
||||
Because the two assignments are unconditional:
|
||||
|
||||
```java
|
||||
.csrf(csrf -> csrf.csrfTokenRepository(myRepository).spa()) // myRepository is gone
|
||||
.csrf(csrf -> csrf.spa().csrfTokenRepository(myRepository)) // this one wins
|
||||
```
|
||||
|
||||
The `spaorder` profile asks for a cookie named `MY-CSRF` and a header named `X-CSRF-TOKEN`;
|
||||
[`docs/output/11-spa-ordering.txt`](output/11-spa-ordering.txt) shows `XSRF-TOKEN` coming back
|
||||
instead. Tracked as [spring-security#18718](https://github.com/spring-projects/spring-security/issues/18718).
|
||||
|
||||
## A CSRF rejection does not look like a CSRF rejection
|
||||
|
||||
`CsrfFilter` logs, at DEBUG:
|
||||
|
||||
```
|
||||
o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:8080/api/data
|
||||
```
|
||||
|
||||
and hands off to its `AccessDeniedHandler`, which calls `sendError(403)`. What the client
|
||||
receives, in a Basic-authenticated API with no `/error` chain, is **401 with
|
||||
`WWW-Authenticate: Basic`** — chapter 5. Every hour spent checking credentials after a
|
||||
403-that-is-a-401 is spent on the wrong thing.
|
||||
|
||||
Turn on `logging.level.org.springframework.security.web.csrf: DEBUG` before anything else.
|
||||
|
||||
## Do you need CSRF at all?
|
||||
|
||||
If the API authenticates with a `Bearer` token held in memory and never with a cookie, then no:
|
||||
there is no ambient credential for a third-party page to ride on, and `csrf.disable()` is
|
||||
correct rather than lazy. If any part of the session lives in a cookie — including a
|
||||
`HttpOnly` refresh cookie — then yes, and `spa()` is the shortest correct configuration.
|
||||
|
||||
---
|
||||
*Prev: [5. The /error dispatch](05-the-error-dispatch.md) · Next: [7. SameSite](07-samesite.md)*
|
||||
111
cors-csrf/docs/07-samesite.md
Normal file
111
cors-csrf/docs/07-samesite.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# 7. SameSite, `Secure`, and the cookie that is never stored
|
||||
|
||||
*Prev: [6. CSRF for SPAs](06-csrf-for-spas.md) · Next: [8. Debugging recipes](08-debugging-recipes.md)*
|
||||
|
||||
CORS decides whether the browser lets your JavaScript *read* a response. SameSite decides whether
|
||||
the browser *sends the cookie* in the first place. Getting CORS perfect and SameSite wrong
|
||||
produces a request that arrives cleanly and is anonymous.
|
||||
|
||||
## What Spring emits by default
|
||||
|
||||
From [`docs/output/12-samesite.txt`](output/12-samesite.txt), under `csrf.spa()`:
|
||||
|
||||
```
|
||||
Set-Cookie: XSRF-TOKEN=<token>; Path=/
|
||||
Set-Cookie: JSESSIONID=<session>; Path=/; HttpOnly; SameSite=Lax
|
||||
```
|
||||
|
||||
The session cookie gets `SameSite=Lax` from Boot's
|
||||
`server.servlet.session.cookie.same-site` default. The CSRF cookie gets **no SameSite attribute
|
||||
at all**: `CookieCsrfTokenRepository`'s default cookie customizer is, in bytecode, a single
|
||||
`return`. Nothing is set.
|
||||
|
||||
An absent `SameSite` is not "no restriction". Chromium-based browsers treat it as `Lax`; Firefox has
|
||||
**not** enabled Lax-by-default on its release channel (`network.cookie.sameSite.laxByDefault` is on in
|
||||
Nightly only). The two disagree, which is why "it works in Firefox and not in Chrome" is so often a
|
||||
missing `SameSite` attribute. `SpecCookieJar` models the Chromium behaviour, because that is the one
|
||||
you have to survive.
|
||||
|
||||
## The two rules that matter
|
||||
|
||||
**Storage (RFC 6265bis §5.5).** *"If the cookie's `same-site-flag` is `None` and the
|
||||
cookie's `secure-only-flag` is false, then abort these steps and ignore the newly created cookie
|
||||
entirely."*
|
||||
|
||||
`SameSite=None` without `Secure` is not a weaker cookie. It is not a cookie. No console warning
|
||||
is required, no error is raised, and the server has no idea.
|
||||
|
||||
**Sending (RFC 6265bis §5.8.3).** `Strict` and `Lax` cookies are not attached to cross-site
|
||||
requests, except that `Lax` allows top-level safe-method navigations. A `fetch()` from a SPA is a
|
||||
subresource request, not a top-level navigation, so `Lax` does not help it.
|
||||
|
||||
## Running the rules instead of quoting them
|
||||
|
||||
`SpecCookieJar` implements those two paragraphs in about sixty lines, and
|
||||
`/diag/cookie-spec` feeds the application's own `Set-Cookie` headers through it. Over a
|
||||
trustworthy origin:
|
||||
|
||||
| `Set-Cookie` | Stored? | Sent on a cross-site `fetch`? |
|
||||
|---|---|---|
|
||||
| `JSESSIONID=s1; HttpOnly; SameSite=Lax` | yes | no |
|
||||
| `JSESSIONID=s2; HttpOnly; SameSite=None` | **no** | — |
|
||||
| `JSESSIONID=s3; Secure; HttpOnly; SameSite=None` | yes | **yes** |
|
||||
| `XSRF-TOKEN=t1` (no SameSite) | yes | no |
|
||||
| `XSRF-TOKEN=t2; SameSite=None` | **no** | — |
|
||||
| `XSRF-TOKEN=t3; Secure; SameSite=None` | yes | **yes** |
|
||||
|
||||
Two of six reach a cross-site fetch, and they are the two carrying both attributes.
|
||||
|
||||
## The trap that costs a day: plain `http` during development
|
||||
|
||||
`Secure` is only honoured from a *trustworthy* origin. Over plain `http` the attribute is
|
||||
discarded, which makes `SameSite=None; Secure` collapse into `SameSite=None` with no `Secure`
|
||||
— which is then rejected outright. The first block of `12-samesite.txt` is that: **nothing
|
||||
survives**.
|
||||
|
||||
`http://localhost` is treated as trustworthy by current browsers, so it works. `http://127.0.0.1`
|
||||
and `http://192.168.x.x` are not, and do not. A developer testing a cross-site SPA against a LAN
|
||||
address will find that the cookie simply never appears, with no message anywhere.
|
||||
|
||||
## The configuration
|
||||
|
||||
Boot writes exactly what you tell it, and does **not** add `Secure` for you when you ask for
|
||||
`none`:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
servlet:
|
||||
session:
|
||||
cookie:
|
||||
same-site: none
|
||||
secure: true # required. Omit it and the cookie is discarded by the browser.
|
||||
http-only: true
|
||||
```
|
||||
|
||||
Spring Security's CSRF cookie is separate and needs its own customizer:
|
||||
|
||||
```java
|
||||
CookieCsrfTokenRepository repository = CookieCsrfTokenRepository.withHttpOnlyFalse();
|
||||
repository.setCookieCustomizer((cookie) -> cookie.sameSite("None").secure(true));
|
||||
|
||||
http.csrf(csrf -> csrf.spa().csrfTokenRepository(repository));
|
||||
```
|
||||
|
||||
Order matters — chapter 6.
|
||||
|
||||
## Partitioned cookies (CHIPS)
|
||||
|
||||
`Partitioned` requires `Secure` and, in practice, `SameSite=None`. It changes the cookie's
|
||||
storage key so that a cookie set in a third-party context is scoped to the top-level site that
|
||||
embedded it. `SpecCookieJar` models the `Secure` requirement; it does not model partitioning,
|
||||
which is noted here rather than pretended.
|
||||
|
||||
If your SPA and API are separate registrable domains, the honest conclusion is:
|
||||
|
||||
> **A same-site deployment removes this entire chapter.** Serving the SPA and the API from one
|
||||
> origin, or from two subdomains of one registrable domain, means `SameSite=Lax` works, `Secure`
|
||||
> is a hygiene setting rather than a prerequisite, and the preflight disappears. A reverse proxy
|
||||
> in front of both is usually less work than everything above.
|
||||
|
||||
---
|
||||
*Prev: [6. CSRF for SPAs](06-csrf-for-spas.md) · Next: [8. Debugging recipes](08-debugging-recipes.md)*
|
||||
77
cors-csrf/docs/08-debugging-recipes.md
Normal file
77
cors-csrf/docs/08-debugging-recipes.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# 8. Debugging recipes
|
||||
|
||||
*Prev: [7. SameSite](07-samesite.md)*
|
||||
|
||||
## Turn on the two log categories first
|
||||
|
||||
```yaml
|
||||
logging:
|
||||
level:
|
||||
org.springframework.web.cors: DEBUG # DefaultCorsProcessor's Skip:/Reject: lines
|
||||
org.springframework.security.web.csrf: DEBUG # "Invalid CSRF token found for ..."
|
||||
```
|
||||
|
||||
Almost every question in this subject is answered by one line from one of those two.
|
||||
|
||||
## Reproduce the preflight without a browser
|
||||
|
||||
```bash
|
||||
curl -s -i -X OPTIONS http://localhost:8080/api/data \
|
||||
-H 'Origin: https://spa.example.com' \
|
||||
-H 'Access-Control-Request-Method: POST' \
|
||||
-H 'Access-Control-Request-Headers: content-type,x-xsrf-token'
|
||||
```
|
||||
|
||||
That is the whole preflight. `scripts/preflight.sh` wraps it. Note the absence of `-u` and
|
||||
`-b`: the browser sends no credentials on a preflight, and reproducing it *with* credentials
|
||||
hides the bug.
|
||||
|
||||
## Is `CorsFilter` even in the chain?
|
||||
|
||||
```bash
|
||||
curl -s localhost:8080/diag/chain | python3 -m json.tool
|
||||
```
|
||||
|
||||
If `CorsFilter` is absent, no amount of MVC configuration will help — chapter 1. The
|
||||
production equivalent, without a diagnostic endpoint, is the startup log:
|
||||
|
||||
```
|
||||
Will secure any request with filters: DisableEncodeUrlFilter, ..., CorsFilter, ...
|
||||
```
|
||||
|
||||
Grep for `with filters:`.
|
||||
|
||||
## Which `CorsConfigurationSource` beans exist, and what are they called?
|
||||
|
||||
```bash
|
||||
curl -s localhost:8080/diag/cors-sources
|
||||
```
|
||||
|
||||
`hasBeanNamedCorsConfigurationSource: false` with a `UrlBasedCorsConfigurationSource` in the list
|
||||
is the chapter 2 failure exactly.
|
||||
|
||||
## Read the status code as a diagnosis
|
||||
|
||||
| Symptom | Look at |
|
||||
|---|---|
|
||||
| Preflight `401`/`403`, no CORS headers | Chapter 1 — no `CorsFilter` |
|
||||
| Preflight `200`, no CORS headers | Chapter 2 — bean name |
|
||||
| Preflight `403`, `Invalid CORS request` | Chapter 3 — read the DEBUG line |
|
||||
| `401` on a request with valid credentials | Chapter 5 — the `/error` dispatch |
|
||||
| `403` on a POST, `GET` is fine | Chapter 6 — CSRF |
|
||||
| Cookie visible in DevTools' response, absent from the jar | Chapter 7 — `SameSite=None` with no `Secure` |
|
||||
| Every request preflights, latency doubled | Chapter 1 — no `Access-Control-Max-Age` |
|
||||
|
||||
## Check the cookie jar, not the response
|
||||
|
||||
DevTools shows the `Set-Cookie` header in the Network tab whether or not the browser stored the
|
||||
cookie. Application → Cookies is the jar. A header present in one and absent from the other
|
||||
is chapter 7, every time.
|
||||
|
||||
## Delete the diagnostic endpoints
|
||||
|
||||
`DiagController` and `CookieSpecReport` publish your filter chain and bean names. They exist to
|
||||
make this repository legible. Do not ship them.
|
||||
|
||||
---
|
||||
*Prev: [7. SameSite](07-samesite.md)*
|
||||
58
cors-csrf/docs/output/01-mvc-only.txt
Normal file
58
cors-csrf/docs/output/01-mvc-only.txt
Normal file
@@ -0,0 +1,58 @@
|
||||
==============================================================================
|
||||
docs/output/01-mvc-only.txt
|
||||
CORS configured with WebMvcConfigurer.addCorsMappings and nothing else.
|
||||
Profile: mvconly
|
||||
==============================================================================
|
||||
|
||||
# The security chain. Note what is NOT in it.
|
||||
$ curl -s localhost:8080/diag/chain
|
||||
{
|
||||
"profiles": [
|
||||
"mvconly"
|
||||
],
|
||||
"chains": [
|
||||
{
|
||||
"size": 11,
|
||||
"filters": [
|
||||
"DisableEncodeUrlFilter",
|
||||
"WebAsyncManagerIntegrationFilter",
|
||||
"SecurityContextHolderFilter",
|
||||
"HeaderWriterFilter",
|
||||
"LogoutFilter",
|
||||
"BasicAuthenticationFilter",
|
||||
"RequestCacheAwareFilter",
|
||||
"SecurityContextHolderAwareRequestFilter",
|
||||
"AnonymousAuthenticationFilter",
|
||||
"ExceptionTranslationFilter",
|
||||
"AuthorizationFilter"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# CorsConfigurationSource beans in the context.
|
||||
$ curl -s localhost:8080/diag/cors-sources
|
||||
{
|
||||
"corsConfigurationSourceBeans": {
|
||||
"mvcHandlerMappingIntrospector": "HandlerMappingIntrospector"
|
||||
},
|
||||
"hasBeanNamedCorsConfigurationSource": false
|
||||
}
|
||||
|
||||
$ curl -s -i -X OPTIONS http://localhost:8080/api/data \
|
||||
-H 'Origin: https://spa.example.com' \
|
||||
-H 'Access-Control-Request-Method: POST' \
|
||||
-H 'Access-Control-Request-Headers: content-type,x-xsrf-token'
|
||||
|
||||
HTTP/1.1 401
|
||||
Set-Cookie: JSESSIONID=<session>; Path=/; HttpOnly; SameSite=Lax
|
||||
WWW-Authenticate: Basic realm="Realm", charset="UTF-8"
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# The MVC CORS mapping is real - it just never runs, because the request is
|
||||
# rejected at AuthorizationFilter (order 4200) and the DispatcherServlet is
|
||||
# downstream of the entire filter chain.
|
||||
56
cors-csrf/docs/output/02-mvc-bridge.txt
Normal file
56
cors-csrf/docs/output/02-mvc-bridge.txt
Normal file
@@ -0,0 +1,56 @@
|
||||
==============================================================================
|
||||
docs/output/02-mvc-bridge.txt
|
||||
The identical MVC CORS mapping plus one line: .cors(Customizer.withDefaults()).
|
||||
Profile: mvcbridge
|
||||
==============================================================================
|
||||
|
||||
$ curl -s localhost:8080/diag/chain
|
||||
{
|
||||
"profiles": [
|
||||
"mvcbridge"
|
||||
],
|
||||
"chains": [
|
||||
{
|
||||
"size": 12,
|
||||
"filters": [
|
||||
"DisableEncodeUrlFilter",
|
||||
"WebAsyncManagerIntegrationFilter",
|
||||
"SecurityContextHolderFilter",
|
||||
"HeaderWriterFilter",
|
||||
"CorsFilter",
|
||||
"LogoutFilter",
|
||||
"BasicAuthenticationFilter",
|
||||
"RequestCacheAwareFilter",
|
||||
"SecurityContextHolderAwareRequestFilter",
|
||||
"AnonymousAuthenticationFilter",
|
||||
"ExceptionTranslationFilter",
|
||||
"AuthorizationFilter"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
$ curl -s -i -X OPTIONS http://localhost:8080/api/data \
|
||||
-H 'Origin: https://spa.example.com' \
|
||||
-H 'Access-Control-Request-Method: POST' \
|
||||
-H 'Access-Control-Request-Headers: content-type,x-xsrf-token'
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
Access-Control-Allow-Origin: https://spa.example.com
|
||||
Access-Control-Allow-Methods: GET,POST
|
||||
Access-Control-Allow-Headers: content-type, x-xsrf-token
|
||||
Access-Control-Allow-Credentials: true
|
||||
Access-Control-Max-Age: 1800
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# CorsFilter is now in the chain at order 1000, between HeaderWriterFilter (900)
|
||||
# and CsrfFilter (1100), and it short-circuits the preflight before authorization
|
||||
# ever sees it. Note Access-Control-Max-Age: 1800 - that default comes from MVC's
|
||||
# CorsRegistration, not from CorsConfiguration.
|
||||
36
cors-csrf/docs/output/03-security-source.txt
Normal file
36
cors-csrf/docs/output/03-security-source.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
==============================================================================
|
||||
docs/output/03-security-source.txt
|
||||
A @Bean named corsConfigurationSource. .cors(..) is never called - it is applied for us.
|
||||
Profile: securitysource
|
||||
==============================================================================
|
||||
|
||||
$ curl -s localhost:8080/diag/cors-sources
|
||||
{
|
||||
"corsConfigurationSourceBeans": {
|
||||
"corsConfigurationSource": "UrlBasedCorsConfigurationSource",
|
||||
"mvcHandlerMappingIntrospector": "HandlerMappingIntrospector"
|
||||
},
|
||||
"hasBeanNamedCorsConfigurationSource": true
|
||||
}
|
||||
|
||||
$ curl -s -i -X OPTIONS http://localhost:8080/api/data \
|
||||
-H 'Origin: https://spa.example.com' \
|
||||
-H 'Access-Control-Request-Method: POST' \
|
||||
-H 'Access-Control-Request-Headers: content-type,x-xsrf-token'
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
Access-Control-Allow-Origin: https://spa.example.com
|
||||
Access-Control-Allow-Methods: GET,POST
|
||||
Access-Control-Allow-Headers: content-type, x-xsrf-token
|
||||
Access-Control-Allow-Credentials: true
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# Compare with 02: there is no Access-Control-Max-Age here. CorsConfiguration
|
||||
# leaves maxAge null, so every single cross-origin call re-runs the preflight.
|
||||
47
cors-csrf/docs/output/04-three-identical-403s.txt
Normal file
47
cors-csrf/docs/output/04-three-identical-403s.txt
Normal file
@@ -0,0 +1,47 @@
|
||||
==============================================================================
|
||||
docs/output/04-three-identical-403s.txt
|
||||
Origin not allowed, method not allowed, header not allowed. One status, one shape.
|
||||
Profile: securitysource, CORS_LOG_LEVEL=DEBUG
|
||||
==============================================================================
|
||||
|
||||
# 1. disallowed origin
|
||||
HTTP/1.1 403
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# 2. disallowed method
|
||||
HTTP/1.1 403
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# 3. disallowed request header
|
||||
HTTP/1.1 403
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# body of a rejected preflight:
|
||||
Invalid CORS request
|
||||
|
||||
# The only thing that distinguishes them is a DEBUG line from DefaultCorsProcessor:
|
||||
<timestamp> DEBUG <pid> --- [cors-csrf-samesite] [nio-8080-exec-N] o.s.web.cors.DefaultCorsProcessor : Reject: 'https://evil.example.com' origin is not allowed
|
||||
<timestamp> DEBUG <pid> --- [cors-csrf-samesite] [nio-8080-exec-N] o.s.web.cors.DefaultCorsProcessor : Reject: HTTP 'DELETE' is not allowed
|
||||
<timestamp> DEBUG <pid> --- [cors-csrf-samesite] [nio-8080-exec-N] o.s.web.cors.DefaultCorsProcessor : Reject: headers '[authorization]' are not allowed
|
||||
<timestamp> DEBUG <pid> --- [cors-csrf-samesite] [nio-8080-exec-N] o.s.web.cors.DefaultCorsProcessor : Reject: 'https://evil.example.com' origin is not allowed
|
||||
39
cors-csrf/docs/output/05-misnamed-bean.txt
Normal file
39
cors-csrf/docs/output/05-misnamed-bean.txt
Normal file
@@ -0,0 +1,39 @@
|
||||
==============================================================================
|
||||
docs/output/05-misnamed-bean.txt
|
||||
The same UrlBasedCorsConfigurationSource bean, named apiCorsSource instead of
|
||||
corsConfigurationSource. It starts. The preflight returns 200. It carries no CORS headers.
|
||||
Profile: misnamed
|
||||
==============================================================================
|
||||
|
||||
$ curl -s localhost:8080/diag/cors-sources
|
||||
{
|
||||
"corsConfigurationSourceBeans": {
|
||||
"apiCorsSource": "UrlBasedCorsConfigurationSource",
|
||||
"mvcHandlerMappingIntrospector": "HandlerMappingIntrospector"
|
||||
},
|
||||
"hasBeanNamedCorsConfigurationSource": false
|
||||
}
|
||||
|
||||
$ curl -s -i -X OPTIONS http://localhost:8080/api/data \
|
||||
-H 'Origin: https://spa.example.com' \
|
||||
-H 'Access-Control-Request-Method: POST' \
|
||||
-H 'Access-Control-Request-Headers: content-type,x-xsrf-token'
|
||||
|
||||
HTTP/1.1 200
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
<timestamp> DEBUG <pid> --- [cors-csrf-samesite] [nio-8080-exec-N] o.s.web.cors.DefaultCorsProcessor : Skip: no CORS configuration has been provided
|
||||
|
||||
# Two different lookups. HttpSecurityConfiguration.applyCorsIfAvailable asks
|
||||
# getBeanNamesForType(UrlBasedCorsConfigurationSource.class) and enables CORS if the
|
||||
# array is non-empty, so the bean above DID switch the configurer on.
|
||||
# CorsConfigurer.getCorsConfigurationSource then asks
|
||||
# containsBeanDefinition("corsConfigurationSource"), which is false, and falls back
|
||||
# to Spring MVC's registrations - of which there are none.
|
||||
# CorsFilter returns from every preflight whether or not it found a configuration:
|
||||
# if (!isValid || CorsUtils.isPreFlightRequest(request)) { return; }
|
||||
# so the OPTIONS never reaches AuthorizationFilter and the client gets a bare 200.
|
||||
44
cors-csrf/docs/output/06-two-sources.txt
Normal file
44
cors-csrf/docs/output/06-two-sources.txt
Normal file
@@ -0,0 +1,44 @@
|
||||
==============================================================================
|
||||
docs/output/06-two-sources.txt
|
||||
Two UrlBasedCorsConfigurationSource beans. The reference documentation says Spring Security
|
||||
'won't automatically configure CORS support for you, because it cannot decide which one to
|
||||
use'. In 7.1.1 it configures it, and the bean NAME decides.
|
||||
Profile: twosources
|
||||
==============================================================================
|
||||
|
||||
{
|
||||
"corsConfigurationSourceBeans": {
|
||||
"corsConfigurationSource": "UrlBasedCorsConfigurationSource",
|
||||
"adminCorsSource": "UrlBasedCorsConfigurationSource",
|
||||
"mvcHandlerMappingIntrospector": "HandlerMappingIntrospector"
|
||||
},
|
||||
"hasBeanNamedCorsConfigurationSource": true
|
||||
}
|
||||
|
||||
# the origin allowed by the bean named corsConfigurationSource:
|
||||
HTTP/1.1 200
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
Access-Control-Allow-Origin: https://spa.example.com
|
||||
Access-Control-Allow-Methods: GET,POST
|
||||
Access-Control-Allow-Headers: content-type
|
||||
Access-Control-Allow-Credentials: true
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# the origin allowed by adminCorsSource, which is never consulted:
|
||||
HTTP/1.1 403
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
<timestamp> DEBUG <pid> --- [cors-csrf-samesite] [nio-8080-exec-N] o.s.web.cors.DefaultCorsProcessor : Reject: 'https://admin.example.com' origin is not allowed
|
||||
41
cors-csrf/docs/output/07-wildcard-credentials.txt
Normal file
41
cors-csrf/docs/output/07-wildcard-credentials.txt
Normal file
@@ -0,0 +1,41 @@
|
||||
==============================================================================
|
||||
docs/output/07-wildcard-credentials.txt
|
||||
allowedOrigins("*") together with allowCredentials(true). Legal to configure, illegal to
|
||||
serve. The failure is thrown on the request, not at startup - and it does not surface as a 500.
|
||||
Profile: wildcard
|
||||
==============================================================================
|
||||
|
||||
HTTP/1.1 401
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
Set-Cookie: JSESSIONID=<session>; Path=/; HttpOnly; SameSite=Lax
|
||||
WWW-Authenticate: Basic realm="Realm", charset="UTF-8"
|
||||
|
||||
# and a plain authenticated GET, with correct credentials:
|
||||
HTTP/1.1 401
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
Set-Cookie: JSESSIONID=<session>; Path=/; HttpOnly; SameSite=Lax
|
||||
WWW-Authenticate: Basic realm="Realm", charset="UTF-8"
|
||||
|
||||
java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header. To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.
|
||||
at org.springframework.web.cors.CorsConfiguration.validateAllowCredentials(CorsConfiguration.java:552) ~[spring-web-7.0.9.jar:7.0.9]
|
||||
at org.springframework.web.cors.CorsConfiguration.checkOrigin(CorsConfiguration.java:678) ~[spring-web-7.0.9.jar:7.0.9]
|
||||
at org.springframework.web.cors.DefaultCorsProcessor.checkOrigin(DefaultCorsProcessor.java:193) ~[spring-web-7.0.9.jar:7.0.9]
|
||||
at org.springframework.web.cors.DefaultCorsProcessor.handleInternal(DefaultCorsProcessor.java:131) ~[spring-web-7.0.9.jar:7.0.9]
|
||||
|
||||
# 401, not 500. The exception escapes CorsFilter, Tomcat re-dispatches to /error,
|
||||
# the security chain runs again on that dispatch without re-reading the credential,
|
||||
# and the anonymous second pass is what answers.
|
||||
54
cors-csrf/docs/output/08-csrf-naive.txt
Normal file
54
cors-csrf/docs/output/08-csrf-naive.txt
Normal file
@@ -0,0 +1,54 @@
|
||||
==============================================================================
|
||||
docs/output/08-csrf-naive.txt
|
||||
CookieCsrfTokenRepository.withHttpOnlyFalse() on its own - the recipe from every pre-6.0
|
||||
tutorial. Three separate things go wrong.
|
||||
Profile: csrfnaive
|
||||
==============================================================================
|
||||
|
||||
# 1. The bootstrap GET. A SPA expects an XSRF-TOKEN cookie here.
|
||||
HTTP/1.1 200
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# cookie jar after the GET:
|
||||
(empty - no cookie was set)
|
||||
|
||||
# 2. POST with no token.
|
||||
HTTP/1.1 401
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
Set-Cookie: XSRF-TOKEN=<token>; Path=/
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
Set-Cookie: JSESSIONID=<session>; Path=/; HttpOnly; SameSite=Lax
|
||||
WWW-Authenticate: Basic realm="Realm", charset="UTF-8"
|
||||
|
||||
# cookie jar now:
|
||||
|
||||
localhost | FALSE | / | FALSE | 0 | XSRF-TOKEN | 4888debb-2e51-4742-b0e7-262c489825b9
|
||||
|
||||
# 3. POST echoing the raw cookie value back in X-XSRF-TOKEN, which is what every
|
||||
# SPA snippet on the internet does.
|
||||
HTTP/1.1 401
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
WWW-Authenticate: Basic realm="Realm", charset="UTF-8"
|
||||
|
||||
<timestamp> DEBUG <pid> --- [cors-csrf-samesite] [nio-8080-exec-N] o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:8080/api/data
|
||||
<timestamp> DEBUG <pid> --- [cors-csrf-samesite] [nio-8080-exec-N] o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:8080/api/data
|
||||
25
cors-csrf/docs/output/09-error-dispatch.txt
Normal file
25
cors-csrf/docs/output/09-error-dispatch.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
==============================================================================
|
||||
docs/output/09-error-dispatch.txt
|
||||
The identical CSRF failure, with one extra filter chain that permits /error.
|
||||
Profile: csrfnaive,errorpermit
|
||||
==============================================================================
|
||||
|
||||
HTTP/1.1 403
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
Set-Cookie: XSRF-TOKEN=<token>; Path=/
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
{"timestamp":"<timestamp>","status":403,"error":"Forbidden","path":"/api/data"}
|
||||
|
||||
# 403, and a body. Without the /error chain the same request answers 401 with an
|
||||
# empty body and a WWW-Authenticate header - see 08. AccessDeniedHandlerImpl calls
|
||||
# response.sendError(403), the container re-dispatches to /error, and the security
|
||||
# chain runs a second time on that dispatch. BasicAuthenticationFilter extends
|
||||
# OncePerRequestFilter and skips error dispatches, so the second pass is anonymous
|
||||
# and AuthorizationFilter answers 401 over the top of the 403.
|
||||
47
cors-csrf/docs/output/10-csrf-spa.txt
Normal file
47
cors-csrf/docs/output/10-csrf-spa.txt
Normal file
@@ -0,0 +1,47 @@
|
||||
==============================================================================
|
||||
docs/output/10-csrf-spa.txt
|
||||
The same flow under csrf.spa(), added in Spring Security 7.0.
|
||||
Profile: csrfspa
|
||||
==============================================================================
|
||||
|
||||
# 1. The bootstrap GET now DOES set the cookie.
|
||||
HTTP/1.1 200
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
Set-Cookie: XSRF-TOKEN=<token>; Path=/
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# 2. POST with no token still fails, as it must.
|
||||
HTTP/1.1 401
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
Set-Cookie: JSESSIONID=<session>; Path=/; HttpOnly; SameSite=Lax
|
||||
WWW-Authenticate: Basic realm="Realm", charset="UTF-8"
|
||||
|
||||
# 3. POST echoing the raw cookie value in X-XSRF-TOKEN.
|
||||
HTTP/1.1 200
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
{"created":true,"received":"{}","cookies":"XSRF-TOKEN"}
|
||||
|
||||
# Note the cookie attributes: Path=/ and nothing else. No SameSite, no Secure,
|
||||
# no HttpOnly. A cookie with no SameSite attribute is treated as Lax, so a
|
||||
# genuinely cross-site SPA still never receives it. See 12.
|
||||
24
cors-csrf/docs/output/11-spa-ordering.txt
Normal file
24
cors-csrf/docs/output/11-spa-ordering.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
==============================================================================
|
||||
docs/output/11-spa-ordering.txt
|
||||
.csrf(c -> c.csrfTokenRepository(custom).spa()) - the custom repository asked for a cookie
|
||||
named MY-CSRF and a header named X-CSRF-TOKEN. Neither reaches the running application.
|
||||
Profile: spaorder
|
||||
==============================================================================
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
Set-Cookie: XSRF-TOKEN=<token>; Path=/
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# cookie jar:
|
||||
|
||||
localhost | FALSE | / | FALSE | 0 | XSRF-TOKEN | 42573eea-76d6-4bc6-a14b-bff76640461d
|
||||
|
||||
# spa() assigns csrfTokenRepository and requestHandler unconditionally; it is not a
|
||||
# 'defaults if unset' method. Swap the two calls and MY-CSRF appears.
|
||||
59
cors-csrf/docs/output/12-samesite.txt
Normal file
59
cors-csrf/docs/output/12-samesite.txt
Normal file
@@ -0,0 +1,59 @@
|
||||
==============================================================================
|
||||
docs/output/12-samesite.txt
|
||||
The Set-Cookie headers this application emits under four configurations, and what
|
||||
SpecCookieJar - a model of RFC 6265bis 5.5 and 5.8.3 - does with them.
|
||||
==============================================================================
|
||||
|
||||
## csrf.spa() defaults, session cookie left at same-site=lax
|
||||
Set-Cookie: XSRF-TOKEN=<token>; Path=/
|
||||
Set-Cookie: JSESSIONID=<session>; Path=/; HttpOnly; SameSite=Lax
|
||||
|
||||
## session cookie set to same-site=none, secure=false
|
||||
Set-Cookie: XSRF-TOKEN=<token>; Path=/
|
||||
Set-Cookie: JSESSIONID=<session>; Path=/; HttpOnly; SameSite=None
|
||||
|
||||
## crosssite profile: SameSite=None and Secure on both cookies
|
||||
Set-Cookie: XSRF-TOKEN=<token>; Path=/; Secure; SameSite=None
|
||||
Set-Cookie: JSESSIONID=<session>; Path=/; Secure; HttpOnly; SameSite=None
|
||||
|
||||
## crosssite profile with -DOMIT_SECURE=true
|
||||
Set-Cookie: XSRF-TOKEN=<token>; Path=/; SameSite=None
|
||||
Set-Cookie: JSESSIONID=<session>; Path=/; HttpOnly; SameSite=None
|
||||
|
||||
## The same headers, run through SpecCookieJar
|
||||
{
|
||||
"origin": "not trustworthy (plain http)",
|
||||
"setCookieOutcomes": {
|
||||
"JSESSIONID=s1; Path=/; HttpOnly; SameSite=Lax": "stored",
|
||||
"JSESSIONID=s2; Path=/; HttpOnly; SameSite=None": "REJECTED JSESSIONID: SameSite=None and no Secure attribute - RFC 6265bis 5.5",
|
||||
"JSESSIONID=s3; Path=/; Secure; HttpOnly; SameSite=None": "REJECTED JSESSIONID: SameSite=None with Secure, but the origin is not trustworthy so Secure is not honoured - RFC 6265bis 5.5",
|
||||
"XSRF-TOKEN=t1; Path=/": "stored",
|
||||
"XSRF-TOKEN=t2; Path=/; SameSite=None": "REJECTED XSRF-TOKEN: SameSite=None and no Secure attribute - RFC 6265bis 5.5",
|
||||
"XSRF-TOKEN=t3; Path=/; Secure; SameSite=None": "REJECTED XSRF-TOKEN: SameSite=None with Secure, but the origin is not trustworthy so Secure is not honoured - RFC 6265bis 5.5"
|
||||
},
|
||||
"sentOnSameSiteRequest": "JSESSIONID=s1; XSRF-TOKEN=t1",
|
||||
"sentOnCrossSiteTopLevelNavigation": "JSESSIONID=s1; XSRF-TOKEN=t1",
|
||||
"sentOnCrossSiteFetch": "(no cookies sent)"
|
||||
}
|
||||
|
||||
{
|
||||
"origin": "trustworthy (https, or http://localhost)",
|
||||
"setCookieOutcomes": {
|
||||
"JSESSIONID=s1; Path=/; HttpOnly; SameSite=Lax": "stored",
|
||||
"JSESSIONID=s2; Path=/; HttpOnly; SameSite=None": "REJECTED JSESSIONID: SameSite=None and no Secure attribute - RFC 6265bis 5.5",
|
||||
"JSESSIONID=s3; Path=/; Secure; HttpOnly; SameSite=None": "stored",
|
||||
"XSRF-TOKEN=t1; Path=/": "stored",
|
||||
"XSRF-TOKEN=t2; Path=/; SameSite=None": "REJECTED XSRF-TOKEN: SameSite=None and no Secure attribute - RFC 6265bis 5.5",
|
||||
"XSRF-TOKEN=t3; Path=/; Secure; SameSite=None": "stored"
|
||||
},
|
||||
"sentOnSameSiteRequest": "JSESSIONID=s3; XSRF-TOKEN=t3",
|
||||
"sentOnCrossSiteTopLevelNavigation": "JSESSIONID=s3; XSRF-TOKEN=t3",
|
||||
"sentOnCrossSiteFetch": "JSESSIONID=s3; XSRF-TOKEN=t3"
|
||||
}
|
||||
|
||||
# Read the second block first: over a trustworthy origin, the only two of the six
|
||||
# that reach a cross-site fetch are the two carrying Secure AND SameSite=None.
|
||||
# Then read the first: over plain http, none do -
|
||||
# which is why a cross-site SPA cannot be developed against http://127.0.0.1.
|
||||
# (http://localhost itself is treated as trustworthy by current browsers; a bare IP
|
||||
# is not.)
|
||||
34
cors-csrf/docs/output/13-tests.txt
Normal file
34
cors-csrf/docs/output/13-tests.txt
Normal file
@@ -0,0 +1,34 @@
|
||||
==============================================================================
|
||||
docs/output/13-tests.txt
|
||||
mvn -B test
|
||||
==============================================================================
|
||||
|
||||
09:33:03.378 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CsrfAndCookieTests$Naive
|
||||
09:33:03.464 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CsrfAndCookieTests$Naive
|
||||
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.904 s -- in CookieCsrfTokenRepository.withHttpOnlyFalse() on its own
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CsrfAndCookieTests$Spa
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CsrfAndCookieTests$Spa
|
||||
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.444 s -- in csrf.spa()
|
||||
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.028 s -- in SpecCookieJar - the storage and sending rules a browser applies
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CsrfAndCookieTests$Ordering
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CsrfAndCookieTests$Ordering
|
||||
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.362 s -- in csrfTokenRepository(..) before spa()
|
||||
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.783 s -- in com.ankurm.cors.CsrfAndCookieTests
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CorsContractTests$MvcOnly
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CorsContractTests$MvcOnly
|
||||
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.305 s -- in CORS on the MVC layer only
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CorsContractTests$Misnamed
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CorsContractTests$Misnamed
|
||||
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.264 s -- in the right type under the wrong bean name
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CorsContractTests$SecuritySource
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CorsContractTests$SecuritySource
|
||||
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.225 s -- in a bean named corsConfigurationSource
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CorsContractTests$TwoSources
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CorsContractTests$TwoSources
|
||||
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.235 s -- in two UrlBasedCorsConfigurationSource beans
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CorsContractTests$MvcBridge
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CorsContractTests$MvcBridge
|
||||
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.217 s -- in the same MVC configuration plus .cors(withDefaults())
|
||||
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.256 s -- in com.ankurm.cors.CorsContractTests
|
||||
[INFO] Tests run: 23, Failures: 0, Errors: 0, Skipped: 0
|
||||
[INFO] BUILD SUCCESS
|
||||
Reference in New Issue
Block a user