Prevent Feign from adding a trailing slash to the URL - spring-cloud-feign

I am trying to retrieve an online image content in a Spring Boot application by using the below Feign client.
#FeignClient(name = "image")
public interface ImageClient {
#RequestMapping(method = RequestMethod.GET)
byte[] getContent(URI uri) throws WebException;
}
The issue that I have is that when I call the getContent method with an URL such as https://images.foo.com/1234567/5c5a7f14-d5d4-4a79-9c2e-78fed8b738c5.jpeg?foo=123, the HTTP call is made to https://images.foo.com/1234567/5c5a7f14-d5d4-4a79-9c2e-78fed8b738c5.jpeg/?foo=123 and I get an error from the server.
Is there any way to prevent Feign from adding a slash before the query parameters ?

to avoid that
divide the URL
#FeignClient(name = "image" ,value url = "BASE")
public interface ImageClient {
#RequestMapping(method = RequestMethod.GET ,value = "X" )
byte[] getContent(URI uri) throws WebException;
}
final is B/X no trailing / added

You can also
#FeignClient(name = "image" , url = "BASE")
public interface ImageClient {
#RequestMapping(method = RequestMethod.GET )
byte[] getContent(URI uri) throws WebException;
}

Related

How to customize the Authorization header of the OAuth2 token request using spring-security-oauth2 with a WebClient?

I am trying to upgrade to spring security 5.5.1 on a WebClient call.
I found out that the oauth2 clientId and secret are now URL encoded in AbstractWebClientReactiveOAuth2AccessTokenResponseClient, but my token provider does not support this (for example if the secret contains a + character it works only when it is sent as a + not as %2B).
I understand this is seen as a bug fix from spring-security side ), but I cannot make the token provider change its behavior easily.
So I tried to find a way to work around this.
The [documentation] (https://docs.spring.io/spring-security/site/docs/current/reference/html5/#customizing-the-access-token-request) on how to customize the access token request does not seem to apply when you use a WebClient configuration (which is my case).
In order to remove the clientid/secret encoding I had to extend and copy most of the existing code from AbstractWebClientReactiveOAuth2AccessTokenResponseClient to customize the WebClientReactiveClientCredentialsTokenResponseClient because most of it has private/default visibility.
I traced this in an enhancement issue in the spring-security project.
Is there an easier way to customize the Authorization header of the token request, in order to skip the url encoding ?
There is definitely room for improvement in some of the APIs around customization, and for sure these types of questions/requests/issues from the community will continue to help highlight those areas.
Regarding the AbstractWebClientReactiveOAuth2AccessTokenResponseClient in particular, there is currently no way to override the internal method to populate basic auth credentials in the Authorization header. However, you can customize the WebClient that is used to make the API call. If it's acceptable in your use case (temporarily, while the behavior change is being addressed and/or a customization option is added) you should be able to intercept the request in the WebClient.
Here's a configuration that will create a WebClient capable of using an OAuth2AuthorizedClient:
#Configuration
public class WebClientConfiguration {
#Bean
public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
// #formatter:off
ServerOAuth2AuthorizedClientExchangeFilterFunction exchangeFilterFunction =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
exchangeFilterFunction.setDefaultOAuth2AuthorizedClient(true);
return WebClient.builder()
.filter(exchangeFilterFunction)
.build();
// #formatter:on
}
#Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
// #formatter:off
WebClientReactiveClientCredentialsTokenResponseClient accessTokenResponseClient =
new WebClientReactiveClientCredentialsTokenResponseClient();
accessTokenResponseClient.setWebClient(createAccessTokenResponseWebClient());
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials(consumer ->
consumer.accessTokenResponseClient(accessTokenResponseClient)
.build())
.build();
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
// #formatter:on
return authorizedClientManager;
}
protected WebClient createAccessTokenResponseWebClient() {
// #formatter:off
return WebClient.builder()
.filter((clientRequest, exchangeFunction) -> {
HttpHeaders headers = clientRequest.headers();
String authorizationHeader = headers.getFirst("Authorization");
Assert.notNull(authorizationHeader, "Authorization header cannot be null");
Assert.isTrue(authorizationHeader.startsWith("Basic "),
"Authorization header should start with Basic");
String encodedCredentials = authorizationHeader.substring("Basic ".length());
byte[] decodedBytes = Base64.getDecoder().decode(encodedCredentials);
String credentialsString = new String(decodedBytes, StandardCharsets.UTF_8);
Assert.isTrue(credentialsString.contains(":"), "Decoded credentials should contain a \":\"");
String[] credentials = credentialsString.split(":");
String clientId = URLDecoder.decode(credentials[0], StandardCharsets.UTF_8);
String clientSecret = URLDecoder.decode(credentials[1], StandardCharsets.UTF_8);
ClientRequest newClientRequest = ClientRequest.from(clientRequest)
.headers(httpHeaders -> httpHeaders.setBasicAuth(clientId, clientSecret))
.build();
return exchangeFunction.exchange(newClientRequest);
})
.build();
// #formatter:on
}
}
This test demonstrates that the credentials are decoded for the internal access token response WebClient:
#ExtendWith(MockitoExtension.class)
public class WebClientConfigurationTests {
private WebClientConfiguration webClientConfiguration;
#Mock
private ExchangeFunction exchangeFunction;
#Captor
private ArgumentCaptor<ClientRequest> clientRequestCaptor;
#BeforeEach
public void setUp() {
webClientConfiguration = new WebClientConfiguration();
}
#Test
public void exchangeWhenBasicAuthThenDecoded() {
WebClient webClient = webClientConfiguration.createAccessTokenResponseWebClient()
.mutate()
.exchangeFunction(exchangeFunction)
.build();
when(exchangeFunction.exchange(any(ClientRequest.class)))
.thenReturn(Mono.just(ClientResponse.create(HttpStatus.OK).build()));
webClient.post()
.uri("/oauth/token")
.headers(httpHeaders -> httpHeaders.setBasicAuth("aladdin", URLEncoder.encode("open sesame", StandardCharsets.UTF_8)))
.retrieve()
.bodyToMono(Void.class)
.block();
verify(exchangeFunction).exchange(clientRequestCaptor.capture());
ClientRequest clientRequest = clientRequestCaptor.getValue();
String authorizationHeader = clientRequest.headers().getFirst("Authorization");
assertThat(authorizationHeader).isNotNull();
String encodedCredentials = authorizationHeader.substring("Basic ".length());
byte[] decodedBytes = Base64.getDecoder().decode(encodedCredentials);
String credentialsString = new String(decodedBytes, StandardCharsets.UTF_8);
String[] credentials = credentialsString.split(":");
assertThat(credentials[0]).isEqualTo("aladdin");
assertThat(credentials[1]).isEqualTo("open sesame");
}
}

