What is the right way to sign POST requests with OAuth-Signpost and Apache HttpComponents?

randomau5 picture randomau5 · Jan 30, 2013 · Viewed 9.7k times · Source

I'm currently using the OAuth-Signpost Java library to sign requests sent from a client to a server which implements OAuth authentication. When making GET requests (using HttpURLConnection) everything works fine: requests are signed, parameters are included and signatures match in destination. However, it doesn't seem to work with POST requests. I'm aware of the issues that may come up when signing POST using HttpURLConnection, so I moved to the Apache HttpComponents library for these requests. The parameters I send in the following example are plain strings and a XML-like string ('rxml'). My code goes as follows:

public Response exampleMethod(String user, String sp, String ep, String rn, String rxml){

   //All these variables are proved to be correct (they work right in GET requests)
    String uri = "...";
    String consumerKey = "...";
    String consumerSecret = "...";
    String token = "...";
    String secret = "...";

  //create the parameters list
    List<NameValuePair> params = new ArrayList<NameValuePair>();
    params.add(new BasicNameValuePair("user", user));
    params.add(new BasicNameValuePair("sp", sp));
    params.add(new BasicNameValuePair("ep", ep));
    params.add(new BasicNameValuePair("rn", rn));
    params.add(new BasicNameValuePair("rxml", rxml));

   // create a consumer object and configure it with the access
   // token and token secret obtained from the service provider
    OAuthConsumer consumer = new CommonsHttpOAuthConsumer(consumerKey, consumerSecret);
    consumer.setTokenWithSecret(token, secret);

   // create an HTTP request to a protected resource
    HttpPost request = new HttpPost(uri);

   // sign the request      
    consumer.sign(request);       

   // set the parameters into the request
    request.setEntity(new UrlEncodedFormEntity(params));

   // send the request
    HttpClient httpClient = new DefaultHttpClient();
    HttpResponse response = httpClient.execute(request);

   //if request was unsuccessful
    if(response.getStatusLine().getStatusCode()!=200){
        return Response.status(response.getStatusLine().getStatusCode()).build();
    }

   //if successful, return the response body
    HttpEntity resEntity = response.getEntity();
    String responseBody = "";

    if (resEntity != null) {
        responseBody = EntityUtils.toString(resEntity);
    }

    EntityUtils.consume(resEntity);

    httpClient.getConnectionManager().shutdown();

    return Response.status(200).entity(responseBody).build();

}

When I send a POST request to the server I get an error telling that the signatures (the one I send and the one the server calculates by itself) don't match, so I guess it has to do with the base string they are signing and the way the POST signing works, since they're handling the same keys and secrets in both sides (checked).

I've read that a way to go through this is setting the parameters as part of the URL (as in a GET request). It wouldn't work for me though, since the XML parameter may exceed the URL length so it needs to be sent as a POST parameter.

I suppose I'm doing something wrong either signing the POST requests or handling the parameters, but I don't know what it is. Please, could you help me out?

P.S: I apologize if I lack context, error traces or additional information regarding this issue, but I'm newbie around here. So please don't hesitate to ask me for more information if you need it.

Answer

Hrafn picture Hrafn · Mar 12, 2013

A bit of backstory/explanation

I've been having a similar problem for the past couple of days, and had almost given up. Until I heard that the guy at my company that was putting up the services I was communicating with, had configured them to read the OAuth information from the query string instead of header parameters.

So instead of reading it from the header parameter Authorization that Signpost puts into the request when you pass it on to be signed, for instance [Authorization: OAuth oauth_consumer_key="USER", oauth_nonce="4027096421883800497", oauth_signature="Vd%2BJEb0KnUhEv1E1g3nf4Vl3SSM%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1363100774", oauth_version="1.0"], the services where trying to read the query string, for example http://myservice.mycompany.com?oauth_consumer_key=USER&oauth_nonce=4027096421883800497&oauth_signature=Vd%2BJEb0KnUhEv1E1g3nf4Vl3SSM%3D&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1363100774&oauth_version=1.0.

The problem with this is that when I tried to sign the url and then build a HttpPost request with it, the url got a basestring with the prefix GET instead of POST which gave another signature then the one the service computed. Signpost isn't doing anything wrong, its url signing method is just by default set to GET with no other possibility available out of the box. This is so because you should read header parameters when doing POST, not the query string (Going to egg the house of that "colleague" of mine), and Signpost adds these when signing request which you should do when doing POST.

The signingbasestring can be observed in the SigningBaseString class method generate in Signpost.

Solution

Now this is how I did it, but other ways may be possible or even better.

  1. Get the signpost source code and add it to your project. Can get it here
  2. Locate the OAuthConsumer class and change the signing method so that you can pass on information that the request should be POST. In my case I added a boolean like so public String sign(String url, boolean POST)
  3. Now you need to change the sign method in the AbstractOAuthConsumer class, which CommonsHttpOAuthConsumer and DefaultOAuthConsumer extend. In my case I added the boolean variable to the method and the following if(POST) request.setMethod("POST"); right before the method calls sign(request);
  4. Now the request is a Signpost specific object, HTTPRequest, so this will throw an error. You'll need to change it and add the method public void setMethod(String method);.
  5. This will cause an error in the following classes HttpURLConnectionRequestAdapter, HttpRequestAdapter and UrlStringRequestAdapter. You'll need to add the method implementation to them all, but in different flavors. For the first you'll add

    public void setMethod(String method){
    try {
     this.connection.setRequestMethod(method);
     } catch (ProtocolException e) {
    
        e.printStackTrace();
     }
    }
    

    for the second you'll add

    public void setMethod(String method){
    
       try {
           RequestWrapper wrapper = new RequestWrapper(this.request);
           wrapper.setMethod(method);
           request = wrapper;
       } catch (org.apache.http.ProtocolException e) {
        e.printStackTrace();
        }   
    }
    

    and for the last you'll add

    public void setMethod(String method){
       mMethod = method;
    }
    

    Be warned that I've only used and tried the first and last. But this at least gives you an idea about how to fix the problem, if you are having the same one as I.

Hope this helps in anyway.

-MrDresden