73

Following the release of Spring Security 4 and it's improved support for testing I've wanted to update my current Spring security oauth2 resource server tests.

At present I have a helper class that sets up a OAuth2RestTemplate using ResourceOwnerPasswordResourceDetails with a test ClientId connecting to an actual AccessTokenUri to requests a valid token for my tests. This resttemplate is then used to make requests in my @WebIntegrationTests.

I'd like to drop the dependency on the actual AuthorizationServer, and the use of valid (if limited) user credentials in my tests, by taking advantage of the new testing support in Spring Security 4.

Up to now all my attempts at using @WithMockUser, @WithSecurityContext, SecurityMockMvcConfigurers.springSecurity() & SecurityMockMvcRequestPostProcessors.* have failed to make authenticated calls through MockMvc, and I can not find any such working examples in the Spring example projects.

Can anyone help me test my oauth2 resource server with some kind of mocked credentials, while still testing the security restrictions imposed?

** EDIT ** Sample code available here: https://github.com/timtebeek/resource-server-testing For each of the test classes I understand why it won't work as it, but I'm looking for ways that would allow me to test the security setup easily.

I'm now thinking of creating a very permissive OAuthServer under src/test/java, which might help a bit. Does anyone have any other suggestions?

Manos Nikolaidis
  • 21,608
  • 12
  • 74
  • 82
Tim
  • 19,793
  • 8
  • 70
  • 95
  • Can you provide an example of what one of your tests looks like? Are you just testing method based security? Are you using MockMvc? Are you making actual REST calls to your service? – Rob Winch Apr 08 '15 at 13:54
  • @RobWinch I've added sample code using each method, and understand why it does not work. I'm looking for ways that will work while still testing the security aspects. – Tim Apr 14 '15 at 16:39
  • Thank you Tim, for all the code. I seem to get 401 when running testHelloUser#MyControllerIT.java. Can you please help me through the issue? – myspri Feb 11 '17 at 04:56
  • Is that with a clean checkout of my repository, or did you copy over parts to your own code base? I'm traveling this month, so can't access a PC to test anything.. Sorry about that! – Tim Feb 11 '17 at 14:25
  • Thanks for the quick reply. I cloned the git repo and just ran the OOB tests. I am going by your original solution which is to have an external Authorization Server at the moment. But I am very interested in your final solution as it much cleaner. Would you mind taking a look when ever you can? – myspri Feb 11 '17 at 16:56
  • @myspri you're absolutely right. Turns out the integration tests didn't run in Maven locally, nor in Travis. I'll look into why they broke and report back to you. – Tim Mar 07 '17 at 18:42
  • @myspri Switching the project back to Spring Platform Athens-SR4 fixed the integration tests; Feel free to have a look now! I'll delve into why Brussels breaks the project.. – Tim Mar 09 '17 at 09:48
  • @Tim, thank you for the solution. Sorry, I forgot to get back to you earlier, but I was able to adopt your approach and resolve the problem by adding this line to my test. SecurityContextHolder.getContext().setAuthentication(getAuthentication()); I am using spring-cloud-starter-oauth2', version: '1.1.3.RELEASE' – myspri Mar 10 '17 at 16:11
  • @myspri no problem at all; I've since been able to fix the issues with Spring Boot 1.5; The `SecurityContextHolder` line you added should not be necessary; Please check the updated GitHub repo. :) – Tim Mar 11 '17 at 14:55
  • The reason why WithMockUser (and so on) is not working is probably a missing Authorization header containing OAuth2 token => ResourceTokenService won't be triggered => OAuth security context is anonymous. I created tooling to easily add such header intercepted by a mocked TokenStore to get expected OAuth security context (see my answer below linking detailed solution there: stackoverflow.com/a/48540159/619830). By the way, @RobWinch, if someone at Spring could review, comment and maybe evaluate opportunity to contribute some of my code to the framework... ;) – ch4mp Feb 18 '19 at 14:34

