Concurrent Sessions Control

Similar to Servlet’s Concurrent Sessions Control, Spring Security also provides support to limit the number of concurrent sessions a user can have in a Reactive application.

When you set up Concurrent Sessions Control in Spring Security, it monitors authentications carried out through Form Login, OAuth 2.0 Login, and HTTP Basic authentication by hooking into the way those authentication mechanisms handle authentication success. More specifically, the session management DSL will add the ConcurrentSessionControlServerAuthenticationSuccessHandler and the RegisterSessionServerAuthenticationSuccessHandler to the list of ServerAuthenticationSuccessHandler used by the authentication filter.

The following sections contains examples of how to configure Concurrent Sessions Control.

Limiting Concurrent Sessions

By default, Spring Security will allow any number of concurrent sessions for a user. To limit the number of concurrent sessions, you can use the maximumSessions DSL method:

Configuring one session for any user
  • Java

  • Kotlin

@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
    http
        // ...
        .sessionManagement((sessions) -> sessions
            .concurrentSessions((concurrency) -> concurrency
                .maximumSessions(SessionLimit.of(1))
            )
        );
    return http.build();
}

@Bean
ReactiveSessionRegistry reactiveSessionRegistry() {
    return new InMemoryReactiveSessionRegistry();
}
@Bean
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        // ...
        sessionManagement {
            sessionConcurrency {
                maximumSessions = SessionLimit.of(1)
            }
        }
    }
}
@Bean
open fun reactiveSessionRegistry(): ReactiveSessionRegistry {
    return InMemoryReactiveSessionRegistry()
}

The above configuration allows one session for any user. Similarly, you can also allow unlimited sessions by using the SessionLimit#UNLIMITED constant:

Configuring unlimited sessions
  • Java

  • Kotlin

@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
    http
        // ...
        .sessionManagement((sessions) -> sessions
            .concurrentSessions((concurrency) -> concurrency
                .maximumSessions(SessionLimit.UNLIMITED))
        );
    return http.build();
}

@Bean
ReactiveSessionRegistry reactiveSessionRegistry() {
    return new InMemoryReactiveSessionRegistry();
}
@Bean
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        // ...
        sessionManagement {
            sessionConcurrency {
                maximumSessions = SessionLimit.UNLIMITED
            }
        }
    }
}
@Bean
open fun reactiveSessionRegistry(webSessionManager: WebSessionManager): ReactiveSessionRegistry {
    return InMemoryReactiveSessionRegistry()
}

Since the maximumSessions method accepts a SessionLimit interface, which in turn extends Function<Authentication, Mono<Integer>>, you can have a more complex logic to determine the maximum number of sessions based on the user’s authentication:

Configuring maximumSessions based on Authentication
  • Java

  • Kotlin

@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
    http
        // ...
        .sessionManagement((sessions) -> sessions
            .concurrentSessions((concurrency) -> concurrency
                .maximumSessions(maxSessions()))
        );
    return http.build();
}

private SessionLimit maxSessions() {
    return (authentication) -> {
        if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_UNLIMITED_SESSIONS"))) {
            return Mono.empty(); // allow unlimited sessions for users with ROLE_UNLIMITED_SESSIONS
        }
        if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMIN"))) {
            return Mono.just(2); // allow two sessions for admins
        }
        return Mono.just(1); // allow one session for every other user
    };
}

@Bean
ReactiveSessionRegistry reactiveSessionRegistry() {
    return new InMemoryReactiveSessionRegistry();
}
@Bean
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        // ...
        sessionManagement {
            sessionConcurrency {
                maximumSessions = maxSessions()
            }
        }
    }
}

fun maxSessions(): SessionLimit {
    return { authentication ->
        if (authentication.authorities.contains(SimpleGrantedAuthority("ROLE_UNLIMITED_SESSIONS"))) Mono.empty
        if (authentication.authorities.contains(SimpleGrantedAuthority("ROLE_ADMIN"))) Mono.just(2)
        Mono.just(1)
    }
}

@Bean
open fun reactiveSessionRegistry(): ReactiveSessionRegistry {
    return InMemoryReactiveSessionRegistry()
}

When the maximum number of sessions is exceeded, by default, the least recently used session(s) will be expired. If you want to change that behavior, you can customize the strategy used when the maximum number of sessions is exceeded.

The Concurrent Session Management is not aware if there is another session in some Identity Provider that you might use via OAuth 2 Login for example. If you also need to invalidate the session against the Identity Provider you must include your own implementation of ServerMaximumSessionsExceededHandler.

Handling Maximum Number of Sessions Exceeded

By default, when the maximum number of sessions is exceeded, the least recently used session(s) will be expired by using the InvalidateLeastUsedServerMaximumSessionsExceededHandler. Spring Security also provides another implementation that prevents the user from creating new sessions by using the PreventLoginServerMaximumSessionsExceededHandler. If you want to use your own strategy, you can provide a different implementation of ServerMaximumSessionsExceededHandler.

Configuring maximumSessionsExceededHandler
  • Java

  • Kotlin

