feat(vaadin-spring): add Vaadin-aware expired session handling - #25625
Conversation
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).
Type of change
How to test
API changescom.vaadin.flow.spring.security.VaadinExpiredSessionStrategy// Added
public class VaadinExpiredSessionStrategy implements SessionInformationExpiredStrategy
public VaadinExpiredSessionStrategy()
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletExceptioncom.vaadin.flow.spring.security.VaadinSecurityConfigurer// Added
public VaadinSecurityConfigurer enableSessionManagementConfiguration(boolean enableSessionManagementConfiguration)
public VaadinSecurityConfigurer expiredSessionStrategy(SessionInformationExpiredStrategy expiredSessionStrategy)Test coverage
Follow-ups
|
…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.
There was a problem hiding this comment.
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.
|
@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
left a comment
There was a problem hiding this comment.
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.
|
@totally-not-ai resolve conflicts with main |
|
@totally-not-ai address #25625 (review) comment |
…sion-strategy # Conflicts: # vaadin-spring/src/test/java/com/vaadin/flow/spring/security/VaadinSecurityConfigurerTest.java
|
@mshabarov Both done. Conflicts: The approving review: the description named the class by its old name in the summary and in the API changes section, and now uses @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 |
|
Source PR: feat(vaadin-spring): add Vaadin-aware expired session handling Source commit: 7f40b2d6cde21efc0ab12559fe29d9ee091e6ee6 Categories: NEW_FEATURE, BEHAVIOR_CHANGE, API_CHANGE
|
Pull request created: #6053
|
|
Documentation Bot: Draft documentation pull request for this change: vaadin/docs — branch Files updated:
It was written from the state of this pull request as you see it now. Please review it and mark it ready for review.
|
…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 -->



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(...)) throughVaadinSecurityConfigurernow getVaadinExpiredSessionStrategyinstead of Spring Security's default. The configured strategy also replaces anexpiredSessionStrategyorexpiredUrlset directly onHttpSecurity, 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 theConcurrentSessionFilterwhen a maximum number of sessions is set.VaadinExpiredSessionStrategy.ConcurrentSessionFilterhas already logged the user out and invalidated the HTTP session, so the strategy simply continues the filter chain carried by theSessionInformationExpiredEvent. The request is then answered like any other request without a session: Flow writes thesessionExpiredmessage for a UIDL request,403for 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.RedirectStrategyso a context path is respected.VaadinSecurityConfigurerinstalls the strategy on the session management configuration, but only when the application actually has session management configured (Spring Boot does by default).enableSessionManagementConfiguration(false)turns the whole session management customization off, andexpiredSessionStrategy(...)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:
To handle expiration differently, for example to send the user to a custom page, pass an own strategy:
API Changes
com.vaadin.flow.spring.security.VaadinExpiredSessionStrategy
com.vaadin.flow.spring.security.VaadinSecurityConfigurer
Test summary
doFilteron the event's filter chain and writes nothing to the response (no body, no redirect)sessionExpired,403or a push response/app/for context path/app)//appURL for an application under a context pathConcurrentSessionFiltercontinues the chain and leaves the body emptyexpiredSessionStrategy(...)replaces the default: the custom strategy writes the body and the chain is never calledenableSessionManagementConfiguration(false)restores Spring's default response ("This session has been expired…")VaadinExpiredSessionStrategyTest.filterChainAvailable_requestContinues→ 1VaadinExpiredSessionStrategyTest.noFilterChain_redirectsToApplicationRoot→ 2VaadinSecurityConfigurerTest.sessionConcurrency_expiredUidlRequest_continuesThroughFilterChain→ 3VaadinSecurityConfigurerTest.expiredSessionStrategy_customStrategyIsUsed→ 4VaadinSecurityConfigurerTest.sessionManagementConfigurationDisabled_springDefaultIsUsed→ 5Left 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).
SpringClassesSerializableTestonly gets the new class added to its known list, so it is not a behavior test.