2

My controller class is a follows:

 @PostMapping(path="/users/{id}")
    @PreAuthorize("hasAnyAuthority('CAN_READ')")
    public @ResponseBody ResponseEntity<User> getUser(@PathVariable int id) {
        ...
    }

I have the following Resource Server config

@Configuration
public class ResourceServerCofig implements ResourceServerConfigurer {

    private static final String RESOURCE_ID = "test";

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.cors()
            .and()
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/public/**").permitAll()
            .anyRequest().authenticated();
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId(RESOURCE_ID);

    }
}

Finally my test looks like this:

 @RunWith(SpringRunner.class)
  @WebMvcTest(ClientController.class)
  public class ClientControllerTest {

      @Autowired
      private MockMvc mockMvc;

      @WithMockUser(authorities={"CAN_READ"})
      @Test
      public void should_get_user_by_id() throws Exception {
          ...

          mockMvc.perform(MockMvcRequestBuilders.get("/user/1")).
              andExpect(MockMvcResultMatchers.status().isOk()).
              andExpect(MockMvcResultMatchers.header().string(HttpHeaders.CONTENT_TYPE, "application/json")).
              andExpect(MockMvcResultMatchers.jsonPath("$.name").value("johnson"));
      }
  }

Issue is I always get a 401 HTTP status with message unauthorized","error_description":"Full authentication is required to access this resource.

How can should I write tests for @PreAuthorized annotated controller methods methods?

Moses Besong
  • 65
  • 1
  • 5
  • Do you have an `@EnableGlobalMethodSecurity(prePostEnabled = true)` annotation on one of your configuration classes? – Gimby Jan 09 '20 at 15:30
  • Yes, I have it on my main class – Moses Besong Jan 09 '20 at 15:35
  • Then I'm out of ideas, your setup looks identical to mine (although I use hasRole() instead of hasAuthority(), but that's nearly identical). You did verify that a live request works right? Otherwise you may be spinning your wheels looking at the test code while there is a problem in the Spring Security setup... – Gimby Jan 09 '20 at 15:46
  • The live request works. I just tested it again. This is really strange. Been on it since yesterday. – Moses Besong Jan 09 '20 at 16:18
  • What is your Spring version ? – RUARO Thibault Jan 09 '20 at 16:43
  • RUARO asks a very relevant question. Come to think of it, I do have a service where the tests are working in the Spring Boot 1.5 version but the same tests fail in the branch which has been upgraded to Spring Boot 2.1 with the same "authentication required" problem - but the live requests work. – Gimby Jan 09 '20 at 16:45
  • @RUAROThibault I am using Spring Boot 2.2.2.RELEASE – Moses Besong Jan 09 '20 at 17:29
  • Are you using OAuth2 as authentication mechanisms ? If so, then you have a resource and authorization server ? – RUARO Thibault Jan 09 '20 at 17:40
  • Yes, I am using OAuth2 and I am hitting an Authorization server. I have a test Bearer Token I use when making requests on the live application. – Moses Besong Jan 09 '20 at 18:09
  • 1
    I'll give you an answer by tomorrow :) – RUARO Thibault Jan 09 '20 at 20:17
  • 1
    Just to note, I solved the problem I had. It in the end had nothing to do with the OAUTH2 stuff because the entire resource server configuration was not even part of the context in the tests (you'd have to specifically `@Import` it); basically the default spring security configuration was activating and that triggered a CSRF check to fail. Fixing that check restored the unit tests to working order. – Gimby Jan 17 '20 at 12:34

1 Answers1

4

I've spent part of the day figuring out how to solve this. I ended up with a solution that I think is not so bad, and could help many.

Based on what you tried to do in your test, you can do a mockMvc to test your controller. Notice that the AuthorizationServer is not called. You stay only in your Resource server for the tests.

  • Create a bean InMemoryTokenStore that will be used in the OAuth2AuthenticationProcessingFilter to authenticate your user. What is great is that you can add tokens to your InMemoryTokenStore before executing your tests. The OAuth2AuthenticationProcessingFilter will authenticate the user based on the token he is using and will not call any remote server.
@Configuration
public class AuthenticationManagerProvider {

    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }
}
  • The annotation @WithMockUser does not work with OAuth2. Indeed, the OAuth2AuthenticationProcessingFilter always checks your token, wi no regard to the SecurityContext. I suggest to use the same approach as @WithMockUser, but using an annotation you create in your code base. (To have some easy to maintain and clean tests):
    @WithMockOAuth2Scope contains almost all the parameters you need to customize your authentication. You can delete those you will never use, but I put a lot to make sure you see the possibilities. (Put those 2 classes in your test folder)
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockOAuth2ScopeSecurityContextFactory.class)
public @interface WithMockOAuth2Scope {

    String token() default "";

    String clientId() default "client-id";

    boolean approved() default true;

    String redirectUrl() default "";

    String[] responseTypes() default {};

    String[] scopes() default {};

    String[] resourceIds() default {};

    String[] authorities() default {};

    String username() default "username";

    String password() default "";

    String email() default "";
}

Then, we need a class to interpret this annotation and fill our `InMemoryTokenStore with the data you need for your test.

@Component
public class WithMockOAuth2ScopeSecurityContextFactory implements WithSecurityContextFactory<WithMockOAuth2Scope> {

    @Autowired
    private TokenStore tokenStore;

    @Override
    public SecurityContext createSecurityContext(WithMockOAuth2Scope mockOAuth2Scope) {

        OAuth2AccessToken oAuth2AccessToken = createAccessToken(mockOAuth2Scope.token());
        OAuth2Authentication oAuth2Authentication = createAuthentication(mockOAuth2Scope);
        tokenStore.storeAccessToken(oAuth2AccessToken, oAuth2Authentication);

        return SecurityContextHolder.createEmptyContext();
    }


    private OAuth2AccessToken createAccessToken(String token) {
        return new DefaultOAuth2AccessToken(token);
    }

    private OAuth2Authentication createAuthentication(WithMockOAuth2Scope mockOAuth2Scope) {

        OAuth2Request oauth2Request = getOauth2Request(mockOAuth2Scope);
        return new OAuth2Authentication(oauth2Request,
                getAuthentication(mockOAuth2Scope));
    }

    private OAuth2Request getOauth2Request(WithMockOAuth2Scope mockOAuth2Scope) {
        String clientId = mockOAuth2Scope.clientId();
        boolean approved = mockOAuth2Scope.approved();
        String redirectUrl = mockOAuth2Scope.redirectUrl();
        Set<String> responseTypes = new HashSet<>(asList(mockOAuth2Scope.responseTypes()));
        Set<String> scopes = new HashSet<>(asList(mockOAuth2Scope.scopes()));
        Set<String> resourceIds = new HashSet<>(asList(mockOAuth2Scope.resourceIds()));

        Map<String, String> requestParameters = Collections.emptyMap();
        Map<String, Serializable> extensionProperties = Collections.emptyMap();
        List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(mockOAuth2Scope.authorities());

        return new OAuth2Request(requestParameters, clientId, authorities,
                approved, scopes, resourceIds, redirectUrl, responseTypes, extensionProperties);
    }

    private Authentication getAuthentication(WithMockOAuth2Scope mockOAuth2Scope) {
        List<GrantedAuthority> grantedAuthorities = AuthorityUtils.createAuthorityList(mockOAuth2Scope.authorities());

        String username = mockOAuth2Scope.username();
        User userPrincipal = new User(username,
                mockOAuth2Scope.password(),
                true, true, true, true, grantedAuthorities);

        HashMap<String, String> details = new HashMap<>();
        details.put("user_name", username);
        details.put("email", mockOAuth2Scope.email());

        TestingAuthenticationToken token = new TestingAuthenticationToken(userPrincipal, null, grantedAuthorities);
        token.setAuthenticated(true);
        token.setDetails(details);

        return token;
    }

}
  • Once everything is setup, create a simple Test class under src/test/java/your/package/. This class will do the mockMvc operations, and use the @ WithMockOAuth2Scope to create the token you need for your test.
@WebMvcTest(SimpleController.class)
@Import(AuthenticationManagerProvider.class)
class SimpleControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @WithMockOAuth2Scope(token = "123456789",
            authorities = "CAN_READ")
    public void test() throws Exception {
        mockMvc.perform(get("/whoami")
                .header("Authorization", "Bearer 123456789"))
                .andExpect(status().isOk())
                .andExpect(content().string("username"));
    }
}

I came up with this solutions thanks to:

For the curious:
When testing, Spring loads a InMemoryTokenStore, and give you the possibility to take one you provide (as a @Bean). When running in production, for my case, Spring uses a RemoteTokenStore, which calls the remote Authorization server to check the token (http://authorization_server/oauth/check_token).
When you decide to use OAuth2, Spring fires the OAuth2AuthenticationProcessingFilter. This was my entry point during all me debugging sessions.

I've learned a lot, thank you for this.
You can find the source code here.

Hope it will help !

RUARO Thibault
  • 2,672
  • 1
  • 9
  • 14
  • Thank you @ruaro for your solution. When I test within my project I get a `org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext` error message. Cloning and running test in the linked Github repository, I get a `java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test` error message. – Moses Besong Jan 11 '20 at 20:08
  • You must execute the `SimpleControllerTest`, not the `OauthtestApplicationTests`. Sorry I didn't clean everything. Regarding the error on your project, could you give us the code you tried ? Where did you put the classes, how do you run your tests? What did you change since the last time ? – RUARO Thibault Jan 11 '20 at 21:53
  • You where right. Solution works. I executed `SimpleControllerTest` and the test passed. This caused me to recheck my code. Now all working fine. Very much thanks again. – Moses Besong Jan 12 '20 at 00:14
  • *"The annotation @WithMockUser does not work with OAuth2"* - with ResourceServerConfigurationAdapter indeed, I kind of came to the same conclusion after several hours of research and experimentation. Which is a regression because in Spring Boot 1.5 it works quite well. – Gimby Jan 13 '20 at 09:02
  • Unfortunaly, Spring-boot is not meant to be compatible when changing a major version (even minor sometimes). They really changed the way of using the security within the tests. This is how they behaved since the beginning of spring-boot. – RUARO Thibault Jan 13 '20 at 10:19