Sign Spring WebClient HTTP request with AWS - java

I would like to AWS sign my HTTP request fired by reactive WebClient of Spring. To sign the request I need access to the followings: URL, HTTP method, query parameters, headers and request body bytes.
I started with writing an ExchangeFilterFunction. Due to ClientRequest interface I can access everything there I need, except the request body:
#Component
public class AwsSigningInterceptor implements ExchangeFilterFunction
{
private final AwsHeaderSigner awsHeaderSigner;
public AwsSigningInterceptor(AwsHeaderSigner awsHeaderSigner)
{
this.awsHeaderSigner = awsHeaderSigner;
}
#Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next)
{
Map<String, List<String>> signingHeaders = awsHeaderSigner.createSigningHeaders(request, new byte[]{}, "es", "us-west-2"); // should pass request body bytes in place of new byte[]{}
ClientRequest.Builder requestBuilder = ClientRequest.from(request);
signingHeaders.forEach((key, value) -> requestBuilder.header(key, value.toArray(new String[0])));
return next.exchange(requestBuilder.build());
}
}
In older spring versions we used RestTemplate with a ClientHttpRequestInterceptor. In that case the bytes of the body were exposed, so signing was possible.
As I see in case of WebClient Spring handles the body as a Publisher, so I'm not sure if an ExchangeFilterFunction is a good place to start.
How should I sign the HTTP request?

Related

The request body is sent as json even though the content type is set as application/x-www-form-urlencoded

This is related to an existing spring boot question raised by me(Request Body is not properly encoded and hidden when using spring form encoder in Feign Client).
According to this question, we can add either content type in headers or add during request mapping itself as consumes.
So what I did was added content type in headers in the client configuration class
public class EmailClientConfiguration {
#Bean
public RequestInterceptor requestInterceptor(Account<Account> account) {
return template -> {
template.header("Content-Type", "application/x-www-form-urlencoded");
};
}
#Bean
public OkHttpClient client() {
return new OkHttpClient();
}
#Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
#Bean
public Decoder feignDecoder() {
return new JacksonDecoder();
}
#Bean
public Encoder feignFormEncoder () {
return new SpringFormEncoder(new JacksonEncoder());
}
}
and I see in the headers the content type is correctly set as application/x-www-form-urlencoded when the request is sent. But the request body is still sent in json format and also not hidden.
Request Body:
Map<String, String> requestBody = new HashMap<>();
requestBody.put("username", "xyz");
requestBody.put("email", "xyz#gmail.com");
requestBody.put("key", "xxx");
Request Body received in server end:
{"{\n \"key\" : \"xxx\",\n \"email\" : \"xyz#gmail.com\",\n \"username\" : \"xyz\"\n}"
When I add consumes in my request mapping as application/x-www-form-urlencoded
#FeignClient(name = "email", url = "localhost:3000",
configuration = EmailClientConfiguration.class)
public interface EmailClient {
#PostMapping(value = "/email/send", consumes = "application/x-www-form-urlencoded")
ResponseDto sendEmail(#RequestBody Map<String, String> requestBody);
}
it works fine(request body is hidden in server end and also properly encoded). And when I removed the header in the configuration class and adding only consumes works fine without no issues but the vice versa has this problem.
I searched in internet for this and couldn't find any answer.
Feign encodes the request body and parameters before passing the request to any RequestInterceptor (and rightly so). If you do not declare consumes = "application/x-www-form-urlencoded", SprinFormEncoder doesn't know that you're trying to send form data, so it delegates serialization to the inner JacksonEncoder which only does JSON (see for yourself by printing template.body() before setting the header).
Handling such a well-supported header in the interceptor doesn't seem like a good idea, when you already have consumes. If you insist on doing so, you have to provide your own encoder which doesn't rely on the header value and always outputs form-urlencoded data.

Follow redirection with cookies using WebFlux

I am using WebFlux with netty to make third party calls for my spring boot app. A post request with form parameters is made on client's provided url and client responds with 302 status and a location. The code I have written below is able to follow redirections but doesn't send cookies with it which is causing subsequent redirections to fail.
WebFlux config
#Bean
public WebClient webClient() {
ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder()
.codecs((configurer) -> {
configurer.defaultCodecs().jaxb2Encoder(new Jaxb2XmlEncoder());
configurer.defaultCodecs().jaxb2Decoder(new Jaxb2XmlDecoder());
}).build();
exchangeStrategies.messageWriters().stream()
.filter(LoggingCodecSupport.class::isInstance)
.forEach(writer -> ((LoggingCodecSupport) writer)
.setEnableLoggingRequestDetails(Boolean.TRUE));
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(HttpClient.create()
.followRedirect(true)
.secure()))
.exchangeStrategies(exchangeStrategies)
.build();
}
Post Request
private <T> Mono <String> buildPostRequest(MultiValueMap <String, String> formData, String postUrl) {
return client.post()
.uri(postUrl)
.body(BodyInserters.fromFormData(formData))
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.retrieve()
.bodyToMono(String.class);
}

