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!