9 Answers9

50

To test resource server security effectively, both with MockMvc and a RestTemplate it helps to configure an AuthorizationServer under src/test/java:

AuthorizationServer

@Configuration
@EnableAuthorizationServer
@SuppressWarnings("static-method")
class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() throws Exception {
        JwtAccessTokenConverter jwt = new JwtAccessTokenConverter();
        jwt.setSigningKey(SecurityConfig.key("rsa"));
        jwt.setVerifierKey(SecurityConfig.key("rsa.pub"));
        jwt.afterPropertiesSet();
        return jwt;
    }

    @Autowired
    private AuthenticationManager   authenticationManager;

    @Override
    public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
        .authenticationManager(authenticationManager)
        .accessTokenConverter(accessTokenConverter());
    }

    @Override
    public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
        .withClient("myclientwith")
        .authorizedGrantTypes("password")
        .authorities("myauthorities")
        .resourceIds("myresource")
        .scopes("myscope")

        .and()
        .withClient("myclientwithout")
        .authorizedGrantTypes("password")
        .authorities("myauthorities")
        .resourceIds("myresource")
        .scopes(UUID.randomUUID().toString());
    }
}

Integration test
For integration tests one can then simply use built in OAuth2 test support rule and annotions:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MyApp.class)
@WebIntegrationTest(randomPort = true)
@OAuth2ContextConfiguration(MyDetails.class)
public class MyControllerIT implements RestTemplateHolder {
    @Value("http://localhost:${local.server.port}")
    @Getter
    String                      host;

    @Getter
    @Setter
    RestOperations              restTemplate    = new TestRestTemplate();

    @Rule
    public OAuth2ContextSetup   context         = OAuth2ContextSetup.standard(this);

    @Test
    public void testHelloOAuth2WithRole() {
        ResponseEntity<String> entity = getRestTemplate().getForEntity(host + "/hello", String.class);
        assertTrue(entity.getStatusCode().is2xxSuccessful());
    }
}

class MyDetails extends ResourceOwnerPasswordResourceDetails {
    public MyDetails(final Object obj) {
        MyControllerIT it = (MyControllerIT) obj;
        setAccessTokenUri(it.getHost() + "/oauth/token");
        setClientId("myclientwith");
        setUsername("user");
        setPassword("password");
    }
}

MockMvc test
Testing with MockMvc is also possible, but needs a little helper class to get a RequestPostProcessor that sets the Authorization: Bearer <token> header on requests:

@Component
public class OAuthHelper {
    // For use with MockMvc
    public RequestPostProcessor bearerToken(final String clientid) {
        return mockRequest -> {
            OAuth2AccessToken token = createAccessToken(clientid);
            mockRequest.addHeader("Authorization", "Bearer " + token.getValue());
            return mockRequest;
        };
    }

    @Autowired
    ClientDetailsService                clientDetailsService;
    @Autowired
    AuthorizationServerTokenServices    tokenservice;

    OAuth2AccessToken createAccessToken(final String clientId) {
        // Look up authorities, resourceIds and scopes based on clientId
        ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
        Collection<GrantedAuthority> authorities = client.getAuthorities();
        Set<String> resourceIds = client.getResourceIds();
        Set<String> scopes = client.getScope();

        // Default values for other parameters
        Map<String, String> requestParameters = Collections.emptyMap();
        boolean approved = true;
        String redirectUrl = null;
        Set<String> responseTypes = Collections.emptySet();
        Map<String, Serializable> extensionProperties = Collections.emptyMap();

        // Create request
        OAuth2Request oAuth2Request = new OAuth2Request(requestParameters, clientId, authorities, approved, scopes,
                resourceIds, redirectUrl, responseTypes, extensionProperties);

        // Create OAuth2AccessToken
        User userPrincipal = new User("user", "", true, true, true, true, authorities);
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userPrincipal, null, authorities);
        OAuth2Authentication auth = new OAuth2Authentication(oAuth2Request, authenticationToken);
        return tokenservice.createAccessToken(auth);
    }
}