Programatically authenticate AzureAd/OpenId to an MVC controller using C# and get redirect uri

I have overridden the built in WebClient as below. Then I call it
public class HttpWebClient : WebClient
{
private Uri _responseUri;
public Uri ResponseUri
{
get { return _responseUri; }
}
protected override WebResponse GetWebResponse(WebRequest request)
{
WebResponse response = base.GetWebResponse(request);
_responseUri = response.ResponseUri;
return response;
}
}
Then I consume it like this:
using (HttpWebClient client = new HttpWebClient())
{
client.Headers[HttpRequestHeader.Authorization] = $"Bearer { _token }";
client.Headers[HttpRequestHeader.ContentType] = "application/json";
client.UploadData(_url, Encoding.UTF8.GetBytes(_data));
string queryString = client.ResponseUri.Query.Split('=').Last();
}
The response uri comes back with "https://login.microsoftonline" rather than url returned from the MVC controller with a query string, as it is authenticating first with that bearer token using AzureAd/OpenId. If i call it twice it returns the original _url but not the redirected one. If I remove AzureAd authentication it works fine. Is there a way to force the response uri to come back as what the MVC controller sets it to?
Assuming you use the 'UseOpenIdConnectAuthentication' and configuring it to use AAD authentication, you can modify the redirect uri by setting Notifications.RedirectToIdentityProvider, something like:
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = async _ =>
{
_.ProtocolMessage.RedirectUri = _.Request.Uri.ToString();
}
}
If you use something else , or maybe I didn't understand your problem - please supply more information

