Skip to content

Commit 7f8ae6c

Browse files
committed
feat: secure async servlet redispatches
1 parent ce37e33 commit 7f8ae6c

14 files changed

Lines changed: 947 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ include migration notes.
77

88
## Unreleased
99

10+
- Added stateless Spring MVC async and streaming support based on a request-attribute
11+
`SecurityContextRepository`, preserving identity across legitimate redispatches without
12+
revalidating JWTs or creating an `HttpSession`.
13+
- Added integration coverage and migration guidance for Spring MVC async return types and
14+
dispatcher authorization.
15+
1016
## 7.1.1 - 2026-07-10
1117

1218
- Updated the Gradle wrapper to 9.6.1, Spotless to 8.8.0, Caffeine to 3.2.4,

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@ Vigil provides JWT lifecycle, request authentication, cookie helpers, tenant con
88

99
## Compatibility
1010

11-
Vigil `7.1.x` supports Java 25, Spring Boot 4.1.x, Spring Framework 7.x, Spring Security 7.1.x, Gradle 9.6.x, and Jackson 3. Vigil `6.0.x` was the final Java 21 / Spring Boot 3.5 line.
11+
Vigil `7.1.x` is certified with Java 25, Spring Boot 4.1.0, Spring Framework 7.0.8,
12+
Spring Security 7.1.0, Gradle 9.6.x, and Jackson 3. Later dependency patches are not claimed as
13+
certified until they pass Vigil's complete gate. Vigil `6.0.x` was the final Java 21 / Spring Boot
14+
3.5 line.
1215

1316
`7.1.1` is the current release line. Public consumers should pin an exact version and review the release notes before upgrading.
1417

1518
## Install
1619

1720
```kotlin
1821
dependencies {
19-
implementation("io.github.sequelcore:vigil-spring-boot-starter:7.0.0")
22+
implementation("io.github.sequelcore:vigil-spring-boot-starter:7.1.1")
2023
}
2124
```
2225

@@ -43,6 +46,7 @@ The application must still configure route authorization. `ignored-paths` skips
4346
Start at the [documentation index](docs/README.md).
4447