Add AWS Signature Header to all rest assured requests

I'm trying to call an get api which is hosted in aws api gateway via rest-assured
I'm able to sign the request and make a call. But to sign the request, I need to pass the full url to AWS to generate the Authorization Header.
For Ex. If I'm going to access an an endpoint
https://my-aws-api.com/basepath/v1/request/123
I need to sign the request via AWSSigner which needs the full endpoint to do so.
My current approach
String baseURI="https://my-aws-api.com";
String basePath="basepath/v1";
String requestPath="request/123";
String endpoint=baseURI+"/"+basePath+"/"+requestPath;
Map<String,String> signedHeaders= aws4sign(endpoint,defaultHeaders);
given()
.log().ifValidationFails()
.headers(signedHeaders)
.when()
.get(endpoint)
.then()
.log().ifValidationFails()
.statusCode(200);
If I do that , then I cant use RestAssured's baseURI, basePath and path params
I want to access it like
RestAssured.baseURI="https://my-aws-api.com";
RestAssured.basePath="basepath/v1";
given()
.log().ifValidationFails()
.pathParam("reqID", "123")
.when()
.get("request/{reqID}")
.then()
.log().ifValidationFails()
.statusCode(200);
AwsSigner
public static Map<String, String> aws4Sign(String endpoint, Map<String, String> headers) throws URISyntaxException {
String serviceName = "execute-api";
AWS4Signer aws4Signer = new AWS4Signer();
aws4Signer.setRegionName(EU_WEST_1.getName());
aws4Signer.setServiceName(serviceName);
DefaultRequest defaultRequest = new DefaultRequest(serviceName);
URI uri = new URI(endpoint);
defaultRequest.setEndpoint(new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), "", "", ""));
defaultRequest.setHttpMethod(HttpMethodName.GET);
defaultRequest.setResourcePath(uri.getRawPath());
defaultRequest.setHeaders(headers);
aws4Signer.sign(defaultRequest, DefaultAWSCredentialsProviderChain.getInstance().getCredentials());
return defaultRequest.getHeaders();
}
So My question is there any way, I can intercept the RestAssured's request before it makes the call, so that I can get the fully generated end point and add the aws signed header to the call.
I am not familiar with this library but from briefly reading its documentation and Javadoc, you should be able to use a RequestFilter to inspect and alter a request before it is sent out.
Take a look at the Filter section of the user guide.
Thanks to #Ashaman.
The Filter Section is what I'm looking for
You can get the uri and other headers that were passed with requests from RequestSpec and then send it to the function to sign them and remove the old headers and put the new headers. Then forward the request
#BeforeAll
public void init() {
RestAssured.baseURI = "https://my-aws-api.com";
RestAssured.filters((requestSpec, responseSpec, ctx) -> {
Map<String, String> headers = requestSpec.getHeaders()
.asList()
.stream()
.collect(Collectors.toMap(Header::getName, Header::getValue));
Map<String, String> signedHeaders = aws4sign(requestSpec.getURI(), headers);
requestSpec.removeHeaders();
requestSpec.headers(signedHeaders);
return ctx.next(requestSpec, responseSpec);
});
}
And for the tests I can use the features of Rest Assured normally
given()
.log().ifValidationFails()
.pathParam("reqID", "123")
.when()
.get("request/{reqID}")
.then()
.log().ifValidationFails()
.statusCode(200);

How to get access token from Authorisation Server using Apache Camel routes?

