Skip to content

Commit d3861e5

Browse files
committed
Use SessionAuthenticationStrategy for Remember-Me authentication
Fixes gh-2253
1 parent 562ba01 commit d3861e5

File tree

4 files changed

+98
-0
lines changed

4 files changed

+98
-0
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurer.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
3434
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;
3535
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;
36+
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
3637
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
3738
import org.springframework.security.web.context.SecurityContextRepository;
3839
import org.springframework.util.Assert;
@@ -296,6 +297,12 @@ public void configure(H http) {
296297
rememberMeFilter.setSecurityContextRepository(securityContextRepository);
297298
}
298299
rememberMeFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
300+
301+
SessionAuthenticationStrategy sessionAuthenticationStrategy = http.getSharedObject(SessionAuthenticationStrategy.class);
302+
if (sessionAuthenticationStrategy != null) {
303+
rememberMeFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
304+
}
305+
299306
rememberMeFilter = postProcess(rememberMeFilter);
300307
http.addFilter(rememberMeFilter);
301308
}

config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060

6161
import static org.assertj.core.api.Assertions.assertThat;
6262
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
63+
import static org.hamcrest.Matchers.startsWith;
6364
import static org.mockito.ArgumentMatchers.any;
6465
import static org.mockito.ArgumentMatchers.anyString;
6566
import static org.mockito.BDDMockito.given;
@@ -76,6 +77,7 @@
7677
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
7778
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie;
7879
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
80+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
7981

8082
/**
8183
* Tests for {@link RememberMeConfigurer}
@@ -334,6 +336,30 @@ public void getWhenCustomSecurityContextRepositoryThenUses() throws Exception {
334336
verify(repository).saveContext(any(), any(), any());
335337
}
336338

339+
@Test
340+
public void rememberMeExpiresSessionWhenSessionManagementMaximumSessionsExceeds() throws Exception {
341+
this.spring.register(RememberMeMaximumSessionsConfig.class).autowire();
342+
343+
MockHttpServletRequestBuilder loginRequest = post("/login")
344+
.with(csrf())
345+
.param("username", "user")
346+
.param("password", "password")
347+
.param("remember-me", "true");
348+
MvcResult mvcResult = this.mvc.perform(loginRequest).andReturn();
349+
Cookie rememberMeCookie = mvcResult.getResponse().getCookie("remember-me");
350+
HttpSession session = mvcResult.getRequest().getSession();
351+
352+
MockHttpServletRequestBuilder exceedsMaximumSessionsRequest = get("/abc")
353+
.cookie(rememberMeCookie);
354+
this.mvc.perform(exceedsMaximumSessionsRequest);
355+
356+
MockHttpServletRequestBuilder sessionExpiredRequest = get("/abc")
357+
.cookie(rememberMeCookie)
358+
.session((MockHttpSession) session);
359+
this.mvc.perform(sessionExpiredRequest)
360+
.andExpect(content().string(startsWith("This session has been expired")));
361+
}
362+
337363
@Configuration
338364
@EnableWebSecurity
339365
static class NullUserDetailsConfig {
@@ -617,6 +643,35 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
617643

618644
}
619645

646+
@Configuration
647+
@EnableWebSecurity
648+
static class RememberMeMaximumSessionsConfig {
649+
650+
@Bean
651+
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
652+
// @formatter:off
653+
http
654+
.authorizeRequests((authorizeRequests) ->
655+
authorizeRequests
656+
.anyRequest().hasRole("USER")
657+
)
658+
.sessionManagement((sessionManagement) ->
659+
sessionManagement
660+
.maximumSessions(1)
661+
)
662+
.formLogin(withDefaults())
663+
.rememberMe(withDefaults());
664+
return http.build();
665+
// @formatter:on
666+
}
667+
668+
@Bean
669+
UserDetailsService userDetailsService() {
670+
return new InMemoryUserDetailsManager(PasswordEncodedUser.user());
671+
}
672+
673+
}
674+
620675
@Configuration
621676
@EnableWebSecurity
622677
static class SecurityContextRepositoryConfig {

web/src/main/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilter.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
import org.springframework.security.core.context.SecurityContextHolderStrategy;
3838
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
3939
import org.springframework.security.web.authentication.RememberMeServices;
40+
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
41+
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
4042
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
4143
import org.springframework.security.web.context.SecurityContextRepository;
4244
import org.springframework.util.Assert;
@@ -81,6 +83,8 @@ public class RememberMeAuthenticationFilter extends GenericFilterBean implements
8183

8284
private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
8385

86+
private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();
87+
8488
public RememberMeAuthenticationFilter(AuthenticationManager authenticationManager,
8589
RememberMeServices rememberMeServices) {
8690
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
@@ -115,6 +119,7 @@ private void doFilter(HttpServletRequest request, HttpServletResponse response,
115119
// Attempt authentication via AuthenticationManager
116120
try {
117121
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
122+
this.sessionStrategy.onAuthentication(rememberMeAuth, request, response);
118123
// Store to SecurityContextHolder
119124
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
120125
context.setAuthentication(rememberMeAuth);
@@ -211,4 +216,17 @@ public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy secur
211216
this.securityContextHolderStrategy = securityContextHolderStrategy;
212217
}
213218

219+
/**
220+
* The session handling strategy which will be invoked immediately after an
221+
* authentication request is successfully processed by the
222+
* <tt>AuthenticationManager</tt>. Used, for example, to handle changing of the
223+
* session identifier to prevent session fixation attacks.
224+
* @param sessionStrategy the implementation to use. If not set a null implementation
225+
* is used.
226+
*/
227+
public void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionStrategy) {
228+
Assert.notNull(sessionStrategy, "sessionStrategy cannot be null");
229+
this.sessionStrategy = sessionStrategy;
230+
}
231+
214232
}

web/src/test/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilterTests.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.springframework.security.web.authentication.NullRememberMeServices;
3636
import org.springframework.security.web.authentication.RememberMeServices;
3737
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
38+
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
3839
import org.springframework.security.web.context.SecurityContextRepository;
3940

4041
import static org.assertj.core.api.Assertions.assertThat;
@@ -170,6 +171,23 @@ public void securityContextRepositoryInvokedIfSet() throws Exception {
170171
verify(securityContextRepository).saveContext(any(), eq(request), eq(response));
171172
}
172173

174+
@Test
175+
public void sessionAuthenticationStrategyInvokedIfSet() throws Exception {
176+
SessionAuthenticationStrategy sessionAuthenticationStrategy = mock(SessionAuthenticationStrategy.class);
177+
AuthenticationManager am = mock(AuthenticationManager.class);
178+
given(am.authenticate(this.remembered)).willReturn(this.remembered);
179+
RememberMeAuthenticationFilter filter = new RememberMeAuthenticationFilter(am,
180+
new MockRememberMeServices(this.remembered));
181+
filter.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler("/target"));
182+
filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
183+
MockHttpServletRequest request = new MockHttpServletRequest();
184+
MockHttpServletResponse response = new MockHttpServletResponse();
185+
FilterChain fc = mock(FilterChain.class);
186+
request.setRequestURI("x");
187+
filter.doFilter(request, response, fc);
188+
verify(sessionAuthenticationStrategy).onAuthentication(any(), eq(request), eq(response));
189+
}
190+
173191
private class MockRememberMeServices implements RememberMeServices {
174192

175193
private Authentication authToReturn;

0 commit comments

Comments
 (0)