Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ProviderManager.authenticate called twice for BadCredentialsException

Spring 4.1 and Spring Security 3.2: We implemented a Custom Authentication Provider, that throws a BadCredentialsException if user enters an incorrect password. When the BadCredentialsException is thrown, the ProviderManager.authenticate method is called, which calls the authenticate method in the Custom Authentication again. When a LockedException is thrown, the authenicate method in the Custom Authentication Provider is not called again. We are planning on keeping a count of number of login attempts, so we don't want the authenticate method called twice. Does anyone know why the authenticate method in the custom authentication class would be called twice?

WebConfig:


  @Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomAuthenticationProvider customAuthenticationProvider;

    @Autowired
    private AMCiUserDetailsService userDetailsService;

    @Autowired
    private CustomImpersonateFailureHandler impersonateFailureHandler;

    @Autowired
    private LoginFailureHandler loginFailureHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
                .antMatchers("/jsp/*.css","/jsp/*.js","/images/**").permitAll()  
                .antMatchers("/login/impersonate*").access("hasRole('ADMIN') or hasRole('ROLE_PREVIOUS_ADMINISTRATOR')") 
                .anyRequest().authenticated()                                    
                .and()
            .formLogin()
                .loginPage("/login.jsp")
                .defaultSuccessUrl("/jsp/Home.jsp",true)                
                .loginProcessingUrl("/login.jsp")                                 
                .failureHandler(loginFailureHandler)
                .permitAll()
                .and()
            .logout()
                .logoutSuccessUrl("/login.jsp?msg=1")
                .permitAll()
                .and()
            .addFilter(switchUserFilter())
            .authenticationProvider(customAuthenticationProvider);

            http.exceptionHandling().accessDeniedPage("/jsp/SecurityViolation.jsp");  //if user not authorized to a page, automatically forward them to this page.
            http.headers().addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN)); 
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(customAuthenticationProvider);
    }

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

    //Used for the impersonate functionality
    @Bean CustomSwitchUserFilter switchUserFilter() {
        CustomSwitchUserFilter filter = new CustomSwitchUserFilter();
        filter.setUserDetailsService(userDetailsService);
        filter.setTargetUrl("/jsp/Impersonate.jsp?msg=0");
        filter.setSwitchUserUrl("/login/impersonate");
        filter.setExitUserUrl("/logout/impersonate");
        filter.setFailureHandler(impersonateFailureHandler);
        return filter;
    }
}

Custom Authentication Provider:

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Autowired(required = true)
    private HttpServletRequest request;

    @Autowired
    private AMCiUserDetailsService userService;

    @Autowired
    private PasswordEncoder encoder;

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        String username = authentication.getName().trim();
        String password = ((String) authentication.getCredentials()).trim();
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
            throw new BadCredentialsException("Login failed! Please try again.");
        }


        UserDetails user;
        try {
            user = userService.loadUserByUsername(username);
            //log successful attempt
            auditLoginBean.setComment("Login Successful");
            auditLoginBean.insert(); 
        } catch (Exception e) {
             try {
                //log unsuccessful attempt
                auditLoginBean.setComment("Login Unsuccessful");
                auditLoginBean.insert();
             } catch (Exception e1) {
                // TODO Auto-generated catch block
             }
            throw new BadCredentialsException("Please enter a valid username and password.");
        }

        if (!encoder.matches(password, user.getPassword().trim())) {
            throw new BadCredentialsException("Please enter a valid username and password.");
        }

        if (!user.isEnabled()) {
            throw new DisabledException("Please enter a valid username and password.");
        }

        if (!user.isAccountNonLocked()) {
            throw new LockedException("Account locked. ");
        }

        Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
        List<GrantedAuthority> permlist = new ArrayList<GrantedAuthority>(authorities);

        return new UsernamePasswordAuthenticationToken(user, password, permlist);
    }


    public boolean supports(Class<? extends Object> authentication) {
        return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
    }
like image 634
Maria Speicher Avatar asked Nov 18 '15 18:11

Maria Speicher


2 Answers

The reason is that you add your authentication provider twice, one time in configure(HttpSecurity) and one time in configure(AuthenticationManagerBuilder). This will create a ProviderManager with two items, both being your provider.

When authentication is processed, the providers will be asked in order until a success is made, unless a LockedException or similar status exception is thrown, then the loop will break.

like image 157
holmis83 Avatar answered Nov 20 '22 12:11

holmis83


There may be a situtation which you don't override configure(AuthenticationManagerBuilder) and still same AuthenticationProver's authenticate method gets called twice like Phil mentioned in his comment in the accepted answer.

Why is that?

The reason is that when you don't override configure(AuthenticationManagerBuilder) and have an AuthenticationProvider bean, it will be registered by Spring Security, you don't have to do anything else.

However, when configure(AuthenticationManagerBuilder) overridden, Spring Security will invoke it and won't try to register any provider by itself. If you're curious, you can take a look the related method. disableLocalConfigureAuthenticationBldr is true if you override configure(AuthenticationManagerBuilder).

So, briefly, if you want to register just one custom AuthenticationProvider then do not override configure(AuthenticationManagerBuilder), do not call authenticationProvider(AuthenticationProvider) in configure(HttpSecurity), just make your AuthenticationProviver implementation bean by annotating @Component and you're good to go.

like image 33
sedooe Avatar answered Nov 20 '22 14:11

sedooe