Authentication Principal is empty while using Spring Session Redis

Tadeusz Kleszcz picture Tadeusz Kleszcz · Apr 8, 2016 · Viewed 7.9k times · Source

I am building rest API using Spring Boot v1.3.3. API is secured by Spring Security. I have implemented custom user details service to have custom principal in authentication context.

I needed to share sessions of API with other Spring app so I choosen to implement Spring Session with Redis server in my app using this tutorial docs.spring.io/spring-session/docs/current/reference/html5/guides/security.html. Unfortunetly it caused Authentication Principal to stop working. When I am trying to get current Principal either by annotation @AuthenticationPrincipal CustomUserDetails user or by SecurityContextHolder.getContext().getAuthentication().getPrincipal() it returns my custom user details but with Id = 0 and all fields set to null (screen from debugging). I can't even get username from SecurityContextHolder.getContext().getAuthentication().getName().

After I commented Redis code and maven dependency it works (see debug screen). How to make it working with Spring Session and Redis server?

Here is some code from the app:

Some example method to check Principal

@RequestMapping(value = "/status", method = RequestMethod.GET)
public StatusData status(@AuthenticationPrincipal CustomUserDetails user) {
    User user2 = (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    if (user != null) {
        String name = user.getUsername();
        return new StatusData(name);
    } else return new StatusData(null);
}

Application and Redis config:

@Configuration
@EnableRedisHttpSession
public class AppConfig {

    @Bean
    public JedisConnectionFactory connectionFactory() {
        return new JedisConnectionFactory();
    }

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("JSESSIONID");
        serializer.setCookiePath("/");
        serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
        return serializer;
    }

    @Bean
    public ShaPasswordEncoder shaEncoder() {
        return new ShaPasswordEncoder(256);
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean(name = "messageSource")
    public ResourceBundleMessageSource messageSource() {
        ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource();
        resourceBundleMessageSource.setBasename("messages/messages");
        return resourceBundleMessageSource;
    }

    @Bean
    public Validator basicValidator() {
        LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
        validator.setValidationMessageSource(messageSource());
        return validator;
    }

    public AppConfig() {
        DateTimeZone.setDefault(DateTimeZone.UTC);
    }
}

Initializer (used for Redis Session)

public class Initializer extends AbstractHttpSessionApplicationInitializer {

}

SecurityInitializer (used for Redis session)

public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {

    public SecurityInitializer() {
        super(WebSecurityConfig.class, AppConfig.class);
    }
}

WebSecurityConfig (Spring Security config)

@Configuration
@EnableWebSecurity
//@EnableWebMvcSecurity
@ComponentScan(basePackageClasses = {UserRepository.class, CustomUserDetailsService.class})
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private UserDetailsService customUserDetailsService;

    @Autowired
    private HttpAuthenticationEntryPoint httpAuthenticationEntryPoint;

    @Autowired
    private AuthSuccessHandler authSuccessHandler;

    @Autowired
    private AuthFailureHandler authFailureHandler;

    @Autowired
    private HttpLogoutSuccessHandler logoutSuccessHandler;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    /**
     * Persistent token repository stored in database. Used for remember me feature.
     */
    @Bean
    public PersistentTokenRepository tokenRepository() {
        JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
        db.setDataSource(dataSource);
        return db;
    }

    /**
     * Enable always remember feature.
     */
    @Bean
    public AbstractRememberMeServices rememberMeServices() {
        CustomTokenPersistentRememberMeServices rememberMeServices = new CustomTokenPersistentRememberMeServices("xxx", customUserDetailsService, tokenRepository());
        rememberMeServices.setAlwaysRemember(true);
        rememberMeServices.setTokenValiditySeconds(1209600);
        return rememberMeServices;
    }

    /**
     * Configure spring security to use in REST API.
     * Set handlers to immediately return HTTP status codes.
     * Enable remember me tokens.
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .exceptionHandling()
                .authenticationEntryPoint(httpAuthenticationEntryPoint)
                .and()
                .authorizeRequests()
                .antMatchers("/cookie", "/register", "/redirect/**", "/track/**")
                .permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .permitAll()
                .successHandler(authSuccessHandler)
                .failureHandler(authFailureHandler)
                .and()
                .logout()
                .permitAll().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler)
                .and()
                .rememberMe().rememberMeServices(rememberMeServices())
                .and()
                .headers()
                .addHeaderWriter(new HeaderWriter() {
                    /**
                     * Header to allow access from javascript AJAX in chrome extension.
                     */
                    @Override
                    public void writeHeaders(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
                        String corsUrl = "https://mail.google.com";
                        if (httpServletRequest.getHeader("Origin") != null && httpServletRequest.getHeader("Origin").equals(corsUrl)) {
                            httpServletResponse.setHeader("Access-Control-Allow-Origin", "https://mail.google.com");
                            httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
                            httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
                            httpServletResponse.setHeader("Access-Control-Expose-Headers", "Location");
                        }
                    }
                });
    }

    /**
     * Set custom user details service to allow for store custom user details and set password encoder to BCrypt.
     */
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(customUserDetailsService).passwordEncoder(bCryptPasswordEncoder);
    }
}

Maven dependencies

<dependencies>
    <dependency>
        <groupId>${project.groupId}</groupId>
        <artifactId>models</artifactId>
        <version>${project.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>5.2.3.Final</version>
    </dependency>
    <dependency>
        <groupId>joda-time</groupId>
        <artifactId>joda-time</artifactId>
    </dependency>
    <dependency>
        <groupId>org.jadira.usertype</groupId>
        <artifactId>usertype.core</artifactId>
        <version>3.1.0.CR1</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.jaxrs</groupId>
        <artifactId>jackson-jaxrs-json-provider</artifactId>
        <version>2.2.1</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.datatype</groupId>
        <artifactId>jackson-datatype-joda</artifactId>
    </dependency>
    <dependency>
        <groupId>com.maxmind.geoip2</groupId>
        <artifactId>geoip2</artifactId>
        <version>2.6.0</version>
    </dependency>
    <dependency>
        <groupId>com.ganyo</groupId>
        <artifactId>gcm-server</artifactId>
        <version>1.0.2</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session</artifactId>
        <version>1.1.1.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <version>4.0.4.RELEASE</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.jayway.jsonpath</groupId>
        <artifactId>json-path</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Answer

Tadeusz Kleszcz picture Tadeusz Kleszcz · Apr 10, 2016

I solved this problem. It turned out that Spring-Session serializes the Principal object. My custom implementation of UserDetails was subclass of Hibernate Model User class. I solved it by implementing Serializable interface in my custom UserDetails, User model and all classes used in this model.