Cross-Origin Resource Sharing for JSON and RAILS

CORS (Cross-Origin Resource Sharing) is a protocol built on-top of HTTP for allowing Javascript on a page originating from one site to access methods on another site.  This is the preferred method for allowing Javascript code to escape from its default Same-Origin
Policy
.  While the protocol has been around for a few years, and is built into all of the major browsers, the protocol does not seem to be widely documented.  Here are some experiences I’ve encountered while enabling access cross-origin access for JSON from a Rails server.

Background

The Same-Origin policy restricts Javascript code to making Ajax calls to the site that its containing page came from.  For example, Javascript on webpage from http://mysite.com is not permitted to make Ajax calls to a web-service at http://othersite.com/method.json.  This policy is a security measure to prevent unwitting visitors to a website from executing malicious code in their browsers.

For web-service implementors, this is an annoying restriction.  Allowing Javascript to access data from multiple sites would allow programmers to create browser-based mash-ups.

Server-side proxying is a traditional method for getting around the Same-Origin restriction for Ajax requests.  With proxying, the owner of mysite.com implements a copy of the remote method that repeats the request from the page-owner’s own site.  The server on “mysite.com” must process each remote method call by calling the method on “othersite.com” so that it can return the results to the browser.

http://mysite.com/method.json --> http://othersite.com/method.json

The great advantage of this method is that it works with any browser.  The drawback is that it imposes work on web-service subscribers that seems redundant.

Cross-Origin Resource Sharing

CORS is a protocol negotiated between a browser and a web-service that tells the browser that it is “OK” to execute Javascript code from a cross-domain call.  The specification covers “Simple” transactions and complex transactions that use a “Preflight” request.  Cross-origin JSON requests with non-standard headers are not “Simple” and require the “Preflight” request.

A great introduction to the CORS protocol appears here:
http://www.nczonline.net/blog/2010/05/25/cross-domain-ajax-with-cross-origin-resource-sharing/

and  an example of the CORS transactions appears here.

http://arunranga.com/examples/access-control/preflightXSInvocation.txt

The “Preflight” request is a new HTTP verb called OPTIONS.  In a browser implementing CORS, each cross-origin GET or POST request is preceded by an OPTIONS request that checks whether the GET or POST is OK.  If it is, the server must return some headers to allow the subsequent GET or POST.  This is actually a wonderful capability.  The server can allow or disallow remote access on a per-method basis, with access determined by HTTP referrer, IP or any other criterial.

The OPTIONS request contains Access-Control headers that are part of the CORS specification.  The response must reply to these headers to allow the subsequent GET or POST to proceed.

For example, an access to

GET http://othersite.com/method.json

would be preceeded by an OPTIONS method that looks like this.

OPTIONS http://othersite.com/method.json
Origin: http://mysite.com
Access-Control-Request-Method: GET

The server would respond with an empty response body of type
“text/plain” that contains headers allowing the request.

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Max-Age: 1728000
Content-Length: 0
Content-Type: text/plain

Custom Headers

If your application uses non-standard headers, you must take special steps to permit them or the browser will flag a CORS violation.   I ran into this restriction in the application I was writing.  Unfortunately, the security violation messages from the browser are obscure, and it took me a while to figure this out.

In our application, our Javascript client uses prototype.js to make Ajax calls.  Prototype adds the following headers to the request.

X-Requested-With: XMLHttpRequest
X-Prototype-Version: N.N.N.N

Our server must explicitly allow these headers in the CORS exchange or the browser will disallow the cross-origin request. The OPTIONS request will specify the headers it wants to add. Our OPTIONS/Response exchange looks like this.

OPTIONS http://othersite.com/method.json
Origin: http://mysite.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: X-Requested-With, X-Prototype-Version

Response:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: X-Requested-With, X-Prototype-Version
Access-Control-Max-Age: 1728000
Content-Length: 0
Content-Type: text/plain

CORS in Rails

I implemented the CORS protocol in a Rails application with just a couple of filter methods added to my controller. Here they are. (If you want to follow this technique, you’ll need to make sure your routes allow access to HTTP “:options” methods.)

before_filter :cors_preflight_check
after_filter :cors_set_access_control_headers

# For all responses in this controller, return the CORS access control headers.

def cors_set_access_control_headers
  headers['Access-Control-Allow-Origin'] = '*'
  headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS'
  headers['Access-Control-Max-Age'] = "1728000"
end

# If this is a preflight OPTIONS request, then short-circuit the
# request, return only the necessary headers and return an empty
# text/plain.

def cors_preflight_check
  if request.method == :options
    headers['Access-Control-Allow-Origin'] = '*'
    headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS'
    headers['Access-Control-Allow-Headers'] = 'X-Requested-With, X-Prototype-Version'
    headers['Access-Control-Max-Age'] = '1728000'
    render :text => '', :content_type => 'text/plain'
  end