I have an authorization server [Simple Class annotated with #SpringBootApplication,
#RestController,#Configuration,#EnableAuthorizationServer & oauth2 security] running on port 8081 which works fine & provides the access token when requested from POSTMAN using POST method along with needful parameters in the form of key value pair,
http://localhost:8080/oauth/token, but how should i implement the camel route in java to get the access token by passing parameters in body ?
This question is more about sending multipart/form-data with Apache Camel. I was playing with it some time ago and solved it with custom Processor, converting headers to multipart/form-data format with Content-Disposition: form-data.
This is my Processor converting headers to multipart/form-data format:
public class PrepareMultipartFormData implements Processor {
private String[] multipartHeaders;
public PrepareMultipartFormData(String... multipartHeaders) {
this.multipartHeaders = multipartHeaders;
}
#Override
public void process(Exchange exchange) throws Exception {
addMultipart(exchange.getIn(), multipartHeaders);
}
private static void addMultipart(Message message, String... multipartKeys){
final String boundary = "---------------------------"+RandomStringUtils.randomAlphanumeric(9);
message.setHeader(Exchange.CONTENT_TYPE, "multipart/form-data;boundary="+boundary);
StringBuilder sb = new StringBuilder("--").append(boundary);
for (String key: multipartKeys) {
sb.append("\r\n")
.append("Content-Disposition: form-data; name=\"").append(key).append("\"")
.append("\r\n\r\n")
.append(message.getHeader(key, String.class))
.append("\r\n")
.append("--").append(boundary);
}
message.setBody(sb.toString());
}
}
To OAuth request token you need to send:
HTTP headers
Authorization header - This is part of standard HTTP component specified by endpoint options authUsername and authPassword
Content-Type - This is added in my PrepareMultipartFormData Processor
Form data - These are converted from headers in PrepareMultipartFormData Processor
grant_type
username
password
client_id
Final route can be implemented in this way:
(Replace constants with some expressions, to set it dynamically. If you need only token in response, add some unmarshalling, since this route returns JSON)
from("direct:getTokenResponse")
.setHeader(Exchange.HTTP_METHOD, constant("POST"))
.setHeader(Exchange.HTTP_PATH, constant("oauth/token"))
.setHeader("grant_type", constant("password"))
.setHeader("username", constant("admin"))
.setHeader("password", constant("admin1234"))
.setHeader("client_id", constant("spring-security-oauth2-read-write-client"))
.process(new PrepareMultipartFormData("grant_type", "username", "password", "client_id"))
.to("http://localhost:8080?authMethod=Basic&authUsername=oauth-endpoint-username&authPassword=oauth-endpoint-password")
.convertBodyTo(String.class)
.to("log:response");
Updating answer to provide a bit shorter implementation of PrepareMultipartFormData#addMultipart using MultipartEntityBuilder.
private static void addMultipart(Message message, String... multipartKeys) throws Exception{
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
for (String key: multipartKeys) {
builder.addTextBody(key, message.getHeader(key, String.class));
}
HttpEntity resultEntity = builder.build();
message.setHeader(Exchange.CONTENT_TYPE, resultEntity.getContentType().getValue());
message.setBody(resultEntity.getContent());
}

Jersey client (2.x) does not send my GET request headers

I've been debugging this for three hours, I still cannot explain why my custom headers (registered via a client request filter) are not sent.
The client is configured as such (full source here):
private WebTarget webTarget(String host, String appId, String appKey) {
return newClient(clientConfiguration(appId, appKey))
.target(host + "/rest");
}
private Configuration clientConfiguration(String appId, String appKey) {
ClientConfig config = new ClientConfig();
config.register(requestFilter(appId, appKey));
return config;
}
private ClientRequestFilter requestFilter(String appId, String appKey) {
return new VidalRequestFilter(apiCredentials(appId, appKey));
}
The filter is as follows:
public class VidalRequestFilter implements ClientRequestFilter {
private final ApiCredentials credentials;
public VidalRequestFilter(ApiCredentials credentials) {
this.credentials = credentials;
}
#Override
public void filter(ClientRequestContext requestContext) throws IOException {
MultivaluedMap<String, Object> headers = requestContext.getHeaders();
headers.add(ACCEPT, APPLICATION_ATOM_XML_TYPE);
headers.add("app_id", credentials.getApplicationId());
headers.add("app_key", credentials.getApplicationKey());
}
}
And the call is like:
String response = webTarget
.path("api/packages")
.request()
.get()
.readEntity(String.class);
All I get is 403 forbidden, because the specific endpoint I am calling is protected (the auth is performed with the custom headers defined above).
The weirdest thing is that, while I'm debugging, I see that sun.net.www.MessageHeader is properly invoked during the request write (i.e. the instance is valued as such: sun.net.www.MessageHeader#14f9390f7 pairs: {GET /rest/api/packages HTTP/1.1: null}{Accept: application/atom+xml}{app_id: XXX}{app_key: YYY}{User-Agent: Jersey/2.22.1 (HttpUrlConnection 1.8.0_45)}{Host: ZZZ}{Connection: keep-alive}.
However, I have the confirmation that neither our API server, nor its reverse proxy received GET requests with the required auth headers (a first HEAD request seems to be OK, though).
I know for sure the credentials are good 'cause the equivalent curl command just works!
I tried the straightforward approach to set headers directly when defining the call without any success.
What am I missing?

Categories

Resources