Comment tester printemps-sécurité-oauth2 de ressources du serveur de sécurité?
après la sortie de Spring Security 4 et c'est support amélioré pour les tests j'ai voulu mettre à jour mes tests actuels du serveur de ressources OAuth2 de sécurité du printemps.
a l'heure actuelle, j'ai une classe de helper qui met en place un OAuth2RestTemplate
en utilisant ResourceOwnerPasswordResourceDetails
avec un test ClientId
connexion à une réelle AccessTokenUri
pour demander un token valide pour mes tests. Ce resttemplate est alors utilisé pour faire des requêtes dans mon @WebIntegrationTest
S.
je voudrais laisser tomber la dépendance sur le le serveur D'autorisations réel, et l'utilisation des justificatifs d'utilisateur valides (si limités) dans mes tests, en profitant du nouveau support de test dans la sécurité de printemps 4.
Jusqu'à présent toutes mes tentatives d'utiliser @WithMockUser
,@WithSecurityContext
,SecurityMockMvcConfigurers.springSecurity()
& SecurityMockMvcRequestPostProcessors.*
n'ont pas réussi à faire des appels authentifiés par MockMvc
, et je ne peux pas trouver de tels exemples dans le Ressort d'exemples de projets.
est-ce que quelqu'un peut m'aider à tester mon serveur de ressources oauth2 avec une sorte de moquerie de justificatifs d'identité, tout en testant les restrictions de sécurité imposées?
***EDIT ** Exemple de code disponible ici:https://github.com/timtebeek/resource-server-testing Pour chacune des classes de test, je comprends pourquoi ça ne fonctionne pas comme ça, mais je cherche des moyens qui me permettraient de tester la configuration de sécurité facilement.
je pense maintenant à créer un OAuthServer très permissif sous src/test/java
, ce qui pourrait aider un peu. Quelqu'un a une autre des suggestions?
8 réponses
pour tester efficacement la sécurité du serveur de ressources, les deux avec MockMvc
et RestTemplate
il aide à configurer un AuthorizationServer
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());
}
}
test D'intégration
Pour les tests d'intégration, il est alors possible d'utiliser simplement la règle de support de test intégrée dans OAuth2 test et les irritations:
@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");
}
}
test MockMvc
Test avec MockMvc
est également possible, mais besoin d'un peu de classe d'assistance pour obtenir un RequestPostProcessor
qui définit l' Authorization: Bearer <token>
l'en-tête de demande:
@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);
}
}
MockMvc
les tests doivent alors obtenir un RequestPostProcessor
OauthHelper
classe et de transmettre les requêtes:
@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());
}
}
Un échantillon complet du projet est disponible sur GitHub:
https://github.com/timtebeek/resource-server-testing
j'ai trouvé un moyen beaucoup plus facile de faire ce qui suit directions que j'ai lu ici: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-method-withsecuritycontext. Cette solution est spécifique aux tests @PreAuthorize
#oauth2.hasScope
mais je suis sûr qu'il pourrait être adapté à d'autres situations.
je crée une annotation qui peut être appliquée à @Test
s:
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;
}
}
Exemple de test à l'aide de 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());
}
}
Et c'est le contrôleur en vertu de test:
@RequestMapping(value = "/schedule", method = RequestMethod.POST)
@PreAuthorize("#oauth2.hasScope('dataLicense')")
public int schedule() {
return 0;
}
Tring Boot 1.5 introduced tester les tranches@WebMvcTest
. En utilisant ces tranches d'essai et de charger manuellement le OAuth2AutoConfiguration
donne à vos tests moins de boilerplate et ils seront plus rapides que ceux proposés @SpringBootTest
solutions basées. Si vous importez également votre configuration de sécurité de production, vous pouvez tester que les chaînes de filtrage configurées fonctionnent pour vos services web.
Voici la configuration avec quelques classes supplémentaires que vous trouverez probablement bénéfique:
contrôleur:
@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 (c'est pour la 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();
}
}
annotation personnalisée pour l'injection des scopes des 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";
}
Classe D'usine pour la manipulation de la coutume 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;
}
}
j'utilise une copie de la réponse de notre serveur d'identité pour la création d'un réaliste OAuth2Authentication
. Tu peux probablement copier mon code. Si vous voulez répéter le processus pour votre serveur d'identité, placez un point d'arrêt dans org.springframework.security.oauth2.provider.token.RemoteTokenServices#loadAuthentication
ou org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices#extractAuthentication
, selon que vous avez configuré un custom ResourceServerTokenServices
ou pas.
OK, je n'ai pas encore été capable de tester mon serveur ressource protégé par JWT autonome oauth2 JWT en utilisant le nouveau @WithMockUser
ou les annotations.
comme solution de contournement, j'ai pu tester la sécurité de mon serveur de ressources en mettant en place permissive AuthorizationServer
sous src/test / java, et avoir qui définissent deux clients, j'utilise à travers une classe helper. Cela me fait un peu de chemin là-bas, mais ce n'est pas encore aussi facile que je voudrais tester divers utilisateurs, rôles, portées, etc.
je suppose qu'à partir de maintenant, il devrait être plus facile de mettre en œuvre le mien WithSecurityContextFactory
qui crée un OAuth2Authentication
, au lieu desUsernamePasswordAuthentication
. Toutefois, je n'ai pas encore été en mesure d'élaborer les détails de la façon de mettre en place facilement ce système. Nous vous invitons à nous faire part de vos commentaires ou suggestions sur la façon de procéder.
j'ai une autre solution pour ce. Voir ci-dessous.
@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
FilterChainProxy springSecurityFilterChain;
@Autowired
UserRepository users;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
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", "wirte")
.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;
}
}
j'Espère que ça aidera!
j'ai trouvé un moyen facile et rapide pour tester le serveur de ressources de sécurité de printemps avec n'importe quel magasin de token. Im mon exemple @EnabledResourceServer
utilise jwt jeton magasin.
La magie est ici que j'ai remplacé JwtTokenStore
InMemoryTokenStore
au test d'intégration.
@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());
}
Il y a une approche alternative que je crois plus propre et plus significative.
l'approche est d'autowire le token store puis d'ajouter un token test qui peut ensuite être utilisé par le client rest.
un exemple de 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));
}
}
personnellement, je crois qu'il n'est pas approprié de tester un contrôleur unitaire avec la sécurité activée puisque la sécurité est un calque séparé du contrôleur. Je créerais un test d'intégration qui teste tout les couches ensemble. Toutefois, l'approche ci-dessus peut facilement être modifié pour créer un test unitaire qui utilise MockMvc.
Le code ci-dessus est inspiré par un test de sécurité du ressort écrit par Dave Syer.
Remarque: cette approche est pour les serveurs de ressources qui partagent le même magasin que le serveur d'autorisation. Si votre serveur de ressources ne partage pas le même magasin que le serveur d'autorisation je recommande utiliser wiremock pour se moquer du http réponses.
une solution de plus que j'ai essayé de détailler assez: - D
Elle est basée sur l'établissement d'une en-tête d'Autorisation, à l'instar de certains ci-dessus, mais je voulais:
- ne pas créer de tokens JWT réellement valides et en utilisant toute la pile d'authentification JWT (tests unitaires...)
- authentification de Test pour contenir des portées et des autorités définies par cas de test
j'ai Donc:
- créé une annotation personnalisée avec
@WithSecurityContext
et de l'usine, comme quelques autres n'a configurer un par testOAuth2Authentication
(@WithJwt
dans mon cas) - @MockBean la TokenStore pour retourner le OAuth2Authentication configuré avec
@WithJwt
- donner
MockHttpServletRequestBuilder
usines qui mettent un en-tête D'autorisation spécifique intercepté par TokenStore mock pour injecter l'authentification attendue.
P.S.
n'hésitez pas à accepter / voter ma solution si elle correspond le mieux à vos besoins ;-)