Issue with Spring Security remember me token not being set on SecurityContextHolder
Asked Answered
C

1

7

I am encountering an issue with my remember me configuration:

[nio-8080-exec-8] s.s.w.a.r.RememberMeAuthenticationFilter : SecurityContextHolder not populated with remember-me token, as it already contained: 'org.springframework.security.authentication.UsernamePasswordAuthenticationToken@73939efa: Principal: Member ...

Here is my Spring security configuration:

@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private MemberUserDetailsService memberUserDetailsService;

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Autowired
    private AccessDecisionManager accessDecisionManager;

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //@formatter:off
        http
        .headers()
        .cacheControl()
          .and()
        .and()
         .csrf()
         .csrfTokenRepository(csrfTokenRepository())
        .and()
         .rememberMe()
         .tokenValiditySeconds(60*60*24*7)
        .and()
            .exceptionHandling()
            .accessDeniedHandler(accessDeniedHandler())
        .and()
            .formLogin()
            .loginProcessingUrl("/api/signin")
            .failureHandler(authenticationFailureHandler())
            .successHandler(authenticationSuccessHandler())
        .and()
            .logout()
            .logoutRequestMatcher(new AntPathRequestMatcher("/api/signout"))
            .logoutSuccessHandler(logoutSuccessHandler())
        .and()
            .addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class)
            .authorizeRequests()
                .accessDecisionManager(accessDecisionManager)
                .antMatchers("/resources/**", "/**").permitAll()
                .anyRequest().authenticated();
        //@formatter:on
    }

    private LogoutSuccessHandler logoutSuccessHandler() {
        return new LogoutSuccessHandler() {
            @Override
            public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                response.setStatus(HttpStatus.OK.value());
            }
        };
    }

    private AccessDeniedHandler accessDeniedHandler() {
        return new AccessDeniedHandler() {
            @Override
            public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
                // TODO: deal with InvalidCsrfTokenException
                response.setStatus(HttpStatus.FORBIDDEN.value());
            }
        };
    }

    private AuthenticationFailureHandler authenticationFailureHandler() {
        return new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
            }
        };
    }

    private AuthenticationSuccessHandler authenticationSuccessHandler() {
        return new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                Member member = (Member) authentication.getPrincipal();
                eventPublisher.publishEvent(new SigninApplicationEvent(member));
                // TODO: overhaul below
                response.addHeader("MEMBER_ROLE", member.getRole().name());
                response.setStatus(HttpStatus.OK.value());
            }
        };
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

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

    private CsrfTokenRepository csrfTokenRepository() {
        HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
        repository.setHeaderName("X-XSRF-TOKEN");
        return repository;
    }
}

and also:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class CoreSecurityConfiguration {

    @Bean
    public MemberUserDetailsService memberUserDetailsService() {
        return new MemberUserDetailsService();
    }

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

    @Bean
    public SessionRegistryImpl sessionRegistry() {
        SessionRegistryImpl sessionRegistry = new SessionRegistryImpl();
        return sessionRegistry;
    }

    @Bean
    public AffirmativeBased accessDecisionManager() {
        AffirmativeBased accessDecisionManager = new AffirmativeBased(accessDecisionVoters());
        return accessDecisionManager;
    }

    private List<AccessDecisionVoter<? extends Object>> accessDecisionVoters() {
        List<AccessDecisionVoter<? extends Object>> accessDecisionVoters = new ArrayList<>();
        accessDecisionVoters.add(roleHierarchyVoter());
        accessDecisionVoters.add(webExpressionVoter());
        return accessDecisionVoters;
    }

    @Bean
    public WebExpressionVoter webExpressionVoter() {
        WebExpressionVoter webExpressionVoter = new WebExpressionVoter();
        webExpressionVoter.setExpressionHandler(defaultWebSecurityExpressionHandler());
        return webExpressionVoter;
    }

    @Bean
    public DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler() {
        DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler();
        defaultWebSecurityExpressionHandler.setRoleHierarchy(roleHierarchy());
        return defaultWebSecurityExpressionHandler;
    }

    @Bean
    public RoleHierarchyVoter roleHierarchyVoter() {
        RoleHierarchyVoter roleHierarchyVoter = new RoleHierarchyVoter(roleHierarchy());
        return roleHierarchyVoter;
    }

    @Bean
    public RoleHierarchyImpl roleHierarchy() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        //@formatter:off
        roleHierarchy.setHierarchy(
                "ROLE_ADMINISTRATOR > ROLE_MODERATOR\n" +
                "ROLE_MODERATOR > ROLE_SUBSCRIBED_PARENTS\n" +
                "ROLE_MODERATOR > ROLE_SUBSCRIBED_CHILDCARE_WORKER\n" +
                "ROLE_SUBSCRIBED_PARENTS > ROLE_BASIC_PARENTS\n" +
                "ROLE_SUBSCRIBED_CHILDCARE_WORKER > ROLE_BASIC_CHILDCARE_WORKER");
        //@formatter:on
        return roleHierarchy;
    }

}

