Reputation: 65
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?
Upvotes: 2
Views: 4602
Reputation: 2850
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.
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();
}
}
@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;
}
}
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 !
Upvotes: 4