@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
    http
        // ...
        .sessionManagement((sessions) -> sessions
            .concurrentSessions((concurrency) -> concurrency
                .maximumSessions(SessionLimit.of(1))
                .maximumSessionsExceededHandler(new PreventLoginMaximumSessionsExceededHandler())
            )
        );
    return http.build();
}

@Bean
ReactiveSessionRegistry reactiveSessionRegistry() {
    return new InMemoryReactiveSessionRegistry();
}
@Bean
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        // ...
        sessionManagement {
            sessionConcurrency {
                maximumSessions = SessionLimit.of(1)
                maximumSessionsExceededHandler = PreventLoginMaximumSessionsExceededHandler()
            }
        }
    }
}

@Bean
open fun reactiveSessionRegistry(): ReactiveSessionRegistry {
    return InMemoryReactiveSessionRegistry()
}

Specifying a ReactiveSessionRegistry

In order to keep track of the user’s sessions, Spring Security uses a ReactiveSessionRegistry, and, every time a user logs in, their session information is saved.

Spring Security ships with InMemoryReactiveSessionRegistry implementation of ReactiveSessionRegistry.

To specify a ReactiveSessionRegistry implementation you can either declare it as a bean:

ReactiveSessionRegistry as a Bean
  • Java

  • Kotlin

@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
    http
        // ...
        .sessionManagement((sessions) -> sessions
            .concurrentSessions((concurrency) -> concurrency
                .maximumSessions(SessionLimit.of(1))
            )
        );
    return http.build();
}

@Bean
ReactiveSessionRegistry reactiveSessionRegistry() {
    return new MyReactiveSessionRegistry();
}
@Bean
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        // ...
        sessionManagement {
            sessionConcurrency {
                maximumSessions = SessionLimit.of(1)
            }
        }
    }
}

@Bean
open fun reactiveSessionRegistry(): ReactiveSessionRegistry {
    return MyReactiveSessionRegistry()
}

or you can use the sessionRegistry DSL method:

ReactiveSessionRegistry using sessionRegistry DSL method
  • Java

  • Kotlin

@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
    http
        // ...
        .sessionManagement((sessions) -> sessions
            .concurrentSessions((concurrency) -> concurrency
                .maximumSessions(SessionLimit.of(1))
                .sessionRegistry(new MyReactiveSessionRegistry())
            )
        );
    return http.build();
}
@Bean
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        // ...
        sessionManagement {
            sessionConcurrency {
                maximumSessions = SessionLimit.of(1)
                sessionRegistry = MyReactiveSessionRegistry()
            }
        }
    }
}

Invalidating Registered User’s Sessions

At times, it is handy to be able to invalidate all or some of a user’s sessions. For example, when a user changes their password, you may want to invalidate all of their sessions so that they are forced to log in again. To do that, you can use the ReactiveSessionRegistry bean to retrieve all the user’s sessions, invalidate them, and them remove them from the WebSessionStore:

Using ReactiveSessionRegistry to invalidate sessions manually
  • Java

public class SessionControl {
    private final ReactiveSessionRegistry reactiveSessionRegistry;

    private final WebSessionStore webSessionStore;

    public Mono<Void> invalidateSessions(String username) {
        return this.reactiveSessionRegistry.getAllSessions(username)
            .flatMap((session) -> session.invalidate().thenReturn(session))
            .flatMap((session) -> this.webSessionStore.removeSession(session.getSessionId()))
            .then();
    }
}

Disabling It for Some Authentication Filters

By default, Concurrent Sessions Control will be configured automatically for Form Login, OAuth 2.0 Login, and HTTP Basic authentication as long as they do not specify an ServerAuthenticationSuccessHandler themselves. For example, the following configuration will disable Concurrent Sessions Control for Form Login:

Disabling Concurrent Sessions Control for Form Login
  • Java

  • Kotlin

@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
    http
        // ...
        .formLogin((login) -> login
            .authenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/"))
        )
        .sessionManagement((sessions) -> sessions
            .concurrentSessions((concurrency) -> concurrency
                .maximumSessions(SessionLimit.of(1))
            )
        );
    return http.build();
}
@Bean
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        // ...
        formLogin {
            authenticationSuccessHandler = RedirectServerAuthenticationSuccessHandler("/")
        }
        sessionManagement {
            sessionConcurrency {
                maximumSessions = SessionLimit.of(1)
            }
        }
    }
}

Adding Additional Success Handlers Without Disabling Concurrent Sessions Control

You can also include additional ServerAuthenticationSuccessHandler instances to the list of handlers used by the authentication filter without disabling Concurrent Sessions Control. To do that you can use the authenticationSuccessHandler(Consumer<List<ServerAuthenticationSuccessHandler>>) method:

Adding additional handlers
  • Java

@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
    http
        // ...
        .formLogin((login) -> login
            .authenticationSuccessHandler((handlers) -> handlers.add(new MyAuthenticationSuccessHandler()))
        )
        .sessionManagement((sessions) -> sessions
            .concurrentSessions((concurrency) -> concurrency
                .maximumSessions(SessionLimit.of(1))
            )
        );
    return http.build();
}

Checking a Sample Application

You can check the sample application here.