Course – LSS – NPI (cat=Spring Security)
announcement - icon

If you're working on a Spring Security (and especially an OAuth) implementation, definitely have a look at the Learn Spring Security course:

>> LEARN SPRING SECURITY

1. Introduction

In our tutorial on Spring method security, we saw how we can use the @PreAuthorize and @PostAuthorize annotations.

In this tutorial, we’ll see how to deny access to methods that lack authorization annotations.

2. Security by Default

After all, we are only human, so we might forget to protect one of our endpoints. Unfortunately, there’s no easy way to deny access to non-annotated endpoints.

Luckily, Spring Security requires authentication for all endpoints by default. However, it will not require a specific role. Also, it will not deny access when we did not add security annotations.

3. Setup

First, let’s take a look at the application for this example. We have a simple Spring Boot application:

@SpringBootApplication
public class DenyApplication {
    public static void main(String[] args) {
        SpringApplication.run(DenyApplication.class, args);
    }
}

Secondly, we have a security configuration. We set up two users and enable the pre/post annotations:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class DenyMethodSecurityConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(
            User.withUsername("user").password("{noop}password").roles("USER").build(),
            User.withUsername("guest").password("{noop}password").roles().build()
        );
    }
}

Finally, we have a rest controller with two methods. However, we “forgot” to protect the /bye endpoint:

@RestController
public class DenyOnMissingController {
    @GetMapping(path = "hello")
    @PreAuthorize("hasRole('USER')")
    public String hello() {
        return "Hello world!";
    }

    @GetMapping(path = "bye")
    // whoops!
    public String bye() {
        return "Bye bye world!";
    }
}

When running the example, we can sign in with user/password. Then, we access the /hello endpoint. We can also sign in with guest/guest. In that case, we cannot access the /hello endpoint.

However, any authenticated user can access the /bye endpoint. In the next section, we write a test to prove that.

4. Testing the Solution

Using MockMvc we can set up a test. We check that our non-annotated method is still accessible:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = DenyApplication.class)
public class DenyOnMissingControllerIntegrationTest {
   
    @Autowired
    private WebApplicationContext context;
    private MockMvc mockMvc;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
    }

    @Test
    @WithMockUser(username = "user")
    public void givenANormalUser_whenCallingHello_thenAccessDenied() throws Exception {
        mockMvc.perform(get("/hello"))
          .andExpect(status().isOk())
          .andExpect(content().string("Hello world!"));
    }

    @Test
    @WithMockUser(username = "user")
    // This will fail without the changes from the next section
    public void givenANormalUser_whenCallingBye_thenAccessDenied() {
        ServletException exception = Assertions.assertThrows(ServletException.class, () -> mockMvc.perform(get("/bye")));

        Assertions.assertNotNull(exception);
        Assertions.assertEquals(exception.getCause().getClass(), AccessDeniedException.class);
    }
}

The second test fails because the /bye endpoint is accessible. In the next section, we update our configuration to deny access to unannotated endpoints.

5. Solution: Deny by Default

Let’s extend our MethodSecurityConfig class and set up a MethodSecurityMetadataSource:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class DenyMethodSecurityConfig {

    @Bean
    public Advisor preAuthorize(CustomPermissionAllowedMethodSecurityMetadataSource manager) {
        JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut();
        pattern.setPattern("com.baeldung.denyonmissing.*");
        AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor(pattern, manager);
        interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder() - 1);
        return interceptor;
    }
    
    // setting up in memory users not repeated
    ....
}

Now let’s implement the MethodSecurityMetadataSource interface:

@Component
public class CustomPermissionAllowedMethodSecurityMetadataSource implements AuthorizationManager<MethodInvocation> {

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation mi) {
        MergedAnnotations annotations = MergedAnnotations.from(mi.getMethod(), MergedAnnotations.SearchStrategy.DIRECT);
        List<ConfigAttribute> attributes = new ArrayList<>();

        MergedAnnotations classAnnotations = MergedAnnotations.from(DenyOnMissingController.class,  MergedAnnotations.SearchStrategy.DIRECT);
        // if the class is annotated as @Controller we should by default deny access to every method
        if (classAnnotations.get(Controller.class).isPresent()) {
            attributes.add(DENY_ALL_ATTRIBUTE);
        }

        if (annotations.get(PreAuthorize.class).isPresent() || annotations.get(PostAuthorize.class).isPresent()) {
            return null;
        }
        return new AuthorizationDecision(!Collections.disjoint(attributes, authentication.get().getAuthorities()));
    }
}

We’ll add the DENY_ALL_ATTRIBUTE to all methods of @Controller classes.

But, we don’t add them if a @PreAuthorize/@PostAuthorize annotation is found. We do this by returning null, indicating that no metadata applies.

With the updated code, our /bye endpoint is protected and the tests succeed.

6. Conclusion

In this short tutorial, we’ve shown how to protect endpoints lacking @PreAuthorize / @PostAuthorize annotations.

Also, we show that non-annotated methods are now indeed protected.

As always, the full source code of the article is available over on GitHub.

Course – LSS (cat=Security/Spring Security)

I just announced the new Learn Spring Security course, including the full material focused on the new OAuth2 stack in Spring Security:

>> CHECK OUT THE COURSE
res – Security (video) (cat=Security/Spring Security)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.