MVC3 Redirect Loop

I have been searching for a solution to this issue for over 10 hours with no answer. In my application I am using the [requirehttps] attribute. When clicking an action method decorated with this attribute I get “cannot display the webpage" in IE. After digging into that issue I saw that I was receiving infinite 302 calls in Fiddler, which would eventually timeout and cause that error. So I decided to create a custom attribute and physically create the https call. I am using IIS Express and successfully created a certificate and binded the port. If I call this URL directly through the browser everything works fine. This is the code I have been using to redirect the request.
public class HttpsAttribute : System.Web.Mvc.RequireHttpsAttribute
{
public bool RequireSecure = false;
public override void OnAuthorization(System.Web.Mvc.AuthorizationContext filterContext){
var builder = new UriBuilder(HttpContext.Current.Request.Url);
if (RequireSecure){
// redirect to HTTP version of page
builder.Scheme = Uri.UriSchemeHttps;
builder.Port = 44300;
filterContext.Result = new RedirectResult(builder.Uri.ToString());
}
else{
// non secure requested
if (filterContext.HttpContext.Request.IsSecureConnection){
HandleNonHttpRequest(filterContext);
}
}
}
protected virtual void HandleNonHttpRequest(AuthorizationContext filterContext){
if (String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)){
// redirect to HTTP version of page
string url = "http://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
filterContext.Result = new RedirectResult(url);
}
}
}
When I set a breakpoint to find out what the builder value is, it is correct. And it is from here I receive an infinite redirect loop. The strange thing is that the first request is never correct when I look at the URL within the browser. It is as if UriBuilder is not sending the correct URL. Any ideas? This is driving me nuts.
The redirect loop is occurring because the code is only checking if the HTTPS redirect is required, not if the current request is already HTTPS (i.e. the redirect has already happened).
if (RequireSecure && !filterContext.HttpContext.Request.IsSecureConnection){
// redirect to HTTP version of page
builder.Scheme = Uri.UriSchemeHttps;
builder.Port = 44300;
filterContext.Result = new RedirectResult(builder.Uri.ToString());
}
else{
// non secure requested
if (filterContext.HttpContext.Request.IsSecureConnection){
HandleNonHttpRequest(filterContext);
}
}
Although the RequireHttps should work correctly for this, unless you need to redirect to the port specified.
EDIT:
Refactored attribute
public class HttpsAttribute : System.Web.Mvc.RequireHttpsAttribute
{
public bool RequireSecure = false;
public override void OnAuthorization(System.Web.Mvc.AuthorizationContext filterContext)
{
var requestUri = HttpContext.Current.Request.Url;
var requestIsSecure = HttpContext.Current.Request.IsSecureConnection;
if (RequireSecure && !requestIsSecure)
filterContext.Result = Redirect(requestUri, Uri.UriSchemeHttps, 44300);
else if (!RequireSecure && requestIsSecure)
filterContext.Result = Redirect(requestUri, Uri.UriSchemeHttp, 80);
}
private RedirectResult Redirect(Uri uri, string scheme, int port)
{
return new RedirectResult(new UriBuilder(uri) { Scheme = scheme, Port = port }.Uri.ToString());
}
}

Posting multipart form data to seam+RESTeasy fails marshalling to InputStream

