What's the best usage for mockOIdcLogin() for WebTestClient? - spring-boot

I'm currently working on a senior project and we decided to use Spring Webflux for our backend and Google as our OAuth2.0 provider. I am currently trying to run some integration tests using Spock and Groovy on some endpoints that are secured behind OAuth2.0 authentication. The endpoint does not use the Authentication principal for anything, is just not supposed to be accessed by someone who isn't authenticated. However, reading the Spring documentation and I came across the method for a webTestClient to use a mock open id connect login in which I might not need to do mock the entire OAuth2 process, however, this is giving me a HTTP 302 status
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, properties = "spring.main.web-application-type=reactive")
class UserControllerITSpec extends Specification {
#Autowired
ReactiveWebApplicationContext context
#Autowired
ApplicationContext applicationContext
#Autowired
WebTestClient client
#Rule
public WireMockRule wireMockRule = new WireMockRule(wireMockConfig()
.port(8077))
def setup() {
client = WebTestClient.bindToApplicationContext(context)
.configureClient()
.build()
}
def "getAllUsers is successful"() {
given: "a request"
def request = client.mutateWith(mockOidcLogin()).get().uri("/api/v2/users/getAllUsers")
stubFor(post("/graphql/")
.withHeader("Authorization", equalTo("F9v4MUqdQuWAh3Wqxe11mteqPfPedUqp78VaQNJt8DSt"))
.withHeader("content-type", equalTo("application/json"))
.withHeader("accept", equalTo("application/json"))
.withRequestBody(equalTo("""{
"query": "query { list_UserItems { _UserItems { _id email displayName appointments } } }",
"variables": null,
"operationName": null
}"""))
.willReturn(aResponse()
.withStatus(200)
.withBodyFile("vendiaResponses/getAllUsersResponse.json")))
stubFor(get("/oauth2/authorization/wiremock")
.willReturn(status(200)))
when: "the request is sent"
def response = request.exchange()
then: "an OK status is returned"
response.expectStatus()
.isOk()
}
}
Is my understanding of the mutateWith(mockOidcLogin()) method incorrect?

