with secure mkyong example company autoconfigure authorities and spring-security spring-security-oauth2

spring security - secure - ¿Cómo probar la seguridad del servidor de recursos spring-security-oauth2?



spring security oauth2 example mkyong (6)

Tras el lanzamiento de Spring Security 4 y su compatibilidad mejorada para las pruebas , he querido actualizar mis pruebas actuales de seguridad de servidor oauth2 de recursos de recursos.

Actualmente tengo una clase auxiliar que configura una OAuth2RestTemplate utilizando ResourceOwnerPasswordResourceDetails con una prueba ClientId conecta a un AccessTokenUri real para solicitar un token válido para mis pruebas. Este resttemplate se usa para realizar solicitudes en mis @WebIntegrationTest s.

Me gustaría eliminar la dependencia en el servidor de autorización real y el uso de credenciales de usuario válidas (si son limitadas) en mis pruebas, aprovechando el nuevo soporte de prueba en Spring Security 4.

Hasta ahora, todos mis intentos de utilizar @WithMockUser , @WithSecurityContext , SecurityMockMvcConfigurers.springSecurity() y SecurityMockMvcRequestPostProcessors.* han podido realizar llamadas autenticadas a través de MockMvc , y no puedo encontrar ejemplos de este tipo en los proyectos de ejemplo de Spring.

¿Alguien puede ayudarme a probar mi servidor de recursos oauth2 con algún tipo de credenciales falsas, mientras sigo probando las restricciones de seguridad impuestas?

** EDITAR ** Código de muestra disponible aquí: https://github.com/timtebeek/resource-server-testing Para cada una de las clases de prueba, entiendo por qué no funcionará como tal, pero estoy buscando formas de que me permitiría probar la configuración de seguridad fácilmente.

Ahora estoy pensando en crear un OAuthServer muy permisivo bajo src/test/java , lo que podría ayudar un poco. ¿Alguien tiene alguna otra sugerencia?


De acuerdo, todavía no he podido probar mi servidor de recursos protegido con tokens JWT oauth2 autónomo utilizando el nuevo @WithMockUser o anotaciones relacionadas.

Como solución, he podido probar la integración de la seguridad de mi servidor de recursos configurando un AuthorizationServer permisivo en src / test / java , y tengo que definir dos clientes que uso a través de una clase auxiliar . Esto me lleva algo de camino hasta allí, pero aún no es tan fácil como me gustaría probar varios usuarios, roles, ámbitos, etc.

Supongo que a partir de ahora debería ser más fácil implementar mi propia WithSecurityContextFactory que crea una OAuth2Authentication , en lugar de la habitual UsernamePasswordAuthentication . Sin embargo, todavía no he podido averiguar los detalles de cómo configurar esto fácilmente. Cualquier comentario o sugerencia de cómo configurar esto son bienvenidos.


Para probar la seguridad del servidor de recursos de manera efectiva, tanto con MockMvc como con MockMvc ayuda a configurar un AuthorizationServer bajo 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()); } }

Examen de integración
Para las pruebas de integración, uno puede simplemente usar la regla y las anotaciones de soporte de prueba integradas de OAuth2:

@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"); } }

Prueba de MockMvc
MockMvc es posible realizar pruebas con MockMvc , pero necesita una pequeña clase de ayuda para obtener un RequestPostProcessor que establezca el encabezado Authorization: Bearer <token> en las solicitudes:

@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); } }

Sus pruebas de MockMvc deben obtener un RequestPostProcessor de la clase OauthHelper y pasarlo al realizar las solicitudes:

@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 proyecto de muestra completo está disponible en GitHub:
https://github.com/timtebeek/resource-server-testing


Encontré una manera mucho más fácil de hacer esto siguiendo las instrucciones que leí aquí: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-method-withsecuritycontext . Esta solución es específica para probar @PreAuthorize con #oauth2.hasScope pero estoy seguro de que también podría adaptarse para otras situaciones.

@Test una anotación que se puede aplicar a @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; } }

Ejemplo de prueba usando 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()); } }

Y este es el controlador bajo prueba:

@RequestMapping(value = "/schedule", method = RequestMethod.POST) @PreAuthorize("#oauth2.hasScope(''dataLicense'')") public int schedule() { return 0; }


Spring Boot 1.5 introdujo cortes de prueba como @WebMvcTest . El uso de estas secciones de prueba y la carga manual de OAuth2AutoConfiguration le da a sus pruebas menos texto estándar y se ejecutarán más rápido que las soluciones basadas en @SpringBootTest propuestas. Si también importa su configuración de seguridad de producción, puede probar que las cadenas de filtro configuradas funcionan para sus servicios web.

Aquí está la configuración junto con algunas clases adicionales que probablemente encuentres beneficiosas:

Controlador :

@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); } }

Prueba :

@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 (esto es para producción):

@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(); } }

Anotación personalizada para inyectar ámbitos de pruebas :

@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"; }

Clase de fábrica para manejar la anotación personalizada :

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; } }

Utilizo una copia de la respuesta de nuestro servidor de identidad para crear una OAuth2Authentication realista. Probablemente puedas simplemente copiar mi código. Si desea repetir el proceso para su servidor de identidad, coloque un punto de interrupción en org.springframework.security.oauth2.provider.token.RemoteTokenServices#loadAuthentication u org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices#extractAuthentication , dependiendo de si ha configurado un ResourceServerTokenServices personalizado o no.


Hay un enfoque alternativo que creo que es más limpio y más significativo.

El enfoque es autoaumentar la tienda de tokens y luego agregar un token de prueba que luego puede ser utilizado por el resto del cliente.

Una prueba de ejemplo :

@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)); } }

Personalmente, creo que no es apropiado probar la unidad de un controlador con seguridad habilitada ya que la seguridad es una capa separada del controlador. Crearía una prueba de integración que pruebe todas las capas juntas. Sin embargo, el enfoque anterior se puede modificar fácilmente para crear una prueba unitaria que utilice MockMvc.

El código anterior está inspirado en una prueba de seguridad de primavera escrita por Dave Syer.

Tenga en cuenta que este enfoque es para los servidores de recursos que comparten el mismo almacén de claves como el servidor de autorizaciones. Si su servidor de recursos no comparte el mismo almacén de fichas que el servidor de autorizaciones, le recomiendo usar wiremock para simular las respuestas http .


Tengo otra solución para esto. Vea abajo.

@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("[email protected]")); 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; } }

Espero que ayude!