Search code examples
spring-bootspring-securitysession-cookiesspring-session

Spring-boot 2.0 session issue when request is coming from different host


So I have updated my application from spring-boot 1.5 to spring-boot 2.0. I was able to login properly with spring-boot 1.5 version when the request was coming from any host, but now the problem with Spring boot 2.0 is only requests from same host is working, but request coming from different host their session is getting changed. Below is the spring security config I have in spring-boot 2.0 whic is causing the issue.

 package com.s4m.digiid.config;

    import java.util.Arrays;
    import java.util.List;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.env.Environment;
    import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy;
    import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
    import org.springframework.session.FindByIndexNameSessionRepository;
    import org.springframework.session.Session;
    import org.springframework.session.security.SpringSessionBackedSessionRegistry;
    import org.springframework.web.cors.CorsConfiguration;
    import org.springframework.web.cors.CorsConfigurationSource;
    import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

    import com.s4m.digiid.handler.CustomAccessDeniedHandler;
    import com.s4m.digiid.handler.CustomAuthFailureHandler;
    import com.s4m.digiid.handler.LoginHandler;
    import com.s4m.digiid.handler.LogoutHandler;
    import com.s4m.digiid.service.impl.AuthenticationService;
    import com.s4m.digiid.util.ApiConstants;

    @EnableGlobalMethodSecurity(prePostEnabled = true)
    @EnableWebSecurity
    @EnableJpaRepositories(basePackages = "com.s4m.digiid.repository")
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {

        @Autowired
        private Environment env;        

        @Autowired
        private LogoutHandler logoutHandler;

        @Autowired
        private LoginHandler loginHandler;

        @Autowired
        private CustomAuthFailureHandler customAuthFailure;

        @Autowired
        private CustomAccessDeniedHandler accessDeniedHandler;

        @Autowired
        private AuthenticationService authService;


        @Value("${maximum.sessions}")
        private Integer maxSessions;

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(authService)
            .passwordEncoder(passwordEncoder());
        }


        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }

        @Autowired
        private FindByIndexNameSessionRepository<? extends Session> sessionRepository;


        @Bean
        public SpringSessionBackedSessionRegistry sessionRegistry() {
            return new SpringSessionBackedSessionRegistry<>(this.sessionRepository);
        }

        @Bean
        public SessionAuthenticationStrategy sessionAuthenticationStrategy() {
            return new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                .antMatchers(ApiConstants.SWAGGER_DOC_API, ApiConstants.SWAGGER_CONFIG_UI, ApiConstants.SWAGGER_RESOURCES,
                    ApiConstants.SWAGGER_CONFIG, ApiConstants.SWAGGER_HTML_UI, ApiConstants.WEB_JARS, ApiConstants.FAVICON).permitAll()
                .and().formLogin().successHandler(loginHandler).failureHandler(customAuthFailure)
                .and().logout()    
                .logoutUrl("/appLogout") 
                .logoutSuccessHandler(logoutHandler)
                .and().exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler)
                .and().csrf().disable()
                .cors().and()
                .addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .sessionManagement().maximumSessions(maxSessions).sessionRegistry(sessionRegistry()).maxSessionsPreventsLogin(false);
        }


        @Bean
        public AuthenticationFilter authenticationFilter() throws Exception {
            AuthenticationFilter filter = new AuthenticationFilter();
            filter.setAuthenticationManager(authenticationManagerBean());
            filter.setAuthenticationFailureHandler(customAuthFailure);
            filter.setAuthenticationSuccessHandler(loginHandler);
            return filter;
        }


        @Bean
        public CorsConfigurationSource corsConfigurationSource()
        {
            List<String> allowedClients = Arrays.asList(env.getProperty("digiid.allowed.clients").split(","));
             CorsConfiguration configuration = new CorsConfiguration();
             configuration.setAllowedOrigins(allowedClients);
             configuration.setAllowedMethods(Arrays.asList("GET", "POST", "OPTIONS"));
             configuration.setMaxAge(Long.parseLong("3600"));
             configuration.setAllowedHeaders(Arrays.asList("X-Requested-With", "Origin", "Content-Type", "Accept", "Authorization,X-Auth-Token"));
             configuration.setAllowCredentials(true);
             UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
             source.registerCorsConfiguration("/**", configuration);
             return source;
        }

    }

Below is the code in spring-boot 1.5 which is working fine.

package com.s4m.digiid.config;

import java.util.Arrays;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import com.s4m.digiid.handler.CustomAccessDeniedHandler;
import com.s4m.digiid.handler.CustomAuthFailureHandler;
import com.s4m.digiid.handler.LoginHandler;
import com.s4m.digiid.handler.LogoutHandler;
import com.s4m.digiid.service.impl.AuthenticationService;
import com.s4m.digiid.util.ApiConstants;

@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
@EnableRedisHttpSession
@EnableJpaRepositories(basePackages = "com.s4m.digiid.repository")
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private Environment env;        

    @Autowired
    private LogoutHandler logoutHandler;

    @Autowired
    private LoginHandler loginHandler;

    @Autowired
    private CustomAuthFailureHandler customAuthFailure;

    @Autowired
    private CustomAccessDeniedHandler accessDeniedHandler;

    @Autowired
    private AuthenticationService authService;

    @Autowired 
    private SpringSessionBackedSessionRegistry sessionRegistry;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(authService)
        .passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers(ApiConstants.SWAGGER_DOC_API, ApiConstants.SWAGGER_CONFIG_UI, ApiConstants.SWAGGER_RESOURCES,
                ApiConstants.SWAGGER_CONFIG, ApiConstants.SWAGGER_HTML_UI, ApiConstants.WEB_JARS, ApiConstants.FAVICON).permitAll()
            .and().formLogin().successHandler(loginHandler).failureHandler(customAuthFailure)
            .and().logout()    
            .logoutUrl("/appLogout") 
            .logoutSuccessHandler(logoutHandler)
            .and().exceptionHandling()
            .accessDeniedHandler(accessDeniedHandler)
            .and().csrf().disable()
            .cors().and()
            .addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            .sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry).maxSessionsPreventsLogin(false);        
    }

    @Bean
    public AuthenticationFilter authenticationFilter() throws Exception {
        AuthenticationFilter filter = new AuthenticationFilter();
        filter.setAuthenticationManager(authenticationManagerBean());
        filter.setAuthenticationFailureHandler(customAuthFailure);
        filter.setAuthenticationSuccessHandler(loginHandler);
        return filter;
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource()
    {
        List<String> allowedClients = Arrays.asList(env.getProperty("digiid.allowed.clients").split(","));
         CorsConfiguration configuration = new CorsConfiguration();
         configuration.setAllowedOrigins(allowedClients);
         configuration.setAllowedMethods(Arrays.asList("GET", "POST", "OPTIONS"));
         configuration.setMaxAge(Long.parseLong("3600"));
         configuration.setAllowedHeaders(Arrays.asList("X-Requested-With", "Origin", "Content-Type", "Accept", "Authorization,X-Auth-Token"));
         configuration.setAllowCredentials(true);
         UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
         source.registerCorsConfiguration("/**", configuration);
         return source;
    }

}

Solution

  • With the spring-boot upgrade to 2.x, "SameSite" attribute of Cookie handled in DefaultCookieSerializer.java class is set to "Lax" by default.      

    Try setting  "SameSite" attribute's value to "None".
    

    For more details, https://datatracker.ietf.org/doc/html/draft-west-first-party-cookies-07