I'd consider using webEnvironment = SpringBootTest.WebEnvironment.MOCK
Are you sure about the Authentication implementation you have at runtime? (you can have Authentication auth auto-magically injected as #Controller method parameter and inspect it with your favorite debugger or log its class)
Isn't there anything of help in those samples: https://github.com/ch4mpy/spring-addons/tree/master/samples (look at those containing webflux in their name and focus on test classes ending with ItegrationTest in their name like this one)
If their isn't a test annotation for your spring Authentication type in the repo yet, open an issue. I might consider implement it.

Related

Wiremock does not receive any requests

I am writing an integrationtest for a resource (Jax-rs with resteasy) which invokes a thirdparty request to the Trustpilot Api to get a review link for a product.
In my tests I wanted to mock this call by using Wiremock and invoke the request by using resteasy Dispatcher.
The code below shows my Setup (names etc are changed).
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = { MyResourceTestContext.class })
public class MyResourceIntegrationTest {
#Rule
public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort().dynamicHttpsPort());
#Autowired
private MyResource myResource;
private Dispatcher dispatcher;
private MockHttpResponse response;
#Before
public void setUp() throws Exception {
dispatcher = MockDispatcherFactory.createDispatcher();
dispatcher.getRegistry().addSingletonResource(myResource);
response = new MockHttpResponse();
}
#Test
public void testResourceShouldReturnRedirectToTrustpilotreview() throws Exception {
URI apiRequestURI = UriBuilder
.fromUri("https://api.trustpilot.com/v1/private/product-reviews/business-units"
+ "/{businessUnitId}}/invitation-links")
.build(BUSINESS_UNIT_ID);
URI expectedRedirectURI = UriBuilder.fromUri("https://products.trustpilot.com/#evaluate/{reviewLinkId}")
.build(REVIEW_LINK_ID);
stubFor(post(apiRequestURI.toString())
.willReturn(okJson("{\"reviewLinkId\":\"" + REVIEW_LINK_ID + "\","
+ "\"reviewUrl\":\"" + expectedRedirectURI.toString() + "\"}")));
MockHttpRequest request = MockHttpRequest.get("/myResource");
dispatcher.invoke(request, response);
WireMock.verify(1, postRequestedFor(urlEqualTo(apiRequestURI.toString())));
Assert.assertEquals(HttpStatus.TEMPORARY_REDIRECT.value(), response.getStatus());
Assert.assertEquals(expectedRedirectURI.toString(), response.getOutputHeaders().getFirst("Location"));
}
But what I'm getting is (with my actual bid in the url):
com.github.tomakehurst.wiremock.client.VerificationException: Expected at least one request matching: {
"url" : "https://api.trustpilot.com/v1/private/product-reviews/business-units/<bid>/invitation-links",
"method" : "POST"
}
Requests received: [ ]
Stepping into the request with the Debugger I can say this request and other requests are actually executed successfully. But they are not recognized/mocked by Wiremock.
Is there something wrong with my Wiremock setup? Any Ideas? Thank you!
There are a couple of reasons why this isn't sending anything to WireMock at the moment:
WireMock only accepts relative URLs when stubbing so passing the full URL including protocol and domain won't work. Try something equivalent to stubFor(post(urlPathEqualTo("/v1/private/product-reviews/business-units/<bid>/invitation-links") instead.
There doesn't seem to be any way for your class under test to know which host/port WireMock is running on and no forward proxying set up so the code is presumably still trying to call the real Trustpilot API.
To solve 2. you have a couple of options:
Set up your class/code under test to use localhost as the destination host and the result of wireMockRule.port() as the port number, or wireMockRule.httpsPort() if you're on HTTPS.
Use the new auto JVM proxy setup feature to proxy your app's calls through WireMock. This assumes you're on WireMock 2.31.0+ and that your app's HTTP client respects the JVM proxy system properties.

Spring Boot Webflux Security - reading Principal in service class when writing tests

