1

I'm using Spring security with JSESSION cookie so every user gets that cookie after its login with credentials:

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeRequests().anyRequest().authenticated().and().httpBasic()
    return http.build();
  }

I created a UserService to create, retrieve, update and delete users and UserDetailsServiceImpl (which uses UserService) to implement UserDetailsService:

  @Service
  public class UserDetailsServiceImpl implements UserDetailsService {

  @Autowired
  private UserService userService;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    return userService.findByUsername(username);
  }
}

So users can be deleted. Maybe the delete command is executed by other user rather than him so I don't have the session context.

I want to invalidate all cookies related to a user when I delete him so he cannot enter the app again using his old cookies.

Is there any way?

Thanks in advance.

italktothewind
  • 1,950
  • 2
  • 28
  • 55
  • No, luckily as that would be quite a security risk. If you are using Spring Session and session tracking in Spring Security you could invalidate that user bound session. – M. Deinum Jun 06 '23 at 09:16
  • 1
    Hi, guys! But the requirement seems legit, @M.Deinum (an admin user wants to *immediately*/asap delete users & invalidate their sessions), and a (single server) solution is shown [here](https://stackoverflow.com/a/35276555/592355) ...for "distributed/lb" you must leverage this somewhat – xerx593 Jun 06 '23 at 09:21
  • It doesn't matter if you are using a single server or not. Fact remains is that you cannot directly access the user session, there has to be something intermediate which is controlled by Spring Security. Which is what the `SessionRegistry` is (which could be an in-memory one, a distributed one etc.). – M. Deinum Jun 06 '23 at 09:26

2 Answers2

2

You can implement org.springframework.security.core.session.SessionRegistry or extend the default implementation org.springframework.security.core.session.SessionRegistryImpl to use getAllSessions for the associated user principal and invalidate the appropriate ones as per your requirement.

For your session registry to be notified of the session life-cycle events, you need to register org.springframework.security.web.session.HttpSessionEventPublisher in Servlet context as below sample:

@Bean
public ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
ServletListenerRegistrationBean<HttpSessionEventPublisher> listenerRegBean =  new ServletListenerRegistrationBean<>();
           
listenerRegBean.setListener(new HttpSessionEventPublisher());
           return listenerRegBean;
}
    
    @Bean
    public SessionRegistry sessionRegistry(){
     return new SessionRegistryImpl();
    }
1

I got it working like so

  1. Basing on gs-securing-web
  2. Get us some test users/modifying:
     @Bean
     InMemoryUserDetailsManager userDetailsService() {
         UserDetails alice = User.withUsername("alice")
                 .password("{noop}alice")
                 .roles("ADMIN")
                 .build();
         UserDetails bob = User.withUsername("bob")
                 .password("{noop}bob")
                 .roles("USER")
                 .build();
         return new InMemoryUserDetailsManager(alice, bob);
     }
    
  3. Adding a (rudimentary admin) control to hello.html:
    <form sec:authorize="hasRole('ROLE_ADMIN')" th:action="@{/invalidate}" method="post">
        <input type="text" name="user" />
        <input type="submit" value="Delete User" />
    </form>
    
  4. The (rudimentary) "invalidate/delete user" controller:
    @Controller
    public class AdminController {
       @Autowired
       InMemoryUserDetailsManager userService;
       @Autowired
       SessionRegistry sessionRegistry; // !
    
       @PostMapping("/invalidate")
       public String invalidate(@RequestParam String user) {
          // omitted (any) validation for brevity...
          // (optional) load user to invalidate:
          User bob = (User) userService.loadUserByUsername(user);
    
          // all "principals":
          sessionRegistry.getAllPrincipals()
                 .stream()
                 // filter correct user name:
                 .filter(p -> p instanceof UserDetails prncp && prncp.getUsername().equals(bob.getUsername()))
                 // "expire" all sessions for principal:
                 .forEach(p -> sessionRegistry.getAllSessions(p, false)
                    .forEach(SessionInformation::expireNow) // !
                 );
    
          // (optional) custom user deletion/modification..
          // userService.updateUser(...);
    
          // (sample) custom response
          return "redirect:/hello";
       }
    }
    
  5. To wire it all up, we (only) need:
     @Bean
     SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
         return http
                 .authorizeHttpRequests(...)
                 ...
                 .sessionManagement(sm -> sm // 1.
                         .maximumSessions(2) // <- example
                         .sessionRegistry(sessionRegistry()) // 2.
                         .expiredSessionStrategy(expireStrategy())) // 3.
                 .build();
     }
    
     @Bean // org.springframework.security.core.session.*:
     SessionRegistry sessionRegistry() { // 2a.
         return new SessionRegistryImpl();
     }
    
     @Bean //org.springframework.security.web.session.*:
     SessionInformationExpiredStrategy expireStrategy() { // 3a.
         // "/" refers to (custom, session invalidation) redirect url
         return new SimpleRedirectSessionInformationExpiredStrategy("/");
     }
    

Testing Time

  1. Bob logs in with:

    • Chrome (anonymous)
    • and Opera browser

    .. to display /hello

  2. Alice logs in (with chrome), and submits bob with "invalidate form"

  3. Next time, when bob tries to request any (un-/secured) resource (from any browser), (spring security default chain) ConcurrentSessionFilter jumps in:

    • detects session expiry (set by us)
    • performs logout (handler)
    • and "fires" a SessionInformationExpiredEvent
  4. (..until server restarts/user service is reset)

For a load balanced/distributed (server) environment

We can ensure "sticky sessions" or ..as for a more resilient solution, implement custom SessionRegistry.

  • For distribution/load balancing the (sessions must be sticky or) SessionRegistry must be aware of all nodes/instances/principals/sessions
  • for (more) resilience, we need to pre-populate our SessionRegistry from valid, active/persistent Sessions. (I experienced them empty after a "devtools-reload", while the underlying (container) sessions still persisted.)
xerx593
  • 12,237
  • 5
  • 33
  • 64