Skip to content

Change of default securityContextRepository in filters causes SessionRegistryImpl to be empty #16878

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
gcorsaro opened this issue Apr 4, 2025 · 0 comments
Labels
status: waiting-for-triage An issue we've not yet triaged type: bug A general bug

Comments

@gcorsaro
Copy link

gcorsaro commented Apr 4, 2025

Introduction
In my project we use websockets to send UI notifications. For this reason we use the SessionRegistry in order to retrieve the logged in users to send notifications to.
Everything was working well with Spring Boot 2.7.x and Spring Security 5.7.x but after the migration to Spring Boot 3 and Spring Security 6.2 I noticed that the SessionRegistry was always empty even though there were logged in users.
After struggling a lot to investigate and hard debugging, I came across the reason why this is happening.
I'm not sure if it's a bug or a wanted behavior, nevertheless I couldn't find any reference in the documentation so I'm raising here the problem.

Situation
Our application is beyond CAS authentication server. So I configured a J2eePreAuthenticatedProcessingFilter in order to catch the logged in user and manage it

	@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		http.authorizeHttpRequests(requests -> requests
				.requestMatchers("/public/**").permitAll()
				.requestMatchers("/secure/**").authenticated())
			.cors(withDefaults())
			.addFilterAfter(j2eePreAuthenticatedProcessingFilter(), J2eePreAuthenticatedProcessingFilter.class)
			.headers(headers -> headers
					.httpStrictTransportSecurity(security -> security.includeSubDomains(true).maxAgeInSeconds(31536000).requestMatcher(AnyRequestMatcher.INSTANCE))
					.frameOptions(options -> options.sameOrigin()))
			.sessionManagement(management -> management
					.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
					.sessionFixation().changeSessionId()
					.maximumSessions(3)
					.sessionRegistry(sessionRegistry()));
		return http.build();
    }

    @Bean
    J2eePreAuthenticatedProcessingFilter j2eePreAuthenticatedProcessingFilter() {
        J2eePreAuthenticatedProcessingFilter filter = new J2eePreAuthenticatedProcessingFilter();
        filter.setAuthenticationManager(authenticationManager());
        filter.setAuthenticationDetailsSource(yestWebAuthenticationDetailsSource());
        filter.setContinueFilterChainOnUnsuccessfulAuthentication(false);
        return filter;
    }

I skipped the other parts of the configuration as not useful for our scenario.

When I logged in the application, the filter chain I could see in the stack was:
....->J2eePreAuthenticatedProcessingFilter.....->SessionManagementFilter...->

In the SessionManagementFilter there's the part which afterwards leads to fill the SessionRegistry by the sessionAuthenticationStrategy

this.sessionAuthenticationStrategy.onAuthentication(authentication, request, response);

as the event is caught by the SessionRegistryImpl instance. However this block of code is executed only if the session wasn't already stored in the securityContextRepository
Here is it the snippet of SessionManagementFilter where this happens (please focus on ----> comments)

		----> if the securityContext is still empty, then it goes inside the if
		if (!this.securityContextRepository.containsContext(request)) {
			Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
			if (this.trustResolver.isAuthenticated(authentication)) {
				// The user has been authenticated during the current request, so call the
				// session strategy
				try {
					----> The event is raised and the SessionRegistry is filled
					this.sessionAuthenticationStrategy.onAuthentication(authentication, request, response);
				}
				catch (SessionAuthenticationException ex) {
					// The session strategy can reject the authentication
					this.logger.debug("SessionAuthenticationStrategy rejected the authentication object", ex);
					this.securityContextHolderStrategy.clearContext();
					this.failureHandler.onAuthenticationFailure(request, response, ex);
					return;
				}
				// Eagerly save the security context to make it available for any possible
				// re-entrant requests which may occur before the current request
				// completes. SEC-1396.
				----> Then the context is saved
				this.securityContextRepository.saveContext(this.securityContextHolderStrategy.getContext(), request,
						response);
			}

In Spring Security 5.7, by default the securityContextRepository for the J2eePreAuthenticatedProcessingFilter is a NullSecurityContextRepository; instead, in the SessionManagementFilter by default it's an instance of HttpSessionSecurityContextRepository.
So, in the block mentioned above, the execution goes inside the if and everything works well.

In Spring Security 6.2 we have the HttpSessionSecurityContextRepository for both the filters and because of the order in the chain, when it comes to the SessionManagementFilter, the context is already present in the securityContextRepository and it never goes inside the if, with the consequence that the event is never raised.

To Fix
In order to make it work I've just added the NullSecurityContextRepository to the J2eePreAuthenticatedProcessingFilter configuration.

	    @Bean
	    J2eePreAuthenticatedProcessingFilter j2eePreAuthenticatedProcessingFilter() {
	        J2eePreAuthenticatedProcessingFilter filter = new J2eePreAuthenticatedProcessingFilter();
	        filter.setAuthenticationManager(authenticationManager());
	        filter.setAuthenticationDetailsSource(yestWebAuthenticationDetailsSource());
	        filter.setContinueFilterChainOnUnsuccessfulAuthentication(false);
	        filter.setSecurityContextRepository(new NullSecurityContextRepository()); ---> THE SOLUTION
	        return filter;
	    }

However I don't know if this is the correct way or just a workaround for a possible bug.
Any suggestion is very welcome

@gcorsaro gcorsaro added status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels Apr 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: waiting-for-triage An issue we've not yet triaged type: bug A general bug
Projects
None yet
Development

No branches or pull requests

1 participant