spring-projects / spring-security

Spring Security
http://spring.io/projects/spring-security
Apache License 2.0
8.79k stars 5.9k forks source link

Configuration rules that worked in Spring Security 5 don't work in 6.0.1 #12750

Closed mhussainshah1 closed 4 months ago

mhussainshah1 commented 1 year ago

Describe the bug Configuration rules that worked in Spring Security 5 don't work in 6.0.1.

After migrating the security configuration to Spring Security 6.0.1, if we use a bad credentials(username/password) then a browser gets stuck, Hibernate runs a query endlessly and the control does not redirect to the login page. The project uses Spring MVC and ThymeLeaf.

To migrate from Spring Security 5 (Spring Boot 2.1.3.RELEASE) to Spring Security 6.0.1 (Spring Boot 3.0.2) I changed SecurityConfiguration.java file from

    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http
                .authorizeRequests()
                .antMatchers("/","/h2-console/**").permitAll()
                .antMatchers("/admin").access("hasAuthority('ADMIN')")
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login").permitAll()
                .and()
                .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/login").permitAll()
                .and()
                .httpBasic();
        http
                .csrf().disable();
        http
                .headers().frameOptions().disable();
    }

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

to

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/", "/h2-console/**")
                        .permitAll()
                        .requestMatchers("/admin").hasRole("ADMIN")
                        .anyRequest().authenticated()
                )
                .formLogin(form -> form
                        .loginPage("/login")
                        .failureUrl("/login?error=true")
                        .permitAll())
                .logout(logout -> logout
                        .logoutUrl("/logout")
                        .logoutSuccessUrl("/login?logout")
                        .permitAll())
                .httpBasic(Customizer.withDefaults());
        http
                .csrf().disable();
        http
                .headers().frameOptions().disable();
        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder())
                .and()
                .build();
    }
}

and I changed SSUserDetailsService.java file from

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try {
            User appUser = userRepository.findByUsername(username);

            if(appUser == null){
                System.out.println("User not found with the provided username" + appUser.toString());
                return null;
            }
            System.out.println("User from username " + appUser.toString());
            return new org.springframework.security.core.userdetails.User(
                    appUser.getUsername(),
                    appUser.getPassword(),
                    getAuthorities(appUser));

        } catch (Exception e){
            throw new UsernameNotFoundException("User not found");
        }
    }

    private Set<GrantedAuthority> getAuthorities(User appUser) {
        Set<GrantedAuthority> authorities = new HashSet<>();
        for(Role role: appUser.getRoles()){
            GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getRole());
            authorities.add(grantedAuthority);
        }
        System.out.println("User authorities are" + authorities.toString());
        return authorities;
    }

to

   @Override
    public UserDetails loadUserByUsername(String username){
        try {
            User appUser = userRepository.findByUsername(username);

            if (appUser == null) {
                System.out.println("User not found with the provided username" + appUser.toString());
                return null;
            }
            System.out.println("User from username " + appUser.getUsername());
            return org.springframework.security.core.userdetails.User
                    .withUsername(appUser.getUsername())
                    .password(appUser.getPassword())
                    .roles(getAuthorities(appUser))
                    .build();

        } catch (Exception e) {
            throw new UsernameNotFoundException("User not found");
        }
    }

    private String[] getAuthorities(User appUser) {
        var authorities = new HashSet<String>();
        for (var role : appUser.getRoles()) {
            var grantedAuthority = new SimpleGrantedAuthority(role.getRole());
            authorities.add(grantedAuthority.getAuthority());
        }
        System.out.println("User authorities are " + authorities);
        return Arrays.copyOf(authorities.toArray(),authorities.size(), String[].class);
    }

To Reproduce Steps to reproduce error behavior (springboot_3.0 branch contains an example with Spring Security 6.0.1 (Spring Boot 3.0.2)):

  1. git clone -b springboot_3.0 https://github.com/mhussainshah1/SpringBoot_404.git

  2. cd SpringBoot_404

  3. mvn clean package

  4. mvn spring-boot:run

  5. Open the browser with link http://localhost:8080/

  6. Login with user/password (user is admin, password is password)

    Hibernate: select u1_0.id,u1_0.email,u1_0.enabled,u1_0.first_name,u1_0.last_name,u1_0.password,u1_0.username from user_data u1_0 where u1_0.username=?
    Hibernate: select r1_0.user_id,r1_1.id,r1_1.role from user_data_roles r1_0 join role r1_1 on r1_1.id=r1_0.role_id where r1_0.user_id=?
    User from username admin
    User authorities are [ADMIN]
  7. Login with bad credential (user is dave, password is begreat)

The browser will stuck and Hibernate query run endlessly until stopped by command CTRL + C

Hibernate: select u1_0.id,u1_0.email,u1_0.enabled,u1_0.first_name,u1_0.last_name,u1_0.password,u1_0.username from user_data u1_0 where u1_0.username=?
Hibernate: select u1_0.id,u1_0.email,u1_0.enabled,u1_0.first_name,u1_0.last_name,u1_0.password,u1_0.username from user_data u1_0 where u1_0.username=?
Hibernate: select u1_0.id,u1_0.email,u1_0.enabled,u1_0.first_name,u1_0.last_name,u1_0.password,u1_0.username from user_data u1_0 where u1_0.username=?
Hibernate: select u1_0.id,u1_0.email,u1_0.enabled,u1_0.first_name,u1_0.last_name,u1_0.password,u1_0.username from user_data u1_0 where u1_0.username=?

Expected behavior After putting in bad credentials (username or password) the login page should appear with the following text Invalid username or password

Steps to reproduce success behavior (main branch contains an example with Spring Security 5 (Spring Boot 2.1.3.RELEASE)):

  1. git clone https://github.com/mhussainshah1/SpringBoot_404.git

  2. cd SpringBoot_404

  3. mvn clean package

  4. mvn spring-boot:run

  5. Open browser with link http://localhost:8080/

  6. Login page will open successfully

  7. Log in with user/password (user is admin, password is password) The home page will open at the link http://localhost:8080/

  8. Login with bad credential (user is dave, password is begreat) It should redirect to Login Page and show as follow

Spring Security 5 Example Sample

A link to a GitHub repository with a minimal, reproducible sample. (see springboot_3.0 branch).

ashutosh049 commented 1 year ago

Hi. Have you tried providing an AuthneticationEntryPoint ?


  private final MyUserDetailsService myUserDetailsService;
  private final AppDefaultAuthenticationFailedEntryPoint appDefaultAuthenticationFailedEntryPoint;
 @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/", "/h2-console/**")
                        .permitAll()
                        .requestMatchers("/admin").hasRole("ADMIN")
                        .anyRequest().authenticated()
                )
                .formLogin(form -> form
                        .loginPage("/login")
                        .failureUrl("/login?error=true")
                        .permitAll())
                .logout(logout -> logout
                        .logoutUrl("/logout")
                        .logoutSuccessUrl("/login?logout")
                        .permitAll())
               // .httpBasic(Customizer.withDefaults());
               .httpBasic(httpBasicConfigurer_ -> {
                  httpBasicConfigurer_.realmName("SOME-REALM");
                  httpBasicConfigurer_.authenticationEntryPoint(appDefaultAuthenticationFailedEntryPoint);
        })

        http
                .csrf().disable();
        http
                .headers().frameOptions().disable();
        return http.build();
    }

AppDefaultAuthenticationFailedEntryPoint.java class

@Slf4j
@RequiredArgsConstructor
@Component
public class AppDefaultAuthenticationFailedEntryPoint implements AuthenticationEntryPoint {
  @Override
  public void commence(
      HttpServletRequest request,
      HttpServletResponse response,
      AuthenticationException authException)
      throws IOException, ServletException {

    HttpStatus status = HttpStatus.UNAUTHORIZED;
    log.error("Failed Authentication: {}", authException.getMessage(), authException);
    response.setStatus(status.value());
    response.sendError(status.value(), "Unauthorized Access");
  }
}
sirDarey commented 1 year ago

Nice Article. However, the method, UserDetails loadUserByUsername is not allowed to return null; You must return UsernameNotFoundException.

sjohnr commented 4 months ago

Hi @mhussainshah1, I believe @sirDarey is correct.