Skip to content

feat(vaadin-spring): add Vaadin-aware expired session handling - #25625

Merged
platosha merged 5 commits into
mainfrom
feat/uidl-expired-session-strategy
Sep 16, 2026
Merged

platosha merged 5 commits into
mainfrom
feat/uidl-expired-session-strategy

Conversation

@totally-not-ai

@totally-not-ai totally-not-ai Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Summary

When Spring Security concurrency control expires a session, the default Spring response is a plain text page that the Vaadin client cannot use. This change makes Vaadin handle such a request itself, so the browser gets a proper session expired message, and it is now applied by default.

What changed

Behavior change: applications that enable Spring Security concurrency control (maximumSessions(...)) through VaadinSecurityConfigurer now get VaadinExpiredSessionStrategy instead of Spring Security's default. The configured strategy also replaces an expiredSessionStrategy or expiredUrl set directly on HttpSecurity, because Spring Security only uses the expired URL when no strategy is set. Applications without concurrency control are not affected, because Spring Security only creates the ConcurrentSessionFilter when a maximum number of sessions is set.

  • New VaadinExpiredSessionStrategy. ConcurrentSessionFilter has already logged the user out and invalidated the HTTP session, so the strategy simply continues the filter chain carried by the SessionInformationExpiredEvent. The request is then answered like any other request without a session: Flow writes the sessionExpired message for a UIDL request, 403 for a heartbeat, the push handler answers a push request, and a request for a view ends in the login view with the saved request intact.
  • If the event carries no filter chain, the strategy logs a debug message and redirects to the application root, using a RedirectStrategy so a context path is respected.
  • VaadinSecurityConfigurer installs the strategy on the session management configuration, but only when the application actually has session management configured (Spring Boot does by default).
  • Two new opt-outs: enableSessionManagementConfiguration(false) turns the whole session management customization off, and expiredSessionStrategy(...) sets a custom strategy.

Use case

An application limits each user to one active session. When a user logs in on a second device, the first browser tab must show Vaadin's "session expired" dialog and reload, instead of silently breaking. With this change the developer only configures the session limit and gets the correct client behavior:

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.with(VaadinSecurityConfigurer.vaadin(),
                        configurer -> configurer.loginView("/login"))
                .sessionManagement(sessionManagement -> sessionManagement
                        .sessionConcurrency(concurrency -> concurrency
                                .maximumSessions(1)))
                .build();
    }
}

To handle expiration differently, for example to send the user to a custom page, pass an own strategy:

http.with(VaadinSecurityConfigurer.vaadin(), configurer -> configurer
        .expiredSessionStrategy(event -> event.getResponse()
                .sendRedirect("/session-expired")));

API Changes

com.vaadin.flow.spring.security.VaadinExpiredSessionStrategy

// Added
public class VaadinExpiredSessionStrategy implements SessionInformationExpiredStrategy
public VaadinExpiredSessionStrategy()
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException

com.vaadin.flow.spring.security.VaadinSecurityConfigurer

// Added
public VaadinSecurityConfigurer enableSessionManagementConfiguration(boolean enableSessionManagementConfiguration) // opt out of the session management customization
public VaadinSecurityConfigurer expiredSessionStrategy(SessionInformationExpiredStrategy expiredSessionStrategy) // null falls back to VaadinExpiredSessionStrategy

Test summary

