enhancement
major
minor
defect
critical
major
minor
major
#30067
React-Oberfläche: Anmeldung in einem Browser-Tab meldet sofort wieder ab, wenn ein zweiter Tab der Anwendung offen ist
Problem
Die Anwendung (React-Oberfläche) ist in zwei Browser-Tabs geöffnet, beide zeigen den Anmeldedialog. Meldet man sich in einem Tab mit Benutzername und Passwort an, lädt der andere Tab neu, und im ersten Tab ist man sofort wieder abgemeldet.
Ursache
Beide Tabs teilen sich über das Session-Cookie eine HTTP-Session S`. Eine Passwort-Anmeldung vermerkte nur die Anmeldung (`PendingSessionAction.requestLogin); der anschließende Reload invalidierte S`, legte eine neue Session `S2 für den Benutzer an und sendete ihr Cookie.
- Das Invalidieren von S schließt sofort auch den Event-Stream des zweiten Tabs.
- Der zweite Tab lädt daraufhin neu (stale-ui) – noch mit dem Cookie S`, weil die Antwort mit dem Cookie von `S2 den Browser noch nicht erreicht hat.
- ViewServlet.handleNoSession legt für das unbekannte S` eine neue '''anonyme''' Session an; ihr Cookie überschreibt `S2 im Browser – auch der erste Tab ist abgemeldet.
#29964 hat dasselbe Rennen innerhalb eines Tabs behoben, aber nicht für einen zweiten Tab, der das Cookie teilt.
Die Anmeldung per OpenID tauschte die Session nicht aus, ersetzte aber ihren TLSessionContext: ein bereits offener anonymer Tab wurde beim nächsten Laden stillschweigend zum angemeldeten Benutzer.
Lösung
Eine Anmeldung in der React-Oberfläche tauscht die HTTP-Session nicht mehr aus, sondern gilt für das Browserfenster (Sub-Session), in dem sie stattfindet:
- Andere Tabs behalten ihren Benutzer. Ein bereits offener anonymer Tab bleibt anonym und wird nicht neu geladen; in verschiedenen Tabs können verschiedene Benutzer angemeldet sein. Ein danach geöffneter Tab startet mit dem zuletzt angemeldeten Benutzer. Die Abmeldung tauscht weiterhin die Session aus und meldet damit alle Tabs ab.
- SessionService.loginUserInWindow(request, response, person) meldet einen Benutzer im Fenster der aktuellen Interaktion an: HTTP-Session und TLSessionContext bleiben erhalten, der Benutzer wird in der Sub-Session des Fensters (mit seiner Sprache) und als ursprünglicher Benutzer der Session gesetzt. Gegen Session-Fixation wechselt die Session-ID (HttpServletRequest.changeSessionId()); die Registrierung im SessionService folgt der neuen ID. Der Anmeldedatensatz des vorherigen Benutzers wird unter der alten ID beendet (LOGGED_OUT), die Anmeldung unter der neuen gemeldet (LOGGED_IN).
- Der SessionEndReloadListener lädt bei einer Abmeldung die Fenster nur noch neu, wenn die Session nicht mehr registriert ist – das Ende eines Anmeldedatensatzes innerhalb einer weiterlaufenden Session ist kein Sitzungsende.
- Die ReactWindowRegistry folgt einem Wechsel der Session-ID (SessionIdChangeListener, ein HttpSessionIdListener im web-fragment.xml von tl-layout-react); ebenso merkt sich der MaintenanceWindowManager die Session statt ihrer ID.
- Passwort-Anmeldung: LoginAction meldet direkt im Anmeldekommando an; die Antwort auf das Kommando bringt die neue Session-ID zum Browser. Der Client verlässt die Seite erst, wenn alle bereits gesendeten Kommandos beantwortet sind (leavePageWhenAnswered), damit der Reload das neue Cookie trägt. PendingSessionAction dient nur noch der Abmeldung.
- Externe Anmeldung (OpenID): Der Anmelde-Button überträgt den Fensternamen als Parameter loginWindow (ExternalAuthenticationServlet.WINDOW_PARAM) und kehrt in dasselbe Fenster zurück (/view/<Fenster>/<Route>). Nennt die Anmeldung ein Fenster der Session, meldet ExternalAuthenticationServlet den Benutzer über loginUserInWindow nur in diesem Fenster an, sonst wie bisher die ganze Session.
- Der Komponentenbaum eines Fensters wird nur für denselben Benutzer wiederverwendet (RenderedView); nach einer Anmeldung wird das Fenster für den neuen Benutzer neu aufgebaut.
Die fehlende Session-ID-Rotation bei einer externen Anmeldung ohne Fenster (klassische Oberfläche) behandelt #30069.
Migration
Eine eigene Implementierung von com.top_logic.base.accesscontrol.loginmethod.LoginMethod muss getInitiationUrl(String returnToUrl) durch getInitiationUrl(String returnToUrl, String windowName) ersetzen und den Fensternamen (falls nicht null) als Parameter ExternalAuthenticationServlet.WINDOW_PARAM in die Initiierungs-URL aufnehmen, siehe Pac4jLoginMethod.