Can somemone please help?

edit 1:

MemberUserDetailsService:

@Component
public class MemberUserDetailsService implements UserDetailsService {

    @Autowired
    private MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        Member member = memberRepository.findByEmail(email);
        if (member == null) {
            throw new UsernameNotFoundException("Username: " + email + " not found!");
        }
        return member;
    }

}

edit 2: Here is the new config:

@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private MemberUserDetailsService memberUserDetailsService;

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Autowired
    private AccessDecisionManager accessDecisionManager;

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Autowired
    private CsrfTokenRepository csrfTokenRepository;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //@formatter:off
        http
        .headers()
        .cacheControl()
          .and()
        .and()
         .csrf()
         .csrfTokenRepository(csrfTokenRepository())
         .and()
         .rememberMe()
         .key("myKey")
         .tokenValiditySeconds(60*60*24*7)
         .userDetailsService(memberUserDetailsService)
        .and()
            .exceptionHandling()
            .accessDeniedHandler(accessDeniedHandler())
        .and()
            .formLogin()
            .loginProcessingUrl("/api/signin")
            .failureHandler(authenticationFailureHandler())
            .successHandler(authenticationSuccessHandler())
        .and()
            .logout()
            .logoutRequestMatcher(new AntPathRequestMatcher("/api/signout"))
            .logoutSuccessHandler(logoutSuccessHandler())
        .and()
            .addFilter(usernamePasswordAuthenticationFilter())
            .addFilter(rememberMeAuthenticationFilter())
            .addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class)
            .authorizeRequests()
                .accessDecisionManager(accessDecisionManager)
                .antMatchers("/resources/**", "/**").permitAll()
                .anyRequest().authenticated();
        //@formatter:on
    }

    private LogoutSuccessHandler logoutSuccessHandler() {
        return new LogoutSuccessHandler() {
            @Override
            public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                response.setStatus(HttpStatus.OK.value());
            }
        };
    }

    private AccessDeniedHandler accessDeniedHandler() {
        return new AccessDeniedHandler() {
            @Override
            public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
                // TODO: deal with InvalidCsrfTokenException & MissingCsrfTokenException
                response.setStatus(HttpStatus.FORBIDDEN.value());
            }
        };
    }

    private AuthenticationFailureHandler authenticationFailureHandler() {
        return new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
            }
        };
    }

    private AuthenticationSuccessHandler authenticationSuccessHandler() {
        return new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                response.setStatus(HttpStatus.OK.value());
                Member member = (Member) authentication.getPrincipal();
                eventPublisher.publishEvent(new SigninApplicationEvent(member));
                response.setStatus(HttpStatus.OK.value());
                // TODO: overhaul below
                response.addHeader("MEMBER_ROLE", member.getRole().name());
            }
        };
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

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

    @Bean
    protected CsrfTokenRepository csrfTokenRepository() {
        HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
        repository.setHeaderName("X-XSRF-TOKEN");
        return repository;
    }

    @Bean
    public RememberMeAuthenticationProvider rememberMeAuthenticationProvider() {
        return new RememberMeAuthenticationProvider("myKey");
    }

    @Bean
    public RememberMeServices rememberMeServices() {
        return new TokenBasedRememberMeServices("myKey", memberUserDetailsService);
    }

    @Bean
    public RememberMeAuthenticationFilter rememberMeAuthenticationFilter() throws Exception {
        return new RememberMeAuthenticationFilter(authenticationManager(), rememberMeServices());
    }

    @Bean
    public UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter() throws Exception {
        UsernamePasswordAuthenticationFilter filter = new UsernamePasswordAuthenticationFilter();
        filter.setRememberMeServices(rememberMeServices());
        filter.setAuthenticationManager(authenticationManager());
        return filter;
    }

}
Crore answered 10/4, 2015 at 14:17 Comment(5)
Please provide the steps which led to this issue.Titoism
The steps that lead to the issue is just a spring security login.Crore
Please share the code for MemberUserDetailsService as well.Titoism
I have edited my post as requested.Crore
I have done a second edit. Using the changes in edit 2, I get the same behavior as before...Crore
T
6