I am quite new to the Spring ecosystem in general and Webflux. There are 2 things that I am trying to figure out and cannot find any specifics about.
My Setup:
I am writing a Spring Boot 2 REST API using WebFlux (not using controllers but rather handler functions). The authentication server is a separate service which issues JWT tokens and those get attached to each request as Authentication headers. Here is a simple example of a request method:
public Mono<ServerResponse> all(ServerRequest serverRequest) {
return principal(serverRequest).flatMap(principal ->
ReactiveResponses.listResponse(this.projectService.all(principal)));
}
Which i use to react to a GET request for a list of all "Projects" that a user has access to.
I afterwards have a service which retrieves the list of projects for this user and i render a json response.
The Problems:
Now in order to filter the projects based on the current user id i need to read it from the request principal. One issue here is that i have plenty service methods which need the current user information and passing it through to the service seems like an overkill. One solution is to read the principal inside the service from:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Question 1:
Is this a good practice in general when writing functional code (If i do this instead of propagating the principal)? is this a good approach despite the complexity of reading and sending the principal from the request to the service in each method?
Question 2:
Should i instead use the SecurityContextHolder Thread Local to fetch the principal, and if i do that how do i write tests for my service?
If i use the Security Context how do i test my service implementations which are expecting a principal that is of type JWTAuthenticationToken
and i always get null when trying to do something like described here: Unit testing with Spring Security
In the service tests, In tests what i've managed to do so far is to propagate the principal to the service methods and use mockito to mock the principal. This is quite straightforward.
In the Endpoint Tests i am using #WithMockUser to populate the principal when doing requests and i mock out the service layer. This has the downside of the principal type being different.
Here is how my test class for the service layer looks:
#DataMongoTest
#Import({ProjectServiceImpl.class})
class ProjectServiceImplTest extends BaseServiceTest {
#Autowired
ProjectServiceImpl projectService;
#Autowired
ProjectRepository projectRepository;
#Mock
Principal principal;
#Mock
Principal principal2;
#BeforeEach
void setUp() {
initMocks(this);
when(principal.getName()).thenReturn("uuid");
when(principal2.getName()).thenReturn("uuid2");
}
// Cleaned for brevity
#Test
public void all_returnsOnlyOwnedProjects() {
Flux<Project> saved = projectRepository.saveAll(
Flux.just(
new Project(null, "First", "uuid"),
new Project(null, "Second", "uuid2"),
new Project(null, "Third", "uuid3")
)
);
Flux<Project> all = projectService.all(principal2);
Flux<Project> composite = saved.thenMany(all);
StepVerifier
.create(composite)
.consumeNextWith(project -> {
assertThat(project.getOwnerUserId()).isEqualTo("uuid2");
})
.verifyComplete();
}
}
Based on the other answer, i managed to solve this problem in the following way.
I added the following methods to read the id from claims where it normally resides within the JWT token.
public static Mono<String> currentUserId() {
return jwt().map(jwt -> jwt.getClaimAsString(USER_ID_CLAIM_NAME));
}
public static Mono<Jwt> jwt() {
return ReactiveSecurityContextHolder.getContext()
.map(context -> context.getAuthentication().getPrincipal())
.cast(Jwt.class);
}
Then i use this within my services wherever needed, and i am not forwarding it through the handler to the service.
The tricky part was always testing. I am able to resolve this using the custom SecurityContextFactory. I created an annotation which i can attach the same way as #WithMockUser, but with some of the claim details i need instead.
#Retention(RetentionPolicy.RUNTIME)
#WithSecurityContext(factory = WithMockTokenSecurityContextFactory.class)
public #interface WithMockToken {
String sub() default "uuid";
String email() default "test#test.com";
String name() default "Test User";
}
Then the Factory:
String token = "....ANY_JWT_TOKEN_GOES_HERE";
#Override
public SecurityContext createSecurityContext(WithMockToken tokenAnnotation) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
HashMap<String, Object> headers = new HashMap<>();
headers.put("kid", "SOME_ID");
headers.put("typ", "JWT");
headers.put("alg", "RS256");
HashMap<String, Object> claims = new HashMap<>();
claims.put("sub", tokenAnnotation.sub());
claims.put("aud", new ArrayList<>() {{
add("SOME_ID_HERE");
}});
claims.put("updated_at", "2019-06-24T12:16:17.384Z");
claims.put("nickname", tokenAnnotation.email().substring(0, tokenAnnotation.email().indexOf("#")));
claims.put("name", tokenAnnotation.name());
claims.put("exp", new Date());
claims.put("iat", new Date());
claims.put("email", tokenAnnotation.email());
Jwt jwt = new Jwt(token, Instant.now(), Instant.now().plus(1, ChronoUnit.HOURS), headers,
claims);
JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(jwt, AuthorityUtils.NO_AUTHORITIES); // Authorities are needed to pass authentication in the Integration tests
context.setAuthentication(jwtAuthenticationToken);
return context;
}
Then a simple test will look like this:
#Test
#WithMockToken(sub = "uuid2")
public void delete_whenNotOwner() {
Mono<Void> deleted = this.projectService.create(projectDTO)
.flatMap(saved -> this.projectService.delete(saved.getId()));
StepVerifier
.create(deleted)
.verifyError(ProjectDeleteNotAllowedException.class);
}
As you are using Webflux you should be using the ReactiveSecurityContextHolder to retrieve the principal like so : Object principal = ReactiveSecurityContextHolder.getContext().getAuthentication().getPrincipal();
The use of the non-reactive one will return null as you are seeing.
There is more info related to the topic in this answer - https://stackoverflow.com/a/51350355/197342

Testing a secure spring mvc restful webservice