end

The before_filter, cors_preflight_check is the last in my filter chain: earlier filters check for allowed access. If the request is for the OPTIONS method, then it short circuits the request, includes the necessary headers and returns a blank text body.

The after_filter, cors_set_access_control_headers, is for all requests that are returned by this controller. It includes the CORS headers for everything else.

Summary

CORS is implemented in all of the popular browsers, but client-side access seems to vary between IE and Safari/Chrome/Firefox. For me, it was interesting to see how server-side access control is not too different from what Adobe does for server-side policy files. It would be nice if these access controls could be unified, but I’m just happy to have them.

This entry was posted in Uncategorized and tagged , , , , , , , . Bookmark the permalink.

32 Responses to Cross-Origin Resource Sharing for JSON and RAILS

  1. Eran Davidov says:

    You’re a lifesaver! I’ve spent the last day trying to get a simple javascript call to talk to my rails server. Since it’s served from a file and not the server itself, it seems to be hitting the browser’s same-origin constraints (even though it shouldn’t). I was finally reading up on CORS and this solved the problem.

    A couple of comments:
    - There’s an extra space between “:” and “options” which should be :options
    - Would it still be valid to restrict the after_filter to only add those fields in OPTIONS requests, not in all requests?

  2. admin says:

    Glad it helped! I wrote up what I learned hoping it would help someone else someday.

    Interesting how you stumbled upon the CORS machinery of your browser. Indeed, Javascript executing on a page served from the filesystem making an AJAX call to a server *is* a cross-origin request! I had a similar revelation.

    - I fixed the :options spacing. Thanks.

    - I believe the after_filter needs to apply the headers to both the OPTIONS (pre-flight request) and the corresponding GET or POST (the “actual” request that follows the pre-flight request). At least, that’s my best understanding.

  3. Hey there,
    thanks a lot for your article! But since I’m a Rails beginner I wonder how I actual manage this: “If you want to follow this technique, you’ll need to make sure your routes allow access to HTTP “:options” methods.”

    Google doesn’t find anything useful, so it would be great if you could give me a direction where I can look :-)

  4. Tom Sheffler says:

    Hi David -

    It seems that quite a number of people end up at this article looking for information on the OPTIONS method and Rails routing. If you’re using Rails’ ReST-ful routes, then the only methods allowed are GET, POST, PUT and DELETE. If you don’t use ReST-ful routes, then you have the flexibility to respond to any method.

    I don’t have an article about this subject myself. Perhaps someone can post a good link here about how they allowed the OPTIONS method in the Rails routes.

  5. Hey Tom,

    I was able to find a way to do it with ReST-ful routes, it’s a very dirty workaround but currently the only thing working for me. (My RoR mentor suggested to write a little piece of rack middleware to pick out the OPTIONS requests)

    I added following to the end of my routes.rb
    controller :tracks do
    match ‘tracks’ => :options_response
    end

    This works because rails obviously checks the routes from top to bottom, and if nothing matches the last rule, matching everything, does the job. Dirty, but well, atleast it works!

  6. Tom Sheffler says:

    Thanks for posting this update here.
    -T

  7. Chris Lee says:

    I’m using Rails 3, and I was able to add the code snippet above to the “app/controllers/application_controller.rb” super class in order to add the customer headers.

    I have no idea what “If you want to follow this technique, you’ll need to make sure your routes allow access to HTTP “:options” methods.” means, as I’m new to Rails, but for me it was as easy cake.

    Nice work either way Tom!

  8. Thanks Tom!! Great post! I was able to use this in getting a Rails app (port 3000) to talk to a sinatra app (port 9292), thanks again!

    The only thing I modified was I added X-CSRF-Token to the headers['Access-Control-Allow-Headers'], so mine looks like this:

    headers['Access-Control-Allow-Headers'] = 'X-Requested-With, X-Prototype-Version, X-CSRF-Token'

  9. Tom Sheffler says:

    Here is a pointer to a follow-up post about CORS with Rails3. This article shows how to write the routes.rb entries, and shows a technique for allowing or disallowing access.

    http://codeodor.com/index.cfm/2011/7/26/Responding-to-the-OPTIONS-HTTP-method-request-in-Rails-Getting-around-the-Same-Origin-Policy/3387

    Thanks to Sam for writing up these details.

  10. Daryl says:

    Hi Tom,

    Thanks for your article.. Im trying to get my GWT application to interact with my Rails backend but am facing SOP issues. I found your great article that has helped me perform DELETE requests successfully… (havent tested Post)

    But there seems to be 2 issues im facing.

    1) Even when the delete completes successfully, the response code i get in my Google Web Toolkit application is zero and i dont know why… it should give a successful code (302 in my case). is there a reason why the 0 status code comes through? Im using request builder to make the request…

    2) I cant seem to use the GET method. It appears that when i make a GET request to rails, rails doesnt see it as an initial OPTIONS request and straight tries to use GET which obviously fails due to SOP.

    Do you have any idea whats going wrong?
    Thanks

  11. Roy Kolak says:

    Really great post. This is definitely the clearest text that I have read about preflight requests and CORS. Thanks!

  12. tomkersten says:

    I know the intention of this post is to outline how to do it within Rails, but if you have control over your web server and want to enable this across your entire app (not a specific action/controller, etc), you can handle it there instead. We set it up in nginx with the following:

    location / {
    # For CORS
    if ($request_method = OPTIONS ) {
    add_header Access-Control-Allow-Origin “http://localhost”;
    add_header Access-Control-Allow-Methods “GET, OPTIONS”;
    add_header Access-Control-Allow-Headers “Authorization”;
    add_header Access-Control-Allow-Credentials “true”;
    add_header Content-Length 0;
    add_header Content-Type text/plain;
    return 200;
    }
    …other awesome nginx stuff here
    }

    So, this will allow GET or OPTIONS requests from only http://localhost…(we needed the Authorization/Credentials stuff b/c we were doing Basic Auth in our app as well).

    This removes all the code changes in your Rails app, but also exposes the entire application…which may or may not be what you want. Anyway, just another way to consider.

  13. Tom Sheffler says:

    Thanks for contributing this tidbit.

  14. tomkersten says:

    Sure! Thanks for the write-up, it helped us understand what was going on. Also, I made a gist of a complete nginx host config in case people are unsure of how the snippet above “fits in” to the nginx setup. You can find it here: https://gist.github.com/1323477.

  15. Kevin says:

    Thanks for this.

    Unfortunately the link to the follow-up article is no longer active. The new link is http://www.codeodor.com/index.cfm/2011/7/26/Responding-to-the-OPTIONS-HTTP-method-request-in-Rails-Getting-around-the-Same-Origin-Policy/3387

    (Subdomain configuration issue).

  16. Thanks for this.

    For the guys using RESTful Rails routes, I got past this by placing a catch-all with a method contraint at the end which redirected to an empty method, which just rendered an empty string.

    In my routes.rb

    match "*path" => "api#xss_options_request", :constraints => {:method => "OPTIONS"}

    And in the APIController

    def xss_options_request
    render :text => ""
    end

  17. From Spine js documentation

    CORs Rails integration

    Let’s create a cor method, which will add some of the request access control headers to the request’s response.

    Add the following to app/application_controller.rb:


    before_filter :cor

    def cor
    headers["Access-Control-Allow-Origin"] = "js-app-origin.com"
    headers["Access-Control-Allow-Methods"] = %w{GET POST PUT DELETE}.join(",")
    headers["Access-Control-Allow-Headers"] = %w{Origin Accept Content-Type X-Requested-With X-CSRF-Token}.join(",")
    head(:ok) if request.request_method == "OPTIONS"
    end

    Although Access-Control-Allow-Origin takes a wildcard, I highly recommend not using it as it opens up your app to all sorts of CSRF attacks. Using a whitelist is much better and more secure.

    The Access-Control-Allow-Headers section is important, especially the X-Requested-With header. Rails doesn’t like it if you send Ajax requests to it without this header, and ignores the request’s Accept header, returning HTML when it should in fact return JSON.

    It’s worth noting that jQuery doesn’t add this header to cross domain requests by default. This is an issue that Spine solves internally, but if you’re using plain jQuery for CORs, you’ll need to specify the header manually.


    jQuery.ajaxSetup({
    headers: {"X-Requested-With": "XMLHttpRequest"}
    });

    Some browsers send an options request to the server first, to make sure the correct access headers are set. You’ll need to catch this in Rails, returning a 200 status with the correct headers. To do this, add the following to your application’s config/routes.rb file:


    match '*all' => 'application#cor', :constraints => {:method => 'OPTIONS'}

    That’s it, you’re all set up for Cross Origin Requests with Spine!

  18. Fayaz says:

    I want to ask one question… i am developing an application which involve JavaScript as client side and node js as server side… i am calling cross domain ajax call with the method ‘DELETE’ but which is not working, its request the Preflight and then a GET request.. is there any way on JavaScript side to send delete request with jsonp datatype …. if not and what shall i need to do on server side (nodejs)
    my sample code


    $.ajax({
    type:"DELETE",
    url: "http://172.16.16.34:3000/" +id +"/design_details.json",
    timeout:20000,
    contentType:"application/json; charset=utf-8",
    dataType:"jsonp",
    success:function (result) {
    alert("deleted");
    }

    });

  19. Tom Sheffler says:

    This discussion thread concerns itself mostly with Rails, and not NodeJS. That being said, I don’t think that “jsonp” is an option for what you are trying to do. Good luck!

  20. Pingback: Rails and cross origin resource sharing (CORS)

  21. Pingback: CORS in rails controller in before_filter or after_filter?

  22. rohitrox says:

    I tried the solution… get request works but post still suffers from that Allow-access-allow-origin
    I can see

    Incoming Headers:
    Origin: http://localhost
    Access-Control-equest-Method:
    Access-Control-Request-Headers:

    in unicorn_err log

    Any idea..

  23. rohitrox says:

    I am seeing this in chrome console
    Origin http://localhost is not allowed by Access-Control-Allow-Origin.

  24. Tom Sheffler says:

    Your note shows “Access-Control-equest-Method”

    Is it really mis-spelled? That is a problem.

  25. rohitrox says:

    My bad …
    Actually I am seeing

    Origin http://localhost is not allowed by Access-Control-Allow-Origin.

    in chrome when I do post request to my remote app.

    and in unicorn_err.log

    Incoming Headers:
    Origin: http://localhost
    Access-Control-Request-Method:
    Access-Control-Request-Headers:

    appears….are the correct headers not set !!

  26. Hmm is anyone else experiencing problems with the pictures
    on this blog loading? I’m trying to find out if its a problem on my end or if it’s the blog.
    Any responses would be greatly appreciated.

  27. Pingback: CORS issue with Rails 3 consuming mongodb rest api | BlogoSfera

  28. Tom Sheffler says:

    My original post was on Rails2. There have been more recent posts regarding Rails3. You can read some above.

  29. Pingback: Acessos de outros domínios (Cross-Origin Resource, Cross-Site) via AJAX e Rails 3 (parte 1) | Arich BLOG

  30. Andrej Antas says:

    Literaly saved my development life :)

    thanks a lot man :)

  31. Tom Sheffler says:

    I’m glad it helped :-)

  32. Bunthoeun Pech says:

    Forgive my bad english,

    I’m currently using Rails4 as API and ExtJS as front-end framework(rest proxy).

    I follow your instruction and work like charm with GET record from server, but it’s seem i missed something since i use other method like POST, PUT or DESTROY, it doesn’t send the actual request after get respond code 200.

    ExtJS store:
    proxy: {
    type: ‘rest’,
    noCache: false,
    reader: {
    type: ‘json’,
    root: ‘sections’,
    successProperty: ‘success’
    },
    writer: {
    type: ‘json’
    },
    api: {
    read: ‘http://localhost:3000/sections’,
    create: ‘http://localhost:3000/sections’,
    update: ‘http://localhost:3000/sections’,
    destroy: ‘http://localhost:3000/sections’
    }
    }

    Rails server respond:

    Started OPTIONS “/sections?_dc=1394515993496&page=1&start=0&limit=25″ for 127.0.0.1 at 2014-03-11 12:33:13 +0700
    ActiveRecord::SchemaMigration Load (0.3ms) SELECT `schema_migrations`.* FROM `schema_migrations`
    Processing by ApplicationController#cors_preflight_check as */*
    Parameters: {“_dc”=>”1394515993496″, “page”=>”1″, “start”=>”0″, “limit”=>”25″, “all”=>”sections”}
    Rendered text template (0.1ms)
    Filter chain halted as :cors_preflight_check rendered or redirected
    Completed 200 OK in 78ms (Views: 17.6ms | ActiveRecord: 0.0ms)

    Started GET “/sections?_dc=1394515993496&page=1&start=0&limit=25″ for 127.0.0.1 at 2014-03-11 12:33:14 +0700
    Processing by SectionsController#index as */*
    Parameters: {“_dc”=>”1394515993496″, “page”=>”1″, “start”=>”0″, “limit”=>”25″}
    Section Load (0.0ms) SELECT A.*,B.name AS Page FROM sections AS A LEFT JOIN pages AS B ON A.page_id=B.id
    Completed 200 OK in 112ms (Views: 6.3ms | ActiveRecord: 5.6ms)

    Started OPTIONS “/sections/3?_dc=1394516006492″ for 127.0.0.1 at 2014-03-11 12:33:26 +0700
    Processing by ApplicationController#cors_preflight_check as */*
    Parameters: {“_dc”=>”1394516006492″, “all”=>”sections/3″}
    Rendered text template (0.0ms)
    Filter chain halted as :cors_preflight_check rendered or redirected
    Completed 200 OK in 6ms (Views: 5.5ms | ActiveRecord: 0.0ms)

    Any Idea will appreciate

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>