From 9761060f8e6d10b214029547d9576a67bd4a8c11 Mon Sep 17 00:00:00 2001 From: mikhail Date: Mon, 21 Sep 2026 11:49:21 +0300 Subject: [PATCH 1/4] docs: record the Spring guarantees of the browserless environment Two Spring behaviors that tests depend on were undocumented: a @SessionScope or @RequestScope bean is resolved per user in multi-user tests, where the second user used to reuse the first user's instance, and the mocked request rotates the session ID on login instead of throwing, so session-fixation protection can be tested. Co-Authored-By: Claude Opus 5 (1M context) --- .../flow/testing/browserless/multi-user.adoc | 1 + .../testing/browserless/spring-security.adoc | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/articles/flow/testing/browserless/multi-user.adoc b/articles/flow/testing/browserless/multi-user.adoc index 5d3b3ad2ad..9c654d9a7f 100644 --- a/articles/flow/testing/browserless/multi-user.adoc +++ b/articles/flow/testing/browserless/multi-user.adoc @@ -370,6 +370,7 @@ Common gotchas worth keeping in mind: - *No parallel access.* Every context is thread-affine. Driving the same [classname]`BrowserlessApplicationContext` from multiple threads in the same test is unsupported. - *Security snapshot is per user, not per window.* Two windows of the same user share one snapshot; a security mutation made while one window is active -- including a logout -- is visible to the user's other windows, both already open and opened afterwards. The snapshot is re-captured only on cross-user switches. - *Calling Vaadin APIs directly.* DSL methods on [classname]`BrowserlessUIContext` activate the window automatically. If the test reaches for [methodname]`UI.getCurrent()`, [methodname]`VaadinSession.getCurrent()`, or [classname]`SecurityContextHolder` between DSL calls, call [methodname]`window.activate()` first to make sure the thread-locals reflect the intended window. +- [since:com.vaadin:vaadin@V25.2]#*Spring request and session scopes follow the user.*# A [annotationname]`@SessionScope` or [annotationname]`@RequestScope` bean is resolved against the active user's own request, so two users never share one instance. [annotationname]`@VaadinSessionScope` has always behaved this way, because it resolves against [methodname]`VaadinSession.getCurrent()`. - *Anonymous users still go through the handler.* On a secured context, [methodname]`newUser()` with no arguments delegates to the handler, which installs its anonymous-equivalent state (for example, Spring's [classname]`AnonymousAuthenticationToken`). diff --git a/articles/flow/testing/browserless/spring-security.adoc b/articles/flow/testing/browserless/spring-security.adoc index 58dfbd9a9e..ba2943820c 100644 --- a/articles/flow/testing/browserless/spring-security.adoc +++ b/articles/flow/testing/browserless/spring-security.adoc @@ -172,6 +172,24 @@ class SecurityTestConfig { ---- +[role="since:com.vaadin:vaadin@V25.3"] +== Session Fixation Protection + +Spring Security gives the session a new ID when a user authenticates, and an application without Spring Security does the same by calling [methodname]`HttpServletRequest.changeSessionId()` after a successful login. The mocked request performs that rotation instead of refusing it, so a test can drive a login flow that protects against session fixation. + +The session itself survives the rotation: the attributes stay, and so does the [classname]`VaadinSession` bound to it. Only the ID changes. + +[source,java] +---- +String idBeforeLogin = VaadinSession.getCurrent().getSession().getId(); + +// Log in through the application's own flow + +Assertions.assertNotEquals(idBeforeLogin, + VaadinSession.getCurrent().getSession().getId()); +---- + + .Multiple Users or Windows? [TIP] This setup drives a single user with a single window. For tests that need several concurrent users or multiple windows of the same user, see <>. From c8c5dc8b22b8f674db17e96e50da53e8f04b7e70 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:54:15 +0000 Subject: [PATCH 2/4] docs: note the injection limitation of Vaadin session scoped beans A @VaadinSessionScope bean has no scoped proxy, so Spring resolves it when it injects the test class field, before the test method has created the application context and its sessions. Record that, the error it produces for every test in the class, and resolving the bean from the test method instead. Spring's own @SessionScope and @RequestScope are proxied and do not have the problem. Co-Authored-By: Claude Opus 5 (1M context) --- articles/flow/testing/browserless/multi-user.adoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/articles/flow/testing/browserless/multi-user.adoc b/articles/flow/testing/browserless/multi-user.adoc index 9c654d9a7f..1ba9e99929 100644 --- a/articles/flow/testing/browserless/multi-user.adoc +++ b/articles/flow/testing/browserless/multi-user.adoc @@ -370,7 +370,8 @@ Common gotchas worth keeping in mind: - *No parallel access.* Every context is thread-affine. Driving the same [classname]`BrowserlessApplicationContext` from multiple threads in the same test is unsupported. - *Security snapshot is per user, not per window.* Two windows of the same user share one snapshot; a security mutation made while one window is active -- including a logout -- is visible to the user's other windows, both already open and opened afterwards. The snapshot is re-captured only on cross-user switches. - *Calling Vaadin APIs directly.* DSL methods on [classname]`BrowserlessUIContext` activate the window automatically. If the test reaches for [methodname]`UI.getCurrent()`, [methodname]`VaadinSession.getCurrent()`, or [classname]`SecurityContextHolder` between DSL calls, call [methodname]`window.activate()` first to make sure the thread-locals reflect the intended window. -- [since:com.vaadin:vaadin@V25.2]#*Spring request and session scopes follow the user.*# A [annotationname]`@SessionScope` or [annotationname]`@RequestScope` bean is resolved against the active user's own request, so two users never share one instance. [annotationname]`@VaadinSessionScope` has always behaved this way, because it resolves against [methodname]`VaadinSession.getCurrent()`. +- [since:com.vaadin:vaadin@V25.2]#*Spring request and session scopes follow the user.*# A [annotationname]`@SessionScope` or [annotationname]`@RequestScope` bean is resolved against the active user's own request, so two users never share one instance. Both are injected as scoped proxies, which resolve on every call, so even a field autowired into the test class reads the active user's instance. [annotationname]`@VaadinSessionScope` has always been per user as well, because it resolves against [methodname]`VaadinSession.getCurrent()`. +- *A [annotationname]`@VaadinSessionScope` bean can't be autowired into the test class.* Unlike Spring's own scopes, it has no scoped proxy, so Spring resolves the bean itself while injecting the field -- which happens before the test method creates the application context, and therefore before any session exists. Spring fails with `Scope 'vaadin-session' is not active for the current thread`, and because the failure is in the test instance itself, it fails every test in the class, including the ones that never touch the bean. Resolve the bean inside the test method with [methodname]`ApplicationContext.getBean()` instead, with the window of the intended user active; each user gets its own instance. - *Anonymous users still go through the handler.* On a secured context, [methodname]`newUser()` with no arguments delegates to the handler, which installs its anonymous-equivalent state (for example, Spring's [classname]`AnonymousAuthenticationToken`). From ff43adb3719c78df2311ab45bb2b3bca9959561a Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 14:00:22 +0000 Subject: [PATCH 3/4] docs: keep the single-user tip with its setup, tighten the scope pitfall The Session Fixation Protection section split the Spring Security page's setup from the closing tip that refers to it, so the section now comes after the tip. The new multi-user pitfall bullet is also split into shorter sentences to match the rest of the list. Co-Authored-By: Claude Opus 5 (1M context) --- articles/flow/testing/browserless/multi-user.adoc | 2 +- articles/flow/testing/browserless/spring-security.adoc | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/articles/flow/testing/browserless/multi-user.adoc b/articles/flow/testing/browserless/multi-user.adoc index 1ba9e99929..6611ce564b 100644 --- a/articles/flow/testing/browserless/multi-user.adoc +++ b/articles/flow/testing/browserless/multi-user.adoc @@ -371,7 +371,7 @@ Common gotchas worth keeping in mind: - *Security snapshot is per user, not per window.* Two windows of the same user share one snapshot; a security mutation made while one window is active -- including a logout -- is visible to the user's other windows, both already open and opened afterwards. The snapshot is re-captured only on cross-user switches. - *Calling Vaadin APIs directly.* DSL methods on [classname]`BrowserlessUIContext` activate the window automatically. If the test reaches for [methodname]`UI.getCurrent()`, [methodname]`VaadinSession.getCurrent()`, or [classname]`SecurityContextHolder` between DSL calls, call [methodname]`window.activate()` first to make sure the thread-locals reflect the intended window. - [since:com.vaadin:vaadin@V25.2]#*Spring request and session scopes follow the user.*# A [annotationname]`@SessionScope` or [annotationname]`@RequestScope` bean is resolved against the active user's own request, so two users never share one instance. Both are injected as scoped proxies, which resolve on every call, so even a field autowired into the test class reads the active user's instance. [annotationname]`@VaadinSessionScope` has always been per user as well, because it resolves against [methodname]`VaadinSession.getCurrent()`. -- *A [annotationname]`@VaadinSessionScope` bean can't be autowired into the test class.* Unlike Spring's own scopes, it has no scoped proxy, so Spring resolves the bean itself while injecting the field -- which happens before the test method creates the application context, and therefore before any session exists. Spring fails with `Scope 'vaadin-session' is not active for the current thread`, and because the failure is in the test instance itself, it fails every test in the class, including the ones that never touch the bean. Resolve the bean inside the test method with [methodname]`ApplicationContext.getBean()` instead, with the window of the intended user active; each user gets its own instance. +- *A [annotationname]`@VaadinSessionScope` bean can't be autowired into the test class.* Unlike Spring's own scopes, it has no scoped proxy. Spring therefore resolves the bean while injecting the field, before the test method has created the application context and its sessions. The error is `Scope 'vaadin-session' is not active for the current thread`, and it fails every test in the class, including the ones that never touch the bean. Resolve the bean inside the test method with [methodname]`ApplicationContext.getBean()` instead, with the intended user's window active; each user gets its own instance. - *Anonymous users still go through the handler.* On a secured context, [methodname]`newUser()` with no arguments delegates to the handler, which installs its anonymous-equivalent state (for example, Spring's [classname]`AnonymousAuthenticationToken`). diff --git a/articles/flow/testing/browserless/spring-security.adoc b/articles/flow/testing/browserless/spring-security.adoc index ba2943820c..0e5d9d4466 100644 --- a/articles/flow/testing/browserless/spring-security.adoc +++ b/articles/flow/testing/browserless/spring-security.adoc @@ -172,6 +172,11 @@ class SecurityTestConfig { ---- +.Multiple Users or Windows? +[TIP] +This setup drives a single user with a single window. For tests that need several concurrent users or multiple windows of the same user, see <>. + + [role="since:com.vaadin:vaadin@V25.3"] == Session Fixation Protection @@ -190,9 +195,4 @@ Assertions.assertNotEquals(idBeforeLogin, ---- -.Multiple Users or Windows? -[TIP] -This setup drives a single user with a single window. For tests that need several concurrent users or multiple windows of the same user, see <>. - - [discussion-id]`2A8B3F1E-9C47-4D5A-B621-8E3F2A7C9D10` From b76a4179bd8d64cdbc416def4584626b660dd0d1 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 14:11:47 +0000 Subject: [PATCH 4/4] docs: point at the environment differences page for bean injection The session-scoped bean injection limitation is documented on the Environment Differences page, so the multi-user pitfall links there instead of repeating it, and keeps only the multi-user specific part: a bean resolved from the test method belongs to the active user. Co-Authored-By: Claude Opus 5 (1M context) --- articles/flow/testing/browserless/multi-user.adoc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/articles/flow/testing/browserless/multi-user.adoc b/articles/flow/testing/browserless/multi-user.adoc index 6ac8c01711..c56235f89e 100644 --- a/articles/flow/testing/browserless/multi-user.adoc +++ b/articles/flow/testing/browserless/multi-user.adoc @@ -381,8 +381,7 @@ Common gotchas worth keeping in mind: - *No parallel access.* Every context is thread-affine. Driving the same [classname]`BrowserlessApplicationContext` from multiple threads in the same test is unsupported. - *Security snapshot is per user, not per window.* Two windows of the same user share one snapshot; a security mutation made while one window is active -- including a logout -- is visible to the user's other windows, both already open and opened afterwards. The snapshot is re-captured only on cross-user switches. - *Calling Vaadin APIs directly.* DSL methods on [classname]`BrowserlessUIContext` activate the window automatically. If the test reaches for [methodname]`UI.getCurrent()`, [methodname]`VaadinSession.getCurrent()`, or [classname]`SecurityContextHolder` between DSL calls, call [methodname]`window.activate()` first to make sure the thread-locals reflect the intended window. -- [since:com.vaadin:vaadin@V25.2]#*Spring request and session scopes follow the user.*# A [annotationname]`@SessionScope` or [annotationname]`@RequestScope` bean is resolved against the active user's own request, so two users never share one instance. Both are injected as scoped proxies, which resolve on every call, so even a field autowired into the test class reads the active user's instance. [annotationname]`@VaadinSessionScope` has always been per user as well, because it resolves against [methodname]`VaadinSession.getCurrent()`. -- *A [annotationname]`@VaadinSessionScope` bean can't be autowired into the test class.* Unlike Spring's own scopes, it has no scoped proxy. Spring therefore resolves the bean while injecting the field, before the test method has created the application context and its sessions. The error is `Scope 'vaadin-session' is not active for the current thread`, and it fails every test in the class, including the ones that never touch the bean. Resolve the bean inside the test method with [methodname]`ApplicationContext.getBean()` instead, with the intended user's window active; each user gets its own instance. +- [since:com.vaadin:vaadin@V25.2]#*Spring request and session scopes follow the user.*# A [annotationname]`@SessionScope` or [annotationname]`@RequestScope` bean is resolved against the active user's own request, so two users never share one instance. [annotationname]`@VaadinSessionScope` has always behaved this way, because it resolves against [methodname]`VaadinSession.getCurrent()`. See <> for why such a bean can't be autowired into a test class field. Resolving it from the test method instead gives the active user's instance, so activate the intended user's window first. - *Anonymous users still go through the handler.* On a secured context, [methodname]`newUser()` with no arguments delegates to the handler, which installs its anonymous-equivalent state (for example, Spring's [classname]`AnonymousAuthenticationToken`).