4548
- [Authentication guide](docs/guides/authentication.md)
49+
- [Async and streaming security](docs/guides/async-streaming-security.md)
4650
- [Configuration reference](docs/reference/configuration.md)
4751
- [System boundaries](docs/architecture/system-boundaries.md)
4852
- [Security model](docs/security/security-model.md)

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ dependencies {
5858

5959
// Testing
6060
testImplementation("org.springframework.boot:spring-boot-starter-test")
61+
testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test")
6162
testImplementation("org.springframework.boot:spring-boot-starter-security")
6263
testImplementation("org.springframework.boot:spring-boot-starter-validation")
6364
testImplementation("org.springframework.security:spring-security-test")

docs/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Vigil is a Spring Boot starter for application-owned JWT authentication. It prov
44

55
## Start here
66

7+
- [Async and streaming security](guides/async-streaming-security.md) — preserve stateless authentication across MVC redispatches.
8+
79
- [Authentication guide](guides/authentication.md) — configure JWTs, cookies, Spring Security, tenants, and reset tokens.
810
- [Configuration reference](reference/configuration.md) — every `vigil.*` property and its defaults.
911
- [System boundaries](architecture/system-boundaries.md) — understand ownership and extension points before integrating.

docs/development/testing.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,14 @@ Changes must prove the behavior at the owning boundary:
2525
- step-up: actor separation, credential failure/lockout, tenant/audience/purpose binding, expiry, and one-time consumption;
2626
- configuration: invalid security settings fail fast at startup.
2727

28+
Async security changes additionally require the real filter chain tests in
29+
`VigilAsyncSecurityIntegrationTest` and the embedded-Tomcat socket tests in
30+
`VigilSseDisconnectTomcatIntegrationTest`. The latter verifies a committed SSE response, a client
31+
RST followed by `IOException`, final `ASYNC` processing, `ERROR` dispatch, callback cleanup, and
32+
the absence of a secondary authentication entry point or access-denied response.
33+
34+
The certified dependency combination is resolved by the Spring Boot BOM in `build.gradle.kts`.
35+
Documentation must name the exact versions exercised by the full gate; an untested `4.1.x`, `7.x`,
36+
or `7.1.x` range is not a supported compatibility claim.
37+
2838
Use application integration tests for application-owned routes and user persistence. Vigil tests do not replace product authorization or user-lifecycle tests.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Async and streaming security
2+
3+
Vigil preserves an authenticated Spring Security context for the lifetime of one servlet request,
4+
including its `ASYNC` and `ERROR` redispatches. It does not create an `HttpSession`, revalidate a
5+
JWT during redispatch, or weaken the application's authorization rules.
6+
7+
## Secure stateless configuration
8+
9+
Use Vigil's request-scoped repository in the application's filter chain. Keep authorization rules
10+
application-owned and continue authorizing every dispatcher type.
11+
12+
```java
13+
@Bean
14+
SecurityFilterChain securityFilterChain(
15+
HttpSecurity http,
16+
VigilAuthenticationFilter vigilAuthenticationFilter) throws Exception {
17+
var requestSecurityContextRepository = new RequestAttributeSecurityContextRepository();
18+
vigilAuthenticationFilter.setSecurityContextRepository(requestSecurityContextRepository);
19+
return http
20+
.sessionManagement(session ->
21+
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
22+
.securityContext(context ->
23+
context.securityContextRepository(requestSecurityContextRepository))
24+
.authorizeHttpRequests(authorize -> authorize
25+
.requestMatchers("/auth/**").permitAll()
26+
.anyRequest().authenticated())
27+
.addFilterBefore(vigilAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
28+
.build();
29+
}
30+
```
31+
32+
Vigil validates credentials and runs authentication hooks and context populators only on the
33+
initial `REQUEST`. After successful authentication it saves the `SecurityContext` in a
34+
`RequestAttributeSecurityContextRepository`. Spring Security's `SecurityContextHolderFilter`
35+
loads that context for a legitimate redispatch and clears its thread-local holder afterward. A
36+
new request, or a fabricated `ASYNC`/`ERROR` dispatch without the request attribute, has no saved
37+
identity and remains subject to normal authorization.
38+
39+
Do not globally `permitAll` `ASYNC` or `ERROR` merely to avoid a secondary authorization failure.
40+
That can bypass application policy. Any deliberate narrow exception remains application-owned.
41+
42+
## MVC lifecycle
43+
44+
`DeferredResult`, `ResponseBodyEmitter`, `SseEmitter`, and `StreamingResponseBody` use Servlet
45+
async processing. MVC leaves the response open after the initial dispatch and later performs an
46+
`ASYNC` dispatch to finish processing. When an emitter write fails because the client disconnected,
47+
the application must not call `complete` or `completeWithError`; the container notifies Spring MVC,
48+
which performs the final error dispatch and cleanup.
49+
50+
A Broken pipe is a normal network event and cannot be prevented. Record expected disconnects
51+
separately from integrity failures. Monitor emitter completion, timeout, active connections, and
52+
unexpected exception-resolver failures. The application owns MVC executors, timeouts, heartbeats,
53+
resource cleanup, and propagation of domain context. Vigil preserves the Spring Security principal,
54+
not arbitrary application `ThreadLocal` values.
55+
56+
## Responsibility matrix
57+
58+
| Vigil | Consuming application |
59+
| --- | --- |
60+
| Validate the initial credential and save its authenticated context on the same request | Define HTTP and business authorization rules |
61+
| Avoid reauthentication and authentication side effects on redispatch | Configure MVC async lifecycle and resource cleanup |
62+
| Save into Spring Security's request-attribute repository contract | Install a `RequestAttributeSecurityContextRepository` in `HttpSecurity` |
63+
| Fail closed without evidence of an authenticated initial request | Decide and test any narrow dispatcher-type exceptions |
64+
| Preserve the Spring Security principal across dispatch threads | Propagate additional tenant/domain context when required |
65+
66+
## Source-backed decisions
67+
68+
The complete auditable research record, including upstream source/tests, issue evidence, and the
69+
alternatives matrix, is in [async and streaming security research](../research/async-streaming-security-sources.md).
70+
71+
- [Jakarta Servlet 6.1](https://jakarta.ee/specifications/servlet/6.1/jakarta-servlet-spec-6.1.pdf): `ASYNC` is a dispatch of the same request, supporting request attributes rather than token replay or sessions.
72+
- [Spring Framework async MVC](https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-async.html): documents final dispatch and emitter `IOException` handling.
73+
- [Spring Security context persistence](https://docs.spring.io/spring-security/reference/7.0/servlet/authentication/persistence.html): defines request-attribute persistence and explicit saving for custom authentication.
74+
- [Spring Security authorization](https://docs.spring.io/spring-security/reference/7.0/servlet/authorization/authorize-http-requests.html): dispatcher authorization remains application policy.
75+
- [Spring Security issue 12758](https://github.com/spring-projects/spring-security/issues/12758): maintainers prescribe this repository for the equivalent JWT and `StreamingResponseBody` failure.
76+
- [Spring Framework issue 33439](https://github.com/spring-projects/spring-framework/issues/33439): disconnect timing is network/container dependent.
77+
78+
## Migration
79+
80+
Synchronous integrations keep their behavior. Async applications must install a
81+
`RequestAttributeSecurityContextRepository` in `HttpSecurity` as shown above. Remove broad
82+
`dispatcherTypeMatchers(ASYNC, ERROR).permitAll()` workarounds after verifying application error
83+
routes. No token, cookie, route, or authorization contract changes are required.

docs/guides/authentication.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,13 @@ Vigil auto-configures `VigilAuthenticationFilter`. Add it inside the application
2323
```java
2424
@Bean
2525
SecurityFilterChain securityFilterChain(
26-
HttpSecurity http, VigilAuthenticationFilter vigilAuthenticationFilter) throws Exception {
26+
HttpSecurity http,
27+
VigilAuthenticationFilter vigilAuthenticationFilter) throws Exception {
28+
var requestSecurityContextRepository = new RequestAttributeSecurityContextRepository();
29+
vigilAuthenticationFilter.setSecurityContextRepository(requestSecurityContextRepository);
2730
return http
31+
.securityContext(context ->
32+
context.securityContextRepository(requestSecurityContextRepository))
2833
.authorizeHttpRequests(authorize -> authorize
2934
.requestMatchers("/auth/**").permitAll()
3035
.anyRequest().authenticated())
@@ -33,6 +38,10 @@ SecurityFilterChain securityFilterChain(
3338
}
3439
```
3540

41+
The request-scoped repository is required for MVC async and streaming return types. See
42+
[async and streaming security](async-streaming-security.md) for the stateless lifecycle and
43+
dispatcher authorization model.
44+
3645
`ignored-paths` bypasses Vigil entirely. `public-paths` permits an anonymous request while making a valid existing authentication available to the application. Neither setting replaces `authorizeHttpRequests`.
3746

3847
## 3. Issue tokens after application credential validation

docs/releases/release-policy.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ between releases when tests and public behavior remain stable.
3535
Current tested compatibility envelope:
3636

3737
- Java 25;
38-
- Spring Boot 4.1.x;
39-
- Spring Framework 7.x through Spring Boot 4.1.x;
40-
- Spring Security 7.1.x through Spring Boot 4.1.x;
38+
- Spring Boot 4.1.0;
39+
- Spring Framework 7.0.8 through the Spring Boot 4.1.0 BOM;
40+
- Spring Security 7.1.0 through the Spring Boot 4.1.0 BOM;
4141
- Jackson 3 through `tools.jackson` packages;
4242
- Gradle 9.6.x wrapper;
4343
- HS256 with a configured 256-bit minimum secret;
4444
- RS256 with configured PEM private/public keys and JWKS publication.
4545

46-
Vigil `7.0.x` is the active supported platform line. Vigil `6.0.x` was the
46+
Vigil `7.1.x` is the active supported platform line. Vigil `6.0.x` was the
4747
final Spring Boot 3.5.x / Java 21 line and is not supported for Spring Boot
4848
4.1 consumers. Do not add compatibility shims between the two lines; Boot 4
4949
changes the default JSON stack to Jackson 3 and modularizes several Boot

0 commit comments

Comments
 (0)