I have a secure web service that I have been trying to test. The service looks something like this:
#GetMapping(value= {"/v1/getSomeStuff"})
public SomeResp getSomeStuff(final UsernamePasswordAuthenticationToken token, #RequestParam(name="searchString", required=true) String request) {
//do some stuff in here
}
The services are secured using Spring Security and I am trying to write an IT test, that will supply the required UsernamePasswordAuthenticationToken. I have tried doing this:
#Test
#Sql(scripts = { "/db-scripts/company-script.sql","/db-scripts/user-script.sql" })
#WithUserDetails(value="testuser")
public void testGetSomeStuff() {
//test some stuff
getRestTemplate().getForEntity("http://localhost:" + port + "/v1/getSomeStuff?searchString=foo", SomeResp.class);
}
The Sql scripts are executing correctly and the User Details Service is being called, but the token is not being passed to the service. I understand (now) that #WithUserDetails is for test method security and won't do what I want. My question then is, is there another method for testing secured web services using the various spring test classes?

Spring cloud contract best practice to handle unhappy path

We used to have wiremock for integration tests with both happy and unhappy paths. Now we are trying to move to Spring cloud contract based integration tests. Though, I could not find any document related to contracts for unhappy paths (status code over 400). And I did some POC with status code 4xx/5xx in response but it didnot work.
Anyone knows the best practice to handle unhappy paths in consumer side? or is it not supported at all for unhappy paths with status code over 400 with spring cloud contract?
Here is an example:
Producer side
Contract.make {
description 'get 404 when entity was not found'
request {
method GET()
url '/entities/0'
}
response {
status NOT_FOUND()
}
}
Client side
#RunWith(SpringRunner.class)
#SpringBootTest(classes = SomeApplication.class)
#AutoConfigureStubRunner(ids = "io.app:entity:+:stubs:8080")
#AutoConfigureTestDatabase
public class EntityClientTest {
#Rule
public ExpectedException exception = ExpectedException.none();
#Autowired
private EntityClient entityClient; // This is a FeignClient
#Test
public void shouldThrowNotFoundWithInvalidId() {
exception.expect(FeignException.class);
exception.expectMessage("404");
entityClient.getById(0);
}
}
As you can see, the getById thrown a 404 because the contract says so.

Spring oauth2 and integration tests

Need a help with integration tests for Oauth2 client.
Setup:
Client with protected UI and API
Authentication server where all
password validation done and access token retrieved
Integration test:
rest-assured used for end-point testing
before implementing Oauth2 tests worked fine
Ole test example:
given().auth()
.preemptive()
.basic(USER_EMAIL,PASSWORD) <-- this not valid any more
.contentType(ContentType.JSON)
.when()
.pathParam("id","123")
.delete(PROFILE_FIELD_BASE_URL)
.andReturn()
.body();
Question:
how I can make this tests work again?
How res-assured setup should be changed to support oauth2?
Do I need to mock Authentication server or can I inject/mock security context?
The code you were shown is only and only for basic auth and for using the rest assured for OAuth, in general, you have to change that code. From REST Assured github page you can see following two exaples:
#Test public void
oauth2_works_with_preemptive_header_signing() {
final String accessToken = "accessToken";
given().
auth().preemptive().oauth2(accessToken).
filter(new Filter() {
public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext ctx) {
assertThat(requestSpec.getHeaders().getValue("Authorization"), equalTo("Bearer "+accessToken));
return new ResponseBuilder().setBody("ok").setStatusCode(200).build();
}
}).
when().
get("/somewhere").
then().
statusCode(200);
}
#Test public void
oauth2_works_with_non_preemptive_header_signing() {
final String accessToken = "accessToken";
given().
auth().oauth2(accessToken).
filter(new Filter() {
public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext ctx) {
AuthenticationScheme scheme = requestSpec.getAuthenticationScheme();
assertThat(scheme, instanceOf(PreemptiveOAuth2HeaderScheme.class));
assertThat(((PreemptiveOAuth2HeaderScheme) scheme).getAccessToken(), equalTo(accessToken));
return new ResponseBuilder().setBody("ok").setStatusCode(200).build();
}
}).
when().
get("/somewhere").
then().
statusCode(200);
}
and as the other example, you can have a look here.

Resources