How to interpolate a property inside Spring Security @PreAuthorize / @PostAuthorize

Featured image for sharing metadata for article

If you're using Spring Security to authorize your application's authentication and authorization needs, you may want to extract some of your rules to configuration, rather than code, to allow quicker changes to the rulesets.

For instance, in this very contrived example, we may have an endpoint that can only be accessed by a single user in our system, bob.

Our code starts to look like this:

import java.util.List;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/apis")
public class ApiController {

  @GetMapping
  @PreAuthorize("authentication.name == 'bob'")
  public List<String> getAll() {
    return List.of("Api name here", "another");
  }
}

Which has the following Integration Test:

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import me.jvt.hacking.application.Application;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(ApiController.class)
@ContextConfiguration(classes = Application.class)
@AutoConfigureMockMvc
class ApiControllerIntegrationTest {
  @Autowired private MockMvc mvc;

  @Test
  @WithMockUser("bob")
  void whenBob() throws Exception {
    mvc.perform(get("/apis")).andExpect(status().isOk());
  }

  @Test
  @WithMockUser("alan")
  void whenNotBob() throws Exception {
    mvc.perform(get("/apis")).andExpect(status().isForbidden());
  }
}

But if we were to need to migrate the access away from bob and instead to jessica, we'd need to introduce code changes, which are slower than a tweak in our application.properties i.e.:

authorization.users.apis-endpoint=bob

Unfortunately, Spring Security doesn't allow us to interpolate the value of a property in the @PreAuthorize / @PostAuthorize annotations, using the Spring Expression Language (SPEL).

Fortunately, Spring Security can delegate to a method provided by a bean in the application context, so we can create a central class for this logic - which is beneficial as it's unit-testable, and can contain other, more complex rules:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class ApisEndpointAuthorizer {
  private final String validPrincipal;

  public ApisEndpointAuthorizer(@Value("${authorization.users.apis-endpoint") String validPrincipal) {
    this.validPrincipal = validPrincipal; // this could also become a `Set<String>`
  }

  public boolean isAuthorized(String principal) { // this could take the `authentication`, and do other checks, too
    return validPrincipal.equals(principal);
  }
}

This allows our controller to reference the bean's method isAuthorized like so:

import java.util.List;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/apis")
public class ApiController {

  @GetMapping
  @PreAuthorize("@apisEndpointAuthorizer.isAuthorized(authentication.name)")
  public List<String> getAll() {
    return List.of("Api name here", "another");
  }
}

And as we're aiming to mock as much as possible, we'd have our Integration Test like so:

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import me.jvt.hacking.Application;
import me.jvt.hacking.infrastructure.ApisEndpointAuthorizer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(ApiController.class)
@ContextConfiguration(classes = Application.class)
@AutoConfigureMockMvc
class ApiControllerIntegrationTest {
  @Autowired private MockMvc mvc;

  @MockBean(name = "apisEndpointAuthorizer") // required, as otherwise its name isn't correct
  private ApisEndpointAuthorizer apisEndpointAuthorizer;

  @Test
  @WithMockUser("bob")
  void whenAuthorized() throws Exception {
    when(apisEndpointAuthorizer.isAuthorized(any())).thenReturn(true);

    mvc.perform(get("/apis")).andExpect(status().isOk());
  }

  @Test
  @WithMockUser("alan")
  void whenNotAuthorized() throws Exception {
    when(apisEndpointAuthorizer.isAuthorized(any())).thenReturn(false);

    mvc.perform(get("/apis")).andExpect(status().isForbidden());
  }
}

And then we'd have a full integration test:

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
class ApplicationIntegrationTest {
  @Autowired private MockMvc mvc;

  @Test
  @WithMockUser("bob")
  void whenAuthorized() throws Exception {
    mvc.perform(get("/apis")).andExpect(status().isOk());
  }

  @Test
  @WithMockUser("alan")
  void whenNotAuthorized() throws Exception {
    mvc.perform(get("/apis")).andExpect(status().isForbidden());
  }
}

Written by Jamie Tanna's profile image Jamie Tanna on , and last updated on .

Content for this article is shared under the terms of the Creative Commons Attribution Non Commercial Share Alike 4.0 International, and code is shared under the Apache License 2.0.

#blogumentation #java #spring #spring-boot #spring-security.

Also on:

This post was filed under articles.

Interactions with this post

Interactions with this post

Below you can find the interactions that this page has had using WebMention.

Have you written a response to this post? Let me know the URL:

Do you not have a website set up with WebMention capabilities? You can use Comment Parade.