2323
2424import org .springframework .context .ApplicationEvent ;
2525import org .springframework .context .ApplicationEventPublisher ;
26+ import org .springframework .context .event .SimpleApplicationEventMulticaster ;
2627import org .springframework .mock .web .MockHttpServletRequest ;
2728import org .springframework .mock .web .MockHttpServletResponse ;
2829import org .springframework .security .core .Authentication ;
2930import org .springframework .security .core .session .SessionIdChangedEvent ;
31+ import org .springframework .security .core .session .SessionRegistryImpl ;
3032
3133import static org .assertj .core .api .Assertions .assertThat ;
3234import static org .mockito .Mockito .mock ;
@@ -48,7 +50,30 @@ public void applySessionFixation() {
4850 }
4951
5052 @ Test
51- public void onAuthenticationPublishesSessionIdChangedEventWithoutHttpSessionEventPublisher () {
53+ public void onAuthenticationWhenRegistryHasOldSessionThenMigratesWithoutHttpSessionEventPublisher () {
54+ // Reproduces gh-19007: without HttpSessionEventPublisher the old session ID used
55+ // to remain as a ghost entry in the registry after session fixation rotation,
56+ // causing ConcurrentSessionControlAuthenticationStrategy to count an extra session.
57+ SessionRegistryImpl registry = new SessionRegistryImpl ();
58+ SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster ();
59+ multicaster .addApplicationListener (registry );
60+ ChangeSessionIdAuthenticationStrategy strategy = new ChangeSessionIdAuthenticationStrategy ();
61+ strategy .setApplicationEventPublisher ((event ) -> {
62+ if (event instanceof ApplicationEvent applicationEvent ) {
63+ multicaster .multicastEvent (applicationEvent );
64+ }
65+ });
66+ MockHttpServletRequest request = new MockHttpServletRequest ();
67+ Object principal = "testPrincipal" ;
68+ registry .registerNewSession (request .getSession ().getId (), principal );
69+ strategy .onAuthentication (mock (Authentication .class ), request , new MockHttpServletResponse ());
70+ String newSessionId = request .getSession ().getId ();
71+ assertThat (registry .getSessionInformation (newSessionId )).isNotNull ();
72+ assertThat (registry .getAllSessions (principal , false )).hasSize (1 );
73+ }
74+
75+ @ Test
76+ public void onAuthenticationPublishesSessionFixationAndSessionIdChangedEvents () {
5277 ChangeSessionIdAuthenticationStrategy strategy = new ChangeSessionIdAuthenticationStrategy ();
5378 MockHttpServletRequest request = new MockHttpServletRequest ();
5479 String oldSessionId = request .getSession ().getId ();
@@ -58,9 +83,12 @@ public void onAuthenticationPublishesSessionIdChangedEventWithoutHttpSessionEven
5883 ArgumentCaptor <ApplicationEvent > captor = ArgumentCaptor .forClass (ApplicationEvent .class );
5984 verify (eventPublisher , times (2 )).publishEvent (captor .capture ());
6085 List <ApplicationEvent > events = captor .getAllValues ();
61- assertThat (events .get (0 )).isInstanceOf (SessionFixationProtectionEvent .class );
62- assertThat (events .get (1 )).isInstanceOf (SessionIdChangedEvent .class );
63- SessionIdChangedEvent idChangedEvent = (SessionIdChangedEvent ) events .get (1 );
86+ assertThat (events ).hasAtLeastOneElementOfType (SessionFixationProtectionEvent .class );
87+ SessionIdChangedEvent idChangedEvent = events .stream ()
88+ .filter (SessionIdChangedEvent .class ::isInstance )
89+ .map (SessionIdChangedEvent .class ::cast )
90+ .findFirst ()
91+ .orElseThrow ();
6492 assertThat (idChangedEvent .getOldSessionId ()).isEqualTo (oldSessionId );
6593 assertThat (idChangedEvent .getNewSessionId ()).isEqualTo (request .getSession ().getId ());
6694 assertThat (idChangedEvent .getNewSessionId ()).isNotEqualTo (oldSessionId );
0 commit comments