Environnement : J'ai une application d'architecture de microservice basée sur Spring Boot composée de plusieurs services d'infrastructure et de services de ressources (contenant la logique métier). L'autorisation et l'authentification sont gérées par un service oAuth2 qui gère les entités utilisateur et crée des jetons JWT pour les clients.
Pour tester une seule application de microservice dans son intégralité, j'ai essayé de créer des tests avec testNG, spring.boot.test, org.springframework.security .test ...
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, properties = {"spring.cloud.discovery.enabled=false", "spring.cloud.config.enabled=false", "spring.profiles.active=test"})
@AutoConfigureMockMvc
@Test
public class ArtistControllerTest extends AbstractTestNGSpringContextTests {
@Autowired
private MockMvc mvc;
@BeforeClass
@Transactional
public void setUp() {
// nothing to do
}
@AfterClass
@Transactional
public void tearDown() {
// nothing to do here
}
@Test
@WithMockUser(authorities = {"READ", "WRITE"})
public void getAllTest() throws Exception {
// EXPECT HTTP STATUS 200
// BUT GET 401
this.mvc.perform(get("/")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
}
}
où la configuration de sécurité (serveur de ressources) est la suivante
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
// get the configured token store
@Autowired
TokenStore tokenStore;
// get the configured token converter
@Autowired
JwtAccessTokenConverter tokenConverter;
/**
* !!! configuration of springs http security !!!
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/**").authenticated();
}
/**
* configuration of springs resource server security
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
// set the configured tokenStore to this resourceServer
resources.resourceId("artist").tokenStore(tokenStore);
}
}
et le contrôle de sécurité basé sur la méthode suivant annoté à l'intérieur de la classe de contrôleur
@PreAuthorize("hasAuthority('READ')")
@RequestMapping(value = "/", method = RequestMethod.GET)
public List<Foo> getAll(Principal user) {
List<Foo> foos = fooRepository.findAll();
return foos;
}
Je pensais que cela fonctionnerait, mais lors de l'exécution du test, je n'obtiens qu'une erreur d'assertion
Java.lang.AssertionError: Status
Expected :200
Actual :401
Question : Y a-t-il quelque chose de totalement évident que je me trompe? Ou est-ce que @WithMockUser ne fonctionnera pas avec @SpringBootTest et @AutoConfigureMockMvc dans un environnement oAuth2? Si tel est le cas ... quelle serait la meilleure approche pour tester les configurations de sécurité basées sur la route et la méthode dans le cadre d'un tel test (d'intégration) comme celui-ci?
Annexe : J'ai également essayé différentes approches comme quelque chose comme ce qui suit ... mais cela a conduit au même résultat :(
this.mvc.perform(get("/")
.with(user("admin").roles("READ","WRITE").authorities(() -> "READ", () -> "WRITE"))
.accept(MediaType.APPLICATION_JSON))
@WithMockUser
Crée l'authentification dans SecurityContext. Il en va de même pour with(user("username"))
.
Par défaut, OAuth2AuthenticationProcessingFilter n'utilise pas SecurityContext, mais crée toujours l'authentification à partir du jeton ("sans état").
Vous pouvez facilement modifier ce comportement en définissant l'indicateur sans état dans la configuration de sécurité du serveur de ressources sur false:
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration implements ResourceServerConfigurer {
@Override
public void configure(ResourceServerSecurityConfigurer security) throws Exception {
security.stateless(false);
}
@Override
public void configure(HttpSecurity http) {}
}
Une autre option consiste à étendre ResourceServerConfigurerAdapter, mais le problème avec cela est qu'il est livré avec une configuration qui force toutes les demandes à être authentifiées. L'implémentation de l'interface laisse votre configuration de sécurité principale inchangée, à l'exception de l'apatridie.
Bien sûr, définissez le drapeau sur false uniquement dans vos contextes de test.
J'ai eu le même problème, et la seule façon que j'ai trouvée était de créer un jeton et de l'utiliser dans le mockMvc
mockMvc.perform(get("/resource")
.with(oAuthHelper.bearerToken("test"))
Et l'OAuthHelper:
@Component
@EnableAuthorizationServer
public class OAuthHelper extends AuthorizationServerConfigurerAdapter {
@Autowired
AuthorizationServerTokenServices tokenservice;
@Autowired
ClientDetailsService clientDetailsService;
public RequestPostProcessor bearerToken(final String clientid) {
return mockRequest -> {
OAuth2AccessToken token = createAccessToken(clientid);
mockRequest.addHeader("Authorization", "Bearer " + token.getValue());
return mockRequest;
};
}
OAuth2AccessToken createAccessToken(final String clientId) {
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
Collection<GrantedAuthority> authorities = client.getAuthorities();
Set<String> resourceIds = client.getResourceIds();
Set<String> scopes = client.getScope();
Map<String, String> requestParameters = Collections.emptyMap();
boolean approved = true;
String redirectUrl = null;
Set<String> responseTypes = Collections.emptySet();
Map<String, Serializable> extensionProperties = Collections.emptyMap();
OAuth2Request oAuth2Request = new OAuth2Request(requestParameters, clientId, authorities,
approved, scopes, resourceIds, redirectUrl, responseTypes, extensionProperties);
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);
}
@Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("test")
.authorities("READ");
}
}
Comme j'essayais spécifiquement d'écrire des tests par rapport à notre ResourceServerConfiguration, j'ai contourné le problème en lui créant un wrapper de test qui définissait security.stateless sur false:
@Configuration
@EnableResourceServer
public class ResourceServerTestConfiguration extends ResourceServerConfigurerAdapter {
private ResourceServerConfiguration configuration;
public ResourceServerTestConfiguration(ResourceServerConfiguration configuration) {
this.configuration = configuration;
}
@Override
public void configure(ResourceServerSecurityConfigurer security) throws Exception {
configuration.configure(security);
security.stateless(false);
}
@Override
public void configure(HttpSecurity http) throws Exception {
configuration.configure(http);
}
}