Your MockMvc tests must then get a RequestPostProcessor from the OauthHelper class and pass it when making requests:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MyApp.class)
@WebAppConfiguration
public class MyControllerTest {
    @Autowired
    private WebApplicationContext   webapp;

    private MockMvc                 mvc;

    @Before
    public void before() {
        mvc = MockMvcBuilders.webAppContextSetup(webapp)
                .apply(springSecurity())
                .alwaysDo(print())
                .build();
    }

    @Autowired
    private OAuthHelper helper;

    @Test
    public void testHelloWithRole() throws Exception {
        RequestPostProcessor bearerToken = helper.bearerToken("myclientwith");
        mvc.perform(get("/hello").with(bearerToken)).andExpect(status().isOk());
    }

    @Test
    public void testHelloWithoutRole() throws Exception {
        RequestPostProcessor bearerToken = helper.bearerToken("myclientwithout");
        mvc.perform(get("/hello").with(bearerToken)).andExpect(status().isForbidden());
    }
}

A full sample project is available on GitHub:
https://github.com/timtebeek/resource-server-testing

yankee
  • 38,872
  • 15
  • 103
  • 162
Tim
  • 19,793
  • 8
  • 70
  • 95
  • What is the difference between using TestingAuthenticationToken (github example) and UsernamePasswordAuthenticationToken (example posted here)? It seems that the code works with any implementation of the Authentication interface... What am I missing? – treaz Nov 05 '15 at 17:23
  • Guess you're referring to my previous use of `TestingAuthenticationToken` on GitHub: There's no real need to use either that or `UsernamePasswordAuthenticationToken`; it's just part of the changes I made in pulling details values from `ClientDetailsService`. You'd be fine using the previous version, but I use this from now on. – Tim Nov 05 '15 at 18:57
  • This works fine but the authorities in UsernamePasswordAuthenticationToken authenticationToken should be those of the user and not of the client. – alex Aug 01 '17 at 12:55
  • 2
    Also of interest to readers: http://engineering.pivotal.io/post/faking_oauth_sso/ – Tim Nov 30 '17 at 19:15
  • The "mockMvc" solution works perfectly, with some specific tweaks in our application indeed. This line `.apply(springSecurity())` is really important in order to add the right security context to the integration test. But personally, the `oAuthHelper` class was not necessary, as you could mock the security details within a mock user that has specific roles, for instance :) – Jaja May 22 '20 at 08:59
31

I found a much easier way to do this following directions I read here: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-method-withsecuritycontext. This solution is specific to testing @PreAuthorize with #oauth2.hasScope but I'm sure it could be adapted for other situations as well.

I create an annotation which can be applied to @Tests:

WithMockOAuth2Scope

import org.springframework.security.test.context.support.WithSecurityContext;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockOAuth2ScopeSecurityContextFactory.class)
public @interface WithMockOAuth2Scope {

    String scope() default "";
}

WithMockOAuth2ScopeSecurityContextFactory

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.test.context.support.WithSecurityContextFactory;

import java.util.HashSet;
import java.util.Set;

public class WithMockOAuth2ScopeSecurityContextFactory implements WithSecurityContextFactory<WithMockOAuth2Scope> {

    @Override
    public SecurityContext createSecurityContext(WithMockOAuth2Scope mockOAuth2Scope) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();

        Set<String> scope = new HashSet<>();
        scope.add(mockOAuth2Scope.scope());

        OAuth2Request request = new OAuth2Request(null, null, null, true, scope, null, null, null, null);

        Authentication auth = new OAuth2Authentication(request, null);

        context.setAuthentication(auth);

        return context;
    }
}