# Status What the test verifies Why it matters
1 The strategy calls doFilter on the event's filter chain and writes nothing to the response (no body, no redirect) This is the whole fix: only an untouched response lets Flow answer with sessionExpired, 403 or a push response
2 With no filter chain in the event, the response redirects to the context root (/app/ for context path /app) Fallback must not produce a broken //app URL for an application under a context path
3 With concurrency control enabled, an expired UIDL request run through the real ConcurrentSessionFilter continues the chain and leaves the body empty Pins the default wiring end to end — this is what every application gets without extra code
4 expiredSessionStrategy(...) replaces the default: the custom strategy writes the body and the chain is never called An application must still be able to define its own expiration handling
5 enableSessionManagementConfiguration(false) restores Spring's default response ("This session has been expired…") The opt-out must really opt out, so existing setups can keep old behavior
  • VaadinExpiredSessionStrategyTest.filterChainAvailable_requestContinues → 1
  • VaadinExpiredSessionStrategyTest.noFilterChain_redirectsToApplicationRoot → 2
  • VaadinSecurityConfigurerTest.sessionConcurrency_expiredUidlRequest_continuesThroughFilterChain → 3
  • VaadinSecurityConfigurerTest.expiredSessionStrategy_customStrategyIsUsed → 4
  • VaadinSecurityConfigurerTest.sessionManagementConfigurationDisabled_springDefaultIsUsed → 5

Left untested on purpose: the debug log message in the no-filter-chain branch, and the fact that the strategy is skipped when the application has no session management configured (covered indirectly by the existing configurer tests). SpringClassesSerializableTest only gets the new class added to its known list, so it is not a behavior test.

Port UidlExpiredSessionStrategy from SSO Kit so that a session expired by
Spring Security concurrency control ends in a client-side reload instead of
a redirect the Vaadin client cannot follow: for framework internal requests
it writes a Vaadin-Refresh token into the response body, other requests are
redirected.

Unlike the SSO Kit original, the destination URL is context-relative and the
redirect goes through a RedirectStrategy, so an application deployed under a
context path gets /app/ instead of //app.
VaadinSecurityConfigurer now installs UidlExpiredSessionStrategy on the
session management configuration, so an application that turns on Spring
Security concurrency control gets Vaadin-aware session expiration without
extra wiring. Spring Security only creates the ConcurrentSessionFilter when
a maximum number of sessions is set, so this is a no-op otherwise.

Session management is only customized when the application has it
configured, which Spring Boot does by default, and can be turned off with
enableSessionManagementConfiguration(false). A custom strategy can be set
with expiredSessionStrategy(SessionInformationExpiredStrategy).
@totally-not-ai

totally-not-ai Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Type of change

  • Feature

How to test

  1. In flow-tests/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/SecurityConfig.java,
    add http.sessionManagement(s -> s.sessionConcurrency(c -> c.maximumSessions(1)))
    to the filter chain.
  2. Start the app and log in as john in one browser.
  3. Log in as john in a second browser (or a private window).
  4. Go back to the first browser and click a link in the app: the session
    expired notification appears and the page reloads to the login view,
    instead of the UI silently stopping.
API changes

com.vaadin.flow.spring.security.VaadinExpiredSessionStrategy

// Added
public class VaadinExpiredSessionStrategy implements SessionInformationExpiredStrategy
public VaadinExpiredSessionStrategy()
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException

com.vaadin.flow.spring.security.VaadinSecurityConfigurer

// Added
public VaadinSecurityConfigurer enableSessionManagementConfiguration(boolean enableSessionManagementConfiguration)
public VaadinSecurityConfigurer expiredSessionStrategy(SessionInformationExpiredStrategy expiredSessionStrategy)
Test coverage
  • VaadinExpiredSessionStrategyTest: the request continues through the filter
    chain and nothing is written to the response; without a filter chain the
    browser is redirected to the application root, context path included.
  • VaadinSecurityConfigurerTest: builds a real filter chain with
    concurrency control on and drives its ConcurrentSessionFilter with an
    expired session. An expired UIDL request continues down the chain; a
    strategy passed to expiredSessionStrategy(...) is used instead; and with
    enableSessionManagementConfiguration(false) the Spring Security default
    answers.
  • mvn -pl vaadin-spring test: 466 tests, all green except the pre-existing
    VaadinServletContextInitializerTest unicode-path failure, which also
    fails on main in this environment.
Follow-ups
  • A Vaadin-aware InvalidSessionStrategy would cover a plain session
    timeout; this PR only covers a session expired by concurrency control.
  • SSO Kit can delegate to this class once it targets a Vaadin version that
    has it.

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Test Results

 1 453 files  +1   1 537 suites  +1   1h 37m 13s ⏱️ +25s
