Monday, February 22, 2010

POSTing to a Rails-based Server

I was building a Java-based BlackBerry client application, and I needed to do what I expected would be fairly straightforwards communication with a Ruby on Rails web server. I needed to submit a form via POST in order to insert some records into a server-side database. Didnt sound difficult, and it was something I'd done many times before in different environments.

Creating the form and submitting the HTTP request was indeed fairly easy. However, every POST request failed for me, with an "ActionController::InvalidAuthenticityToken" error. It appeared that Rails was expecting my client to set a token as part of the POST request. After a bit of research, I discovered that Rails enforces this token requirement to prevent Cross-Site forging requests. (There's a better explanation of CSRF than I can give here. ) So - the first, obvious thing I needed to do was to retrieve this Authenticity Token and inject it in to my POST request. The second, more subtle thing (that it took me a looong time to realize) was that in addition to having a valid Authenticity Token, all requests needed to belong to the same session. i.e. a server-side cookie corresponding to the session needed to be passed along with my client's HTTP request.

So finally, after much frustration, I felt I had a handle on the problem, so coming up with a solution was not too difficult.

To submit a POST request successfully, the following is needed:

  • Make an HTTP GET request to the "root/home" page on the Rails site (i.e. some generic home page) and parse the HTTP response, to retrieve the Authenticity Token and the server-side cookie.
  • Build a POST request, setting the cookie as an HTTP request property, and the Authenticity Token as a POST parameter. This way, the POST is part of the same session as initial GET request, and the Authenticity Key is consistent, indicating that both requests originated from the same client.
Following are the steps I used for the BlackBerry-based Java app:

Send the HTTP GET Request:




try {
c = (HttpConnection) Connector.open(url);

String cookie = c.getHeaderField("Set-Cookie");

InputStream is = c.openInputStream();
}
catch ( Exception e ) {
...
}





From here, we get the value of the server-side cookie. We also get the HTTP response from the HTTPConnection object. I retrieved it as an input stream, then parsed out the Authenticity Key using a simple SAX parser:




SAXParser parser = SAXParserFactory.newInstance().newSAXParser();

TokenHandler handler = new TokenHandler();

parser.parse(is, handler);

...

private class TokenHandler extends DefaultHandler {

public void startElement(String uri, String localName, String qName,
Attributes attributes) {

if (qName.equals("input")) {
if (attributes != null) {
for (int i = 0; i < attributes.getLength(); i++) {

if ( (attributes.getLocalName(i).equals("name")) &&
(attributes.getValue(i).equals("authenticity_token"))) {
isAttribute = true;

}
else if ( (attributes.getLocalName(i).equals("value"))
&& (isAttribute) ) {
token = attributes.getValue(i);
isAttribute = false;
break;
}
}
}
}
}

}




Finally, I created and submitted the POST request using the retrieved cookie value and Authenticity Token. The cookie needs to be set as a request property:

HttpConnection hc = ...
String cookie = ...

hc.setRequestProperty("cookie", cookie);


The Authenticity Token simply needs to be added as another form data parameter, with name="authenticity_token" and value= [retrieved Authenticity Key value].

So - something that proved surprisingly difficult for me to do - so hopefully it'll save you some pain in the future.