Davide Marcoli
Davide Marcoli

Reputation: 151

@PreAuthorize not working after upgrading to Spring Boot 3 (Spring Security 6)

I have upgraded my Spring Boot Project to Spring Boot 3.

I've also updated the WebSecurityConfig, it now looks like that:

// imports...

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class CustomWebSecurityConfig {
    final UserDetailsServiceImpl userDetailsService;

    private final AuthEntryPointJwt unauthorizedHandler;
    private final PasswordEncoder passwordEncoder;

    @Bean
    public AuthTokenFilter authenticationJwtTokenFilter() {
        return new AuthTokenFilter();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();

        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder);

        return authProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }

    /**
     * Sets up a chain of antmatchers specifying what permissions and roles have access to which resources.
     *
     * @param http          Injected HttpSecurity object
     * @return              Chain of Security filters
     * @throws Exception    Currently throws general exception
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                // https://stackoverflow.com/questions/74447778/spring-security-in-spring-boot-3
                .authorizeHttpRequests(requests -> requests.requestMatchers("/api/auth/**").permitAll()
                        .requestMatchers("/api/test/**").permitAll()
                        .requestMatchers("/").permitAll()
                        .requestMatchers("/index.html").permitAll()
                        .requestMatchers("/favicon.ico").permitAll()
                        .requestMatchers("/main.js").permitAll()
                        .requestMatchers("/polyfills.js").permitAll()
                        .requestMatchers("/runtime.js").permitAll()
                        .requestMatchers("/styles.css").permitAll()
                        .requestMatchers("/vendor.css").permitAll()
                        .requestMatchers("/assets/**").permitAll()
                        .requestMatchers("/error").permitAll()
                        .requestMatchers("/**").permitAll()
                        .anyRequest().authenticated());

        http.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.authenticationProvider(authenticationProvider());

        http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

This is a sample Endpoint with @PreAuthorize:


// imports...

@RestController
@RequestMapping("/api/test")
public class TestController {
    @GetMapping("/all")
    public String allAccess() {
        return "Public Content.";
    }

    @GetMapping("/user")
    @PreAuthorize("hasRole('USER') or hasRole('MODERATOR') or hasRole('ADMIN')")
    public String userAccess() {
        return "User Content.";
    }

    @GetMapping("/mod")
    @PreAuthorize("hasRole('MODERATOR')")
    public String moderatorAccess() {
        return "Moderator Board.";
    }

    @GetMapping("/admin")
    @PreAuthorize("hasRole('ADMIN')")
    public String adminAccess() {
        return "Admin Board.";
    }
}

The tests i have written for this usecase fail partially, because a logged in user has access to all the Endpoint, but only has the "USER"-Role per default. These 2 tests fail:

@Test
@DisplayName("Give user no token and forbid access")
@WithMockUser(roles = "USER")
void givenUserToken_whenGetSecureRequest_thenForbidden() throws Exception {
    mockMvc.perform(get("/api/test/mod"))
            .andExpect(status().isForbidden());
}

@Test
@DisplayName("Give user no token and forbid access v.2")
@WithMockUser(roles = "USER")
void givenUserToken_whenGetSecureRequest_thenForbidden2() throws Exception {
    mockMvc.perform(get("/api/test/admin"))
            .andExpect(status().isForbidden());
}

I read something about @EnableMethodSecurity, but i haven't found a way to use it and fix @PreAuthorize not working

Upvotes: 13

Views: 13633

Answers (1)

jcompetence
jcompetence

Reputation: 8383

According to the documentation:

EnableMethodSecurity

In Spring Security 5.6, we can enable annotation-based security using the @EnableMethodSecurity annotation on any @Configuration instance.

Simply add this annotation on top of your @Configuration class and it should work.

Example:

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeHttpRequests((authorize) -> authorize
                        .anyRequest().authenticated()
                ).httpBasic(withDefaults());
        return http.build();
    }
}

This improves upon @EnableGlobalMethodSecurity in a number of ways. @EnableMethodSecurity:

Uses the simplified AuthorizationManager API instead of metadata sources, config attributes, decision managers, and voters. This simplifies reuse and customization.

Favors direct bean-based configuration, instead of requiring extending GlobalMethodSecurityConfiguration to customize beans

Is built using native Spring AOP, removing abstractions and allowing you to use Spring AOP building blocks to customize

Checks for conflicting annotations to ensure an unambiguous security configuration

Complies with JSR-250

Enables @PreAuthorize, @PostAuthorize, @PreFilter, and @PostFilter by default

https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html

Upvotes: 21

Related Questions