Example test using MockMvc:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class LoadScheduleControllerTest {

    private MockMvc mockMvc;

    @Autowired
    LoadScheduleController loadScheduleController;

    @Before
    public void setup() {
        mockMvc = MockMvcBuilders.standaloneSetup(loadScheduleController)
                    .build();
    }

    @Test
    @WithMockOAuth2Scope(scope = "dataLicense")
    public void testSchedule() throws Exception {
        mockMvc.perform(post("/schedule").contentType(MediaType.APPLICATION_JSON_UTF8).content(json)).andDo(print());
    }
}

And this is the controller under test:

@RequestMapping(value = "/schedule", method = RequestMethod.POST)
@PreAuthorize("#oauth2.hasScope('dataLicense')")
public int schedule() {
    return 0;
}
mclaassen
  • 5,018
  • 4
  • 30
  • 52
  • 2
    Interesting approach! Potentially saves me from having to setup an AuthorizationServer and getting test tokens. I'm having trouble adapting your sample to use a particular user in the `OAuth2Authentication` though.. My security model is mostly based on who you are, not the scope of your token. Any suggestions on how to adapt your sample to support that? – Tim Dec 08 '16 at 18:11
  • 1
    @Tim You should just be able to set the `Authentication` in the security context to any arbitrary Authentication object. I think the key difference here might be that you are trying to send requests using a real `OAuth2RestTemplate` whereas what I am doing in my tests is using mockMvc to send the requests. – mclaassen Dec 11 '16 at 16:33
  • 1
    Thanks! Was finally able to look at this a bit more clearly, and have updated my sample project accordingly: https://github.com/timtebeek/resource-server-testing/pull/1 Both approaches work now but serve different purposes. For username/scope based access rules I'd recommend your approach; In my case I decode the access tokens and have multi-tenant access rules based on properties therein; That really needs an actual token. :) – Tim Dec 15 '16 at 16:40
  • I was able to successfully use a fake a complete UserDetails and proceed with my tests. – Anand Rockzz Feb 19 '17 at 22:14
  • I was trying to bypass a @PreAuthorize with oauth2.hasScope in my Integration Test and this save me. Thank you! – Dherik Dec 29 '17 at 10:41
  • 2
    If someone are interested in mock the token value, you can set the `details` in the `OAuth2AuthenticationDetails` and pass a `httpServletrequest` with attributes `OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE` with "Bearer" and `OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE` with the token value. After that, you can access the token value in you app with `((OAuth2AuthenticationDetails) SecurityContextHolder.getContext().getAuthentication().getDetails()).getTokenValue()` – Dherik Dec 29 '17 at 16:39
  • @mclaassen i used **.access("#oauth2.hasScope('backend')");** in my resource server config, but it threw exceptions: org.springframework.security.access.AccessDeniedException: Insufficient scope for this resource – Hantsy Jun 26 '18 at 06:26
  • I had to [change stateless `false`](https://github.com/spring-projects/spring-security-oauth/issues/385#issuecomment-474492098). – DEWA Kazuyuki - 出羽和之 Sep 17 '19 at 04:55
14

Spring Boot 1.5 introduced test slices like @WebMvcTest. Using these test slices and manually load the OAuth2AutoConfiguration gives your tests less boilerplate and they'll run faster than the proposed @SpringBootTest based solutions. If you also import your production security configuration, you can test that the configured filter chains is working for your web services.

Here's the setup along with some additional classes that you'll probably find beneficial:

Controller:

@RestController
@RequestMapping(BookingController.API_URL)
public class BookingController {

    public static final String API_URL = "/v1/booking";

    @Autowired
    private BookingRepository bookingRepository;

    @PreAuthorize("#oauth2.hasScope('myapi:write')")
    @PatchMapping(consumes = APPLICATION_JSON_UTF8_VALUE, produces = APPLICATION_JSON_UTF8_VALUE)
    public Booking patchBooking(OAuth2Authentication authentication, @RequestBody @Valid Booking booking) {
        String subjectId = MyOAuth2Helper.subjectId(authentication);
        booking.setSubjectId(subjectId);
        return bookingRepository.save(booking);
    }
}

Test:

@RunWith(SpringRunner.class)
@AutoConfigureJsonTesters
@WebMvcTest
@Import(DefaultTestConfiguration.class)
public class BookingControllerTest {

    @Autowired
    private MockMvc mvc;

    @Autowired
    private JacksonTester<Booking> json;

    @MockBean
    private BookingRepository bookingRepository;

    @MockBean
    public ResourceServerTokenServices resourceServerTokenServices;

    @Before
    public void setUp() throws Exception {
        // Stub the remote call that loads the authentication object
        when(resourceServerTokenServices.loadAuthentication(anyString())).thenAnswer(invocation -> SecurityContextHolder.getContext().getAuthentication());
    }

    @Test
    @WithOAuthSubject(scopes = {"myapi:read", "myapi:write"})
    public void mustHaveValidBookingForPatch() throws Exception {
        mvc.perform(patch(API_URL)
            .header(AUTHORIZATION, "Bearer foo")
            .content(json.write(new Booking("myguid", "aes")).getJson())
            .contentType(MediaType.APPLICATION_JSON_UTF8)
        ).andExpect(status().is2xxSuccessful());
    }
}

DefaultTestConfiguration:

@TestConfiguration
@Import({MySecurityConfig.class, OAuth2AutoConfiguration.class})
public class DefaultTestConfiguration {

}

MySecurityConfig (this is for production):

@Configuration
@EnableOAuth2Client
@EnableResourceServer
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/v1/**").authenticated();
    }

}

Custom annotation for injecting scopes from tests:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithOAuthSubjectSecurityContextFactory.class)
public @interface WithOAuthSubject {

    String[] scopes() default {"myapi:write", "myapi:read"};

    String subjectId() default "a1de7cc9-1b3a-4ecd-96fa-dab6059ccf6f";

}

Factory class for handling the custom annotation:

public class WithOAuthSubjectSecurityContextFactory implements WithSecurityContextFactory<WithOAuthSubject> {

    private DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter();

    @Override
    public SecurityContext createSecurityContext(WithOAuthSubject withOAuthSubject) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();

        // Copy of response from https://myidentityserver.com/identity/connect/accesstokenvalidation
        Map<String, ?> remoteToken = ImmutableMap.<String, Object>builder()
            .put("iss", "https://myfakeidentity.example.com/identity")
            .put("aud", "oauth2-resource")
            .put("exp", OffsetDateTime.now().plusDays(1L).toEpochSecond() + "")
            .put("nbf", OffsetDateTime.now().plusDays(1L).toEpochSecond() + "")
            .put("client_id", "my-client-id")
            .put("scope", Arrays.asList(withOAuthSubject.scopes()))
            .put("sub", withOAuthSubject.subjectId())
            .put("auth_time", OffsetDateTime.now().toEpochSecond() + "")
            .put("idp", "idsrv")
            .put("amr", "password")
            .build();

        OAuth2Authentication authentication = defaultAccessTokenConverter.extractAuthentication(remoteToken);
        context.setAuthentication(authentication);
        return context;
    }
}

I use a copy of the response from our identity server for creating a realistic OAuth2Authentication. You can probably just copy my code. If you want to repeat the process for your identity server, place a breakpoint in org.springframework.security.oauth2.provider.token.RemoteTokenServices#loadAuthentication or org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices#extractAuthentication, depending on whether you have configured a custom ResourceServerTokenServices or not.

Edwin Diaz
  • 1,693
  • 1
  • 12
  • 16
gogstad
  • 3,607
  • 1
  • 29
  • 32
  • Wow thanks for putting the effort in to come up with a completely new way to test this, that's as you say quite possibly faster and doesn't setup unneeded parts of the application context. Very cool! :) – Tim Apr 29 '17 at 07:14
  • I had tried your solution, but forgot to add the Authentication header while building test requests and off course, it didn't work :/. Maybe could put a little more emphasis on the necessity to add this Authorization header to each and every request involving security ? – ch4mp Feb 01 '18 at 17:10
7

I have another solution for this. See below:

@RunWith(SpringRunner.class)
@SpringBootTest
@WebAppConfiguration
@ActiveProfiles("test")
public class AccountContollerTest {

    public static Logger log = LoggerFactory.getLogger(AccountContollerTest.class);

    @Autowired
    private WebApplicationContext webApplicationContext;

    private MockMvc mvc;

    @Autowired
    private FilterChainProxy springSecurityFilterChain;

    @Autowired
    private UserRepository users;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private CustomClientDetailsService clientDetialsService;

    @Before
    public void setUp() {
         mvc = MockMvcBuilders
                 .webAppContextSetup(webApplicationContext)
                 .apply(springSecurity(springSecurityFilterChain))
                 .build();

         BaseClientDetails testClient = new ClientBuilder("testclient")
                    .secret("testclientsecret")
                    .authorizedGrantTypes("password")
                    .scopes("read", "write")
                    .autoApprove(true)
                    .build();

         clientDetialsService.addClient(testClient);

         User user = createDefaultUser("testuser", passwordEncoder.encode("testpassword"), "max", "Mustermann", new Email("myemail@test.de"));

         users.deleteAll();
         users.save(user);

    }

    @Test
    public void shouldRetriveAccountDetailsWithValidAccessToken() throws Exception {
        mvc.perform(get("/api/me")
                .header("Authorization", "Bearer " + validAccessToken())
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andDo(print())
                .andExpect(jsonPath("$.userAuthentication.name").value("testuser"))
                .andExpect(jsonPath("$.authorities[0].authority").value("ROLE_USER"));
    }

    @Test
    public void shouldReciveHTTPStatusUnauthenticatedWithoutAuthorizationHeader() throws Exception{
        mvc.perform(get("/api/me")
                .accept(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isUnauthorized());
    }

    private String validAccessToken() throws Exception {  
        String username = "testuser";
        String password = "testpassword";

        MockHttpServletResponse response = mvc
            .perform(post("/oauth/token")
                    .header("Authorization", "Basic "
                           + new String(Base64Utils.encode(("testclient:testclientsecret")
                            .getBytes())))
                    .param("username", username)
                    .param("password", password)
                    .param("grant_type", "password"))
            .andDo(print())
            .andReturn().getResponse();

    return new ObjectMapper()
            .readValue(response.getContentAsByteArray(), OAuthToken.class)
            .accessToken;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    private static class OAuthToken {
        @JsonProperty("access_token")
        public String accessToken;
    }
}

Hope it will help!

Jaumzera
  • 2,305
  • 1
  • 30
  • 44
Rocks360
  • 358
  • 1
  • 4
  • 14
6

There is alternative approach which I believe to be cleaner and more meaningful.

The approach is to autowire the token store and then add a test token which can then be used by the rest client.

An example test:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class UserControllerIT {

    @Autowired
    private TestRestTemplate testRestTemplate;

    @Autowired
    private TokenStore tokenStore;

    @Before
    public void setUp() {

        final OAuth2AccessToken token = new DefaultOAuth2AccessToken("FOO");
        final ClientDetails client = new BaseClientDetails("client", null, "read", "client_credentials", "ROLE_CLIENT");
        final OAuth2Authentication authentication = new OAuth2Authentication(
                new TokenRequest(null, "client", null, "client_credentials").createOAuth2Request(client), null);

        tokenStore.storeAccessToken(token, authentication);

    }

    @Test
    public void testGivenPathUsersWhenGettingForEntityThenStatusCodeIsOk() {

        final HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.AUTHORIZATION, "Bearer FOO");
        headers.setContentType(MediaType.APPLICATION_JSON);

        // Given Path Users
        final UriComponentsBuilder uri = UriComponentsBuilder.fromPath("/api/users");

        // When Getting For Entity
        final ResponseEntity<String> response = testRestTemplate.exchange(uri.build().toUri(), HttpMethod.GET,
                new HttpEntity<>(headers), String.class);

        // Then Status Code Is Ok
        assertThat(response.getStatusCode(), is(HttpStatus.OK));
    }

}

Personally I believe that it is not appropriate to unit test a controller with security enabled since security is a separate layer to the controller. I would create an integration test that tests all of the layers together. However the above approach can easily be modified to create a Unit Test with that uses MockMvc.

The above code is inspired by a Spring Security test written by Dave Syer.

Note this approach is for resource servers that share the same token store as the authorisation server. If your resource server does not share the same token store as the authorisation server I recommend using wiremock to mock the http responses.

4

OK, I've not yet been able to test my standalone oauth2 JWT token protected resource-server using the new @WithMockUser or related annotations.

As a workaround, I have been able to integration test my resource server security by setting up a permissive AuthorizationServer under src/test/java, and having that define two clients I use through a helper class. This gets me some of the way there, but it's not yet as easy as I'd like to test various users, roles, scopes, etc.

I'm guessing from here on it should be easier to implement my own WithSecurityContextFactory that creates an OAuth2Authentication, instead of the usual UsernamePasswordAuthentication. However, I have not yet been able to work out the detail of how to easily set this up. Any comments or suggestions how to set this up are welcome.

Tim
  • 19,793
  • 8
  • 70
  • 95
4

I found an easy and rapid way for testing spring security resource server with any token store. Im my example @EnabledResourceServeruses jwt token store.

The magic here is I replaced JwtTokenStore with InMemoryTokenStore at integration test.

@RunWith (SpringRunner.class)
@SpringBootTest (classes = {Application.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles ("test")
@TestPropertySource (locations = "classpath:application.yml")
@Transactional
public class ResourceServerIntegrationTest {

@Autowired
private TokenStore tokenStore;

@Autowired
private ObjectMapper jacksonObjectMapper;

@LocalServerPort
int port;

@Configuration
protected static class PrepareTokenStore {

    @Bean
    @Primary
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }

}

private OAuth2AccessToken token;
private OAuth2Authentication authentication;

@Before
public void init() {

    RestAssured.port = port;

    token = new DefaultOAuth2AccessToken("FOO");
    ClientDetails client = new BaseClientDetails("client", null, "read", "client_credentials", "ROLE_READER,ROLE_CLIENT");

    // Authorities
    List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
    authorities.add(new SimpleGrantedAuthority("ROLE_READER"));
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken("writer", "writer", authorities);

    authentication = new OAuth2Authentication(new TokenRequest(null, "client", null, "client_credentials").createOAuth2Request(client), authenticationToken);
    tokenStore.storeAccessToken(token, authentication);

}

@Test
public void gbsUserController_findById() throws Exception {

    RestAssured.given().log().all().when().headers("Authorization", "Bearer FOO").get("/gbsusers/{id}", 2L).then().log().all().statusCode(HttpStatus.OK.value());

}
talipkorkmaz
  • 332
  • 2
  • 7
1

One more solution I tried to detail enough :-D

It is based on setting an Authorization header, like some above, but I wanted:

  • Not to create actually valid JWT tokens and using all JWT authentication stack (unit tests...)
  • Test authentication to contain test-case defined scopes and authorities

So I've:

  • created custom annotations to set up a per-test OAuth2Authentication: @WithMockOAuth2Client (direct client connection) & @WithMockOAuth2User (client acting on behalf of an end user => includes both my custom @WithMockOAuth2Client and Spring @WithMockUser)
  • @MockBean the TokenStore to return the OAuth2Authentication configured with above custom annotations
  • provide MockHttpServletRequestBuilder factories that set a specific Authorization header intercepted by TokenStore mock to inject expected authentication.

The result to get you tested:

@WebMvcTest(MyController.class) // Controller to unit-test
@Import(WebSecurityConfig.class) // your class extending WebSecurityConfigurerAdapter
public class MyControllerTest extends OAuth2ControllerTest {

    @Test
    public void testWithUnauthenticatedClient() throws Exception {
        api.post(payload, "/endpoint")
                .andExpect(...);
    }

    @Test
    @WithMockOAuth2Client
    public void testWithDefaultClient() throws Exception {
        api.get("/endpoint")
                .andExpect(...);
    }

    @Test
    @WithMockOAuth2User
    public void testWithDefaultClientOnBehalfDefaultUser() throws Exception {
            MockHttpServletRequestBuilder req = api.postRequestBuilder(null, "/uaa/refresh")
                .header("refresh_token", JWT_REFRESH_TOKEN);

        api.perform(req)
                .andExpect(status().isOk())
                .andExpect(...)
    }

    @Test
    @WithMockOAuth2User(
        client = @WithMockOAuth2Client(
                clientId = "custom-client",
                scope = {"custom-scope", "other-scope"},
                authorities = {"custom-authority", "ROLE_CUSTOM_CLIENT"}),
        user = @WithMockUser(
                username = "custom-username",
                authorities = {"custom-user-authority"}))
    public void testWithCustomClientOnBehalfCustomUser() throws Exception {
        api.get(MediaType.APPLICATION_ATOM_XML, "/endpoint")
                .andExpect(status().isOk())
                .andExpect(xpath(...));
    }
}
ch4mp
  • 6,622
  • 6
  • 29
  • 49
  • 1
    Reading the all stack again, I just realize how close to my solution, [this one](https://stackoverflow.com/a/43676742/619830) is. I had tried, missing the line where the header is set, and built my own solution from ground up. In the end, I just push a little further OAuth2Authentication configuration options and add wrappers never to forget this bloody header. – ch4mp Feb 01 '18 at 17:16
0

I've tried many ways. But my solution is easier than others. I'm using OAuth2 JWT authentication in my spring boot application. My goal is to do a contract test. I'm writing a script with groovy and the contract plugin generates test codes for me. Therefore, I cannot interfere with the codes. I have a simple BaseTest class. I need to do all the necessary configurations in this class. This solution worked for me.

Imported dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-verifier</artifactId>
    <version>2.1.1.RELEASE</version>
    <scope>test</scope>
</dependency>

Imported Plugins:

    <plugin>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-contract-maven-plugin</artifactId>
        <version>2.1.1.RELEASE</version>
        <extensions>true</extensions>
        <configuration>
            <baseClassForTests>com.test.services.BaseTestClass
            </baseClassForTests>
        </configuration>
    </plugin>

BaseTestClass.java

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@DirtiesContext
@AutoConfigureMessageVerifier
@ContextConfiguration
@WithMockUser(username = "admin", roles = {"USER", "ADMIN"})
public class BaseTestClass {

    @Autowired
    private MyController myController;

    @Autowired
    private WebApplicationContext webApplicationContext;

    @Before
    public void setup() {
        StandaloneMockMvcBuilder standaloneMockMvcBuilder = MockMvcBuilders.standaloneSetup(myController);
        RestAssuredMockMvc.standaloneSetup(standaloneMockMvcBuilder);
        RestAssuredMockMvc.webAppContextSetup(webApplicationContext);
    }

}

myFirstScenario.groovy (package:"/test/resources/contracts"):

import org.springframework.cloud.contract.spec.Contract

Contract.make {
    description "should return ok"
    request {
        method GET()
        url("/api/contract/test") {
            headers {
                header("Authorization","Bearer FOO")
            }
        }
    }
    response {
        status 200
    }
}

MyController.java:

@RestController
@RequestMapping(value = "/api/contract")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public class MyController {
...
}

if you want to test for non-admin users you can use:

@WithMockUser(username = "admin", roles = {"USER"})
Bilal Demir
  • 587
  • 6
  • 17