Since you have not specified the remember-me service implementation type, TokenBasedRememberMeServices is used by default.

Please find the below note from the documentation when using TokenBasedRememberMeServices:

Don't forget to add your RememberMeServices implementation to your UsernamePasswordAuthenticationFilter.setRememberMeServices() property, include the RememberMeAuthenticationProvider in your AuthenticationManager.setProviders() list, and add RememberMeAuthenticationFilter into your FilterChainProxy (typically immediately after your UsernamePasswordAuthenticationFilter)

You need to make the following changes:

  1. In configure() method you need to add a key and filters

    http.rememberMe().key("yourKey")

    .addFilter(usernamePasswordAuthenticationFilter()) .addFilter(rememberMeAuthenticationFilter())

  2. Create UsernamePasswordAuthenticationFilter and RememberMeAuthenticationFilter

    @Bean
    public UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter() 
                   throws Exception {
        UsernamePasswordAuthenticationFilter filter = 
                   new UsernamePasswordAuthenticationFilter();
        filter.setRememberMeServices(memberUserDetailsService);     
        filter.setAuthenticationManager(authenticationManager());
        return filter;
    }
    
    @Bean
    public RememberMeAuthenticationFilter rememberMeAuthenticationFilter() 
                        throws Exception {
        RememberMeAuthenticationFilter filter = 
                new RememberMeAuthenticationFilter(authenticationManager(), memberUserDetailsService);
        return filter;
    }
    
  3. Add RememberMeAuthenticationProvider to the list of providers:

    @Override
    protected void configure(AuthenticationManagerBuilder auth) 
        throws Exception {            
        auth.userDetailsService(memberUserDetailsService)
            .passwordEncoder(passwordEncoder)
            .and()
            .authenticationProvider(new RememberMeAuthenticationProvider("yourKey"));
    }
    
Titoism answered 13/4, 2015 at 4:33 Comment(5)
Thanks for your input. I have tried the provided code. It has two errors though: in filter.setRememberMeServices(memberUserDetailsService); & new RememberMeAuthenticationFilter(authenticationManager(), memberUserDetailsService); memberUserDetailsService should be rememberMeServices?Crore
It does not seem to work unfortunately. I am editing my post to reflect the exact changes. What about filter order? Does it matter?Crore
Thank you for the corrctions. With respect to the ordering of filters, UsernamePasswordAuthenticationFilter should come before RememberMeAuthenticationFilter in filter chain, which is already taken care in your configuration. Just one more change, not sure if it will work, could you remove CsrfHeaderFilter and try.Titoism
Hi @Mithun, I tried removing the CsrfHeaderFilter. It still has the same behavior..Crore
Thanks man, this just saved me my sanity ... even ~8 years later I owe you a beerSheilahshekel

© 2022 - 2024 — McMap. All rights reserved.