I'm trying to post image data to a seam+RESTeasy endpoint and I'm getting a very cryptic error during JBoss startup. The HTTP request I'm sending has a content-type of multipart/form-data which has a single image/jpeg part with name "attachment". My service method looks like this:
#POST
#Path("uploadSymptomsImage/{appointmentGUID}")
#Consumes(MediaType.MULTIPART_FORM_DATA)
#Produces("application/json")
public String uploadSymptomsImage( #FormParam("attachment") InputStream fileInputStream,
#PathParam("appointmentGUID") String strAppointmentGUID )
{ ...
The error that I get is during startup:
Caused by: java.lang.RuntimeException: Unable to find a constructor that takes a String param or a valueOf() or fromString() method for javax.ws.rs.FormParam("attachment") on public java.lang.String com....AppointmentRestService.uploadSymptomsImage(java.io.InputStream,java.lang.String) for basetype: java.io.InputStream
at org.jboss.resteasy.core.StringParameterInjector.initialize(StringParameterInjector.java:206) [:]
at org.jboss.resteasy.core.StringParameterInjector.<init>(StringParameterInjector.java:57) [:]
at org.jboss.resteasy.core.FormParamInjector.<init>(FormParamInjector.java:22) [:]
My understanding was that media types could be automatically marshalled to InputStream. I've also tried java.io.File, java.io.Reader - both with same error. When I replace with byte[] or String I get a zero length array, or null as the parameter value.
How would you go about debugging this? Also, is it possible to access the raw request or pre-marshalled values?
Any suggestions here would be greatly appreciated.
You should retrieve the contents using MultipartFormDataInput. See the following example:
#POST
#Path("uploadSymptomsImage/{appointmentGUID}")
#Consumes(MediaType.MULTIPART_FORM_DATA)
#Produces("application/json")
public String uploadSymptomsImage(#PathParam("appointmentGUID") String strAppointmentGUID,
MultipartFormDataInput formData) {
Map<String, List<InputPart>> formDataMap = formData.getFormDataMap();
List<InputPart> attachments = formDataMap.get("attachment");
for(InputPart attachment : attachments) {
String fileName = extractFilename(attachment);
if(fileName.isEmpty()) continue;
InputStream in = attachment.getBody(new GenericType<InputStream>() {});
// Interact with stream
}
// Respond
}
The extractFilename method is a helper method I wrote:
private static String extractFilename(final InputPart attachment) {
Preconditions.checkNotNull(attachment);
MultivaluedMap<String, String> headers = attachment.getHeaders();
String contentDispositionHeader = headers.getFirst("Content-Disposition");
Preconditions.checkNotNull(contentDispositionHeader);
for(String headerPart : contentDispositionHeader.split(";(\\s)+")) {
String[] split = headerPart.split("=");
if(split.length == 2 && split[0].equalsIgnoreCase("filename")) {
return split[1].replace("\"", "");
}
}
return null;
}

ASP.NET MVC - Using cURL or similar to perform requests in application

I'm building an application in ASP.NET MVC (using C#) and I would like to know how I can perform calls like curl http://www.mywebsite.com/clients_list.xml inside my controller
Basically I would like to build a kind of REST API to perform actions such as show edit and delete, such as Twitter API.
But unfortunately until now I didn't find anything besides that cURL for windows on this website: http://curl.haxx.se/
So I don't know if is there any traditional way to retrieve this kind of call from URL with methods like post delete and put on the requests, etc...
I just would like to know an easy way to perform commands like curl inside my controller on my ASP.NET MVC Application.
UPDATE:
Hi so I managed to make GET Requests but now I'm having a serious problem in retrieve POST Request for example, I'm using the update status API from Twitter that in curl would work like this:
curl -u user:password -d "status=playing with cURL and the Twitter API" http://twitter.com/statuses/update.xml
but on my ASP.NET MVC application I'm doing like this inside my custom function:
string responseText = String.Empty;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://twitter.com/statuses/update.xml");
request.Method = "POST";
request.Credentials = new NetworkCredential("username", "password");
request.Headers.Add("status", "Tweeting from ASP.NET MVC C#");
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
using (StreamReader sr = new StreamReader(response.GetResponseStream()))
{
responseText = sr.ReadToEnd();
}
return responseText;
Now the problem is that this request is returning 403 Forbidden,
I really don't know why if it works perfectly on curl
:\
UPDATE:
I finally manage to get it working, but probably there's a way to make it cleaner and beautiful, as I'm new on C# I'll need more knowledge to do it, the way the POST params are passed makes me very confused because is a lot of code to just pass params.
Well, I've created a Gist - http://gist.github.com/215900 , so everybody feel free to revise it as you will. Thanks for your help çağdaş
also follow the code here:
public string TwitterCurl()
{
//PREVENT RESPONSE 417 - EXPECTATION FAILED
System.Net.ServicePointManager.Expect100Continue = false;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://twitter.com/statuses/update.xml");
request.Method = "POST";
request.Credentials = new NetworkCredential("twitterUsername", "twitterPassword");
//DECLARE POST PARAMS
string headerVars = String.Format("status={0}", "Tweeting from ASP.NET MVC C#");
request.ContentLength = headerVars.Length;
//SEND INFORMATION
using (StreamWriter streamWriter = new StreamWriter(request.GetRequestStream(), ASCIIEncoding.ASCII))
{
streamWriter.Write(headerVars);
streamWriter.Close();
}
//RETRIEVE RESPONSE
string responseText = String.Empty;
using (StreamReader sr = new StreamReader(request.GetResponse().GetResponseStream()))
{
responseText = sr.ReadToEnd();
}
return responseText;
/*
//I'M NOT SURE WHAT THIS IS FOR
request.Timeout = 500000;
request.ContentType = "application/x-www-form-urlencoded";
request.UserAgent = "Custom Twitter Agent";
#if USE_PROXY
request.Proxy = new WebProxy("http://localhost:3000", false);
#endif
*/
}
Try using Microsoft.Http.HttpClient. This is what your request would look like
var client = new HttpClient();
client.DefaultHeaders.Authorization = Credential.CreateBasic("username","password");
var form = new HttpUrlEncodedForm();
form.Add("status","Test tweet using Microsoft.Http.HttpClient");
var content = form.CreateHttpContent();
var resp = client.Post("http://www.twitter.com/statuses/update.xml", content);
string result = resp.Content.ReadAsString();
You can find this library and its source included in the WCF REST Starter kit Preview 2, however it can be used independently of the rest of the stuff in there.
P.S. I tested this code on my twitter account and it works.
Example code using HttpWebRequest and HttpWebResponse :
public string GetResponseText(string url) {
string responseText = String.Empty;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "GET";
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
using (StreamReader sr = new StreamReader(response.GetResponseStream())) {
responseText = sr.ReadToEnd();
}
return responseText;
}
To POST data :
public string GetResponseText(string url, string postData) {
string responseText = String.Empty;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "POST";
request.ContentLength = postData.Length;
using (StreamWriter sw = new StreamWriter(request.GetRequestStream())) {
sw.Write(postData);
}
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
using (StreamReader sr = new StreamReader(response.GetResponseStream())) {
responseText = sr.ReadToEnd();
}
return responseText;
}
This is the single line of code I use for calls to a RESTful API that returns JSON.
return ((dynamic) JsonConvert.DeserializeObject<ExpandoObject>(
new WebClient().DownloadString(
GetUri(surveyId))
)).data;
Notes
The Uri is generated off stage using the surveyId and credentials
The 'data' property is part of the de-serialized JSON object returned
by the SurveyGizmo API
The Complete Service
public static class SurveyGizmoService
{
public static string UserName { get { return WebConfigurationManager.AppSettings["SurveyGizmo.UserName"]; } }
public static string Password { get { return WebConfigurationManager.AppSettings["SurveyGizmo.Password"]; } }
public static string ApiUri { get { return WebConfigurationManager.AppSettings["SurveyGizmo.ApiUri"]; } }
public static string SurveyId { get { return WebConfigurationManager.AppSettings["SurveyGizmo.Survey"]; } }
public static dynamic GetSurvey(string surveyId = null)
{
return ((dynamic) JsonConvert.DeserializeObject<ExpandoObject>(
new WebClient().DownloadString(
GetUri(surveyId))
)).data;
}
private static Uri GetUri(string surveyId = null)
{
if (surveyId == null) surveyId = SurveyId;
return new UriBuilder(ApiUri)
{
Path = "/head/survey/" + surveyId,
Query = String.Format("user:pass={0}:{1}", UserName, Password)
}.Uri;
}
}
Look into the System.Net.WebClient class. It should offer the functionality you require. For finer grained control, you might find WebRequest to be more useful, but WebClient seems the best fit for your needs.

Resources