Quantcast
Channel: Baeldung
Viewing all articles
Browse latest Browse all 3524

Deny Access on Missing @PreAuthorize to Spring Controller Methods

$
0
0

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
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class DenyMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    @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 {
    @Rule
    public ExpectedException expectedException = ExpectedException.none();

    @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() throws Exception {
        expectedException.expectCause(isA(AccessDeniedException.class));

        mockMvc.perform(get("/bye"));
    }
}

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 
@EnableGlobalMethodSecurity(prePostEnabled = true) 
public class DenyMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    @Override
    protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() {
        return new CustomPermissionAllowedMethodSecurityMetadataSource();
    }
    // setting up in memory users not repeated
    ...
}

Now let's implement the MethodSecurityMetadataSource interface:

public class CustomPermissionAllowedMethodSecurityMetadataSource 
  extends AbstractFallbackMethodSecurityMetadataSource {
    @Override
    protected Collection findAttributes(Class<?> clazz) { return null; }

    @Override
    protected Collection findAttributes(Method method, Class<?> targetClass) {
        Annotation[] annotations = AnnotationUtils.getAnnotations(method);
        List attributes = new ArrayList<>();

        // if the class is annotated as @Controller we should by default deny access to all methods
        if (AnnotationUtils.findAnnotation(targetClass, Controller.class) != null) {
            attributes.add(DENY_ALL_ATTRIBUTE);
        }

        if (annotations != null) {
            for (Annotation a : annotations) {
                // but not if the method has at least a PreAuthorize or PostAuthorize annotation
                if (a instanceof PreAuthorize || a instanceof PostAuthorize) {
                    return null;
                }
            }
        }
        return attributes;
    }

    @Override
    public Collection getAllConfigAttributes() { return null; }
}

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.


Viewing all articles
Browse latest Browse all 3524

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>