11 908 tests +5  11 840 ✅ +5  68 💤 ±0  0 ❌ ±0 
12 226 runs  +5  12 158 ✅ +5  68 💤 ±0  0 ❌ ±0 

Results for commit 7f40b2d. ± Comparison against base commit dd7f0f6.

♻️ This comment has been updated with latest results.

totally-not-ai Bot added a commit to vaadin/docs that referenced this pull request Sep 10, 2026
…ions

vaadin/flow#25625 adds UidlExpiredSessionStrategy and makes
VaadinSecurityConfigurer install it by default, and vaadin/flow#25627 adds
KeycloakOidcUserMapper behind a keycloakRoleMapping() opt-in. Both were
ported from SSO Kit, so two of the migration gaps close.

Moves the two features out of the gaps section and into the migration steps
that need them, with a since badge for the version they arrive in and the
previous manual approach kept in a note for earlier versions. Updates the
feature mapping table and the checklist to match.

@heruan heruan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description says the app "got a plain redirect". Spring's default writes a 200 plain text body; the client fails to parse it as JSON and stops. Also, this is a port from SSO Kit rather than a new feature, the description should say so.

Inline comments are for discussion, nothing blocking. If the pass-through approach is taken, the docs PR (vaadin/docs#5950) only needs a wording change in Step 5.

…vlet

Instead of writing a Vaadin-Refresh token by hand,
UidlExpiredSessionStrategy now continues the filter chain carried by the
SessionInformationExpiredEvent. ConcurrentSessionFilter has already logged
the user out and invalidated the HTTP session, so the request is answered
like any other request that arrives without a session: Flow's session
expired handlers write the sessionExpired message for a UIDL request, 403
for a heartbeat and the push handler answers a push request, while a
request for a view ends in the login view with the saved request intact.

This removes the destination URL, the context path arithmetic, the redirect
strategy and the copied Vaadin-Refresh literal from the class, and fixes
heartbeat requests, which never read the response body.
@totally-not-ai

Copy link
Copy Markdown
Contributor Author

@heruan Description updated: it now says that Spring Security answers with a 200 and a plain text body the Vaadin client cannot read as JSON, so the UI stops responding until a manual reload, and that this moves the handling SSO Kit has shipped since 1.0 into Flow. The bullets and the risk list follow the pass-through approach, and the first comment has the reduced API surface and the new test coverage.

On the docs PR: the pass-through approach is in, so vaadin/docs#5950 needs the wording change in Step 5.

The strategy no longer inspects the request type, so the Uidl prefix
claimed a check the class does not make. The new name says who the
strategy is for, like VaadinDefaultRequestCache.
heruan
heruan previously approved these changes Sep 15, 2026

@heruan heruan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description still names UidlExpiredSessionStrategy in the summary and in the API changes section. Update it to the new name before merging. The docs PR (vaadin/docs#5950) needs the same rename and the Step 5 wording change.

@mshabarov

Copy link
Copy Markdown
Contributor

@totally-not-ai resolve conflicts with main

@mshabarov

Copy link
Copy Markdown
Contributor

@totally-not-ai address #25625 (review) comment

…sion-strategy

# Conflicts:
#	vaadin-spring/src/test/java/com/vaadin/flow/spring/security/VaadinSecurityConfigurerTest.java
@totally-not-ai

Copy link
Copy Markdown
Contributor Author

@mshabarov Both done.

Conflicts: main is merged into the branch. The only conflict was in VaadinSecurityConfigurerTest, where both sides had added tests at the same place; both sets are kept. mvn -pl vaadin-spring test runs 471 tests with no failures, apart from a unicode-path failure in VaadinServletContextInitializerTest that also fails on main in this environment.

The approving review: the description named the class by its old name in the summary and in the API changes section, and now uses VaadinExpiredSessionStrategy throughout.

@heruan The migration guide in vaadin/docs#5950 is updated too: the replacement class in the mapping table and the Step 5 text now use the new name, and Step 5 explains that the expired request is passed to the servlet, so Flow answers a UIDL request with its session expired message and a heartbeat with 403, instead of mentioning the Vaadin-Refresh token. The row for the kit's own class keeps its original name, since that is what applications are migrating from.

@sonarqubecloud

Copy link
Copy Markdown

@platosha
platosha added this pull request to the merge queue Sep 16, 2026
Merged via the queue into main with commit 9170c2e Sep 16, 2026
71 of 73 checks passed
@platosha
platosha deleted the feat/uidl-expired-session-strategy branch September 16, 2026 12:30
vaadin-bot added a commit to vaadin/docs that referenced this pull request Sep 16, 2026
Source PR: feat(vaadin-spring): add Vaadin-aware expired session handling
Source commit: 7f40b2d6cde21efc0ab12559fe29d9ee091e6ee6
Categories: NEW_FEATURE, BEHAVIOR_CHANGE, API_CHANGE
@github-actions

Copy link
Copy Markdown
Contributor

Pull request created: #6053

Generated by Documentation Bot · agent · 88.6 AIC · ⌖ 5.13 AIC · ⊞ 8.5K

@github-actions

Copy link
Copy Markdown
Contributor

Documentation Bot: Draft documentation pull request for this change: vaadin/docs — branch doc-bot/vaadin-flow/25625 (PR created by this run; see the vaadin/docs repository for the number).

Files updated:

  • articles/flow/security/vaadin-security-configurer.adoc

It was written from the state of this pull request as you see it now. Please review it and mark it ready for review.

Generated by Documentation Bot for #25625 · agent · 88.6 AIC · ⌖ 5.13 AIC · ⊞ 8.5K ·

mshabarov pushed a commit to vaadin/docs that referenced this pull request Sep 21, 2026
…adin-aware expired session handling (#6053)

Documentation for vaadin/flow#25625 by `@totally-not-ai`[bot].

> [!NOTE]
> The source pull request is merged, so this documentation describes the
final
> shape of the change. Please review it and mark it ready for review.

**Change categories:** NEW_FEATURE, BEHAVIOR_CHANGE, API_CHANGE

| File | Change |
|------|--------|
| `articles/flow/security/vaadin-security-configurer.adoc` | Documented
the new `SessionManagementConfigurer` entry in "Applied Configurers",
the new `enableSessionManagementConfiguration()` and
`expiredSessionStrategy()` methods, a new "Handling Expired Sessions"
example showing `maximumSessions(1)` with a custom strategy, and the
corresponding entry in "Features That Can Be Disabled". |

Auto-generated by the Documentation Bot — review before merging.
Anything marked `TODO: Verify` needs a closer look.

> Generated by [Documentation
Bot](https://github.com/vaadin/flow/actions/runs/35096221440) for #25625
· agent · 88.6 AIC · ⌖ 5.13 AIC · ⊞ 8.5K ·
[◷](https://github.com/search?q=repo%3Avaadin%2Fdocs+%22gh-aw-workflow-id%3A+doc-bot%22&type=pullrequests)
> - [x] expires <!-- gh-aw-expires: 2026-10-16T12:40:33.761Z --> on Oct
16, 2026, 12:40 PM UTC

<!-- gh-aw-agentic-workflow: Documentation Bot, engine: claude, model:
agent, id: 35096221440, workflow_id: doc-bot, run:
https://github.com/vaadin/flow/actions/runs/35096221440 -->

<!-- gh-aw-expires-type: pull-request -->

<!-- gh-aw-workflow-id: doc-bot -->
<!-- gh-aw-workflow-call-id: vaadin/flow/doc-bot -->
peholmst pushed a commit to vaadin/docs that referenced this pull request Sep 21, 2026
…adin-aware expired session handling (#6053) (CP: v25.3) (#6103)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants