enhancement
major
minor
defect
critical
major
minor
major
#30067
React Interface: Logging in in one browser tab immediately logs you out if a second tab for the application is open
Problem
The application (React interface) is open in two browser tabs, both of which display the login dialog. If you log in to one tab with your username and password, the other tab reloads, and you are immediately logged out of the first tab.
Cause
Both tabs share an HTTP session `S` via the session cookie . A password-based login only recorded the login (`PendingSessionAction.requestLogin`); the subsequent reload invalidated `S`, created a new session `S2` for the user, and sent its cookie.
- Invalidating ` S ` also immediately closes the event stream of the second tab.
- The second tab then reloads (stale-ui)—still with the `S` cookie , because the response containing the `S2` cookie has not yet reached the browser.
- ViewServlet.handleNoSession creates a new '''anonymous''' session for the unknown `S`; its cookie overwrites `S2` in the browser—the first tab is also logged out.
#29964 fixed the same race condition within a single tab, but not for a second tab that shares the cookie.
Logging in via OpenID did not replace the session, but it did replace its `TLSessionContext`: an already open anonymous tab was silently converted to the logged-in user upon the next page load.
Solution
Logging in via the React interface no longer replaces the HTTP session but applies to the browser window (sub-session) in which it occurs:
- Other tabs retain their user. An already open incognito tab remains incognito and is not reloaded; different users can be logged in across different tabs. A tab opened afterward starts with the most recently logged-in user. Logging out still replaces the session and thereby logs out all tabs.
- SessionService.loginUserInWindow(request, response, person) logs a user into the window of the current interaction: the HTTP session and TLSessionContext are preserved, and the user is set in the window’s sub-session (with their language) and as the original user of the session. To prevent session fixation, the session ID is changed (HttpServletRequest.changeSessionId()); registration in the SessionService follows the new ID. The previous user’s login record is terminated under the old ID (LOGGED_OUT), and the login under the new ID is registered (LOGGED_IN).
- When a user logs out, the SessionEndReloadListener reloads the windows only if the session is no longer registered—the end of a login record within an ongoing session does not constitute the end of the session.
- The ReactWindowRegistry tracks a change in the session ID (SessionIdChangeListener, an HttpSessionIdListener in the web-fragment.xml file of tl-layout-react); similarly, the MaintenanceWindowManager remembers the session itself rather than its ID.
- Password-based login: LoginAction logs in directly via the login command; the response to the command sends the new session ID to the browser. The client does not leave the page until all previously sent commands have been answered (leavePageWhenAnswered), so that the reload includes the new cookie. PendingSessionAction is now used only for logout.
- External login (OpenID): The login button transmits the window name as the `loginWindow` parameter (ExternalAuthenticationServlet.WINDOW_PARAM) and returns to the same window (/view/<window>/<route>). If the login refers to a specific window within the session, ExternalAuthenticationServlet logs the user in only to that window via `loginUserInWindow`; otherwise, it logs the user in to the entire session as before.
- A window’s component tree is reused only for the same user (RenderedView); after a login, the window is rebuilt for the new user.
The lack of session ID rotation during external login without a window (classic interface) is addressed in #30069.
Migration
A custom implementation of com.top_logic.base.accesscontrol.loginmethod.LoginMethod must replace getInitiationUrl(String returnToUrl) with getInitiationUrl(String returnToUrl, String windowName) and include the window name (if not null) as the ExternalAuthenticationServlet.WINDOW_PARAM in the initiation URL; see Pac4jLoginMethod.