request.xhr CORS Layout Fail - ruby-on-rails

If I send a remote: true request to our subdomain controller, our layout renders (which it shouldn't)
Having tested the request.xhr? method in the action, it's returning nil (not the true / false) that you'd expect. This works for non-CORS ajax. It only stops working with CORS-ajax (for subdomain)
Here's the code:
#app/views/controller/view.html.haml
= link_to "test", new_user_session_url, remote: :true
#routes
new_user_session_path GET /login(.:format) admin/users/sessions#new {:subdomain=>"test"}
The response occurs & we get a layout
We want no layout, which works without CORS:
#app/controllers/application_controller.rb
layout :layout_select
def layout_select
if request.xhr?
false
else
devise_controller? ? "admin" : "application"
end
end
We have CORS policy set up & working. However, it seems our CORS-request is not being treated as xhr. Any ideas?

In light of the comments from Mike Campbell, I was able to determine that CORS requests are not treated as ajax (xhr) by default
The problem lies in the headers which are passed on an Ajax request. Standard ajax passes the following header:
#actionpack/lib/action_dispatch/http/request.rb
def xhr?
#env['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/
end
This is passed with a "naked" Ajax request, which allows the xhr? method to return either true or false. The issue is in a CORS ajax call, this header is not passed. I don't know why, but it just sends an ORIGIN header instead
The proposed fix from Github suggested to include this:
uri.host != #env['HTTP_HOST'] || uri.scheme != #env['rack.url_scheme'] || uri.port != #env['HTTP_PORT'].to_i
This would basically return a boolean response based on whether the request was from the same domain as the script. Although this was prevented for being unsafe, it lead me to two fixes:
Append a new cors? method to the Rails core
Send the http_x_requested_with header through a CORS request
Considering we are just trying to access a subdomain with the request, we felt that editing the Rails core was slightly overkill, although right. Instead, we found that using the inbuilt headers argument in jquery would help:
$.ajaxSetup({ headers: {"X-Requested-With": "XMLHttpRequest"}});
Whilst this works, I am eager to hear about any security issues it may present. And plus, whether we can access json and other formats with it

It's because of the Same-Origin Policy. Please read carefully this article: HTTP access control (CORS).
The possible solutions are:
make simple request – instead of remote: true do usual form post or maybe $.getJSON() will work (not sure).
implement preflight request – you'll need to implement on your server response for OPTIONS request.
if you need to maintain cookies then you'll also need to write custom ajax code, eg. $.ajax({url: a_cross_domain_url, crossDomain: true, xhrFields: {withCredentials: true}}); and return some specific headers from the server.

Related

How to set custom header for all requests that go through OmniAuth?

This is mostly a thought exercise that I couldn't figure out how to solve. :)
Does anyone know how to set a custom request header for all requests that use the omniauth or omniauth-oauth2 gems?
I've tried something like the following, but I don't see the header in my developer tools in Chrome, for example.
OmniAuth.config.before_request_phase do |env|
env['MY_CUSTOM_HEADER'] = 'true'
end
What am I doing wrong here? What am I missing here?
Edit based on responses:
Why I need the custom header set doesn't really matter here -- as I mentioned at the top of the question: this is just a curiosity I had. The constraint is that I'd like to test the custom header on any OS or browser, so it's not sufficient to just play with developer tools in Chrome here as not all browsers have that capability on across all OSes.
What I'm trying to do is add a custom header to all outgoing OAuth requests coming out the gem. That's it.
In browser developer tools you can only see request headers that are sent by or response headers received by the browser. Any request headers set by load balancers, reverse proxies, middleware and so on - are only visible to next stage in request handling chain.
OmniAuth lives in middleware, so technically any request in your app uses it, unless some other middleware terminates request chain and renders some response.
Also "request" in omniauth terms is not http request, it's an auth phase, usually happens on /auth/:provider (also there're "options" and "callback" phases).
It's not clear why you need setting a request header in a middleware on a request that goes into your own application - when request is going to be handled by omniauth it will not hit your controllers, except for callback phase where request.env['omniauth.auth'] is going to be set.
Since you're mentioned developer tools - probably you want response headers on request phase, to set these you need to override rack response returned from request_phase in your strategy. But for oauth2 there's only a redirect (still possible to set headers, but even less sense).
So first you need to be exact on which headers you want to be set at which request/response, there're several of those. Simplified sequence:
An OAuth request handler cannot force a browser (or any similar user-agent) to disclose more information than specified in the HTTP protocol. Be glad of that: any other posture could lead to information leakage.
The only connection that Omniauth might make itself is that exchanging a code for an access/refresh token. That is specific to the strategy in question, but strategies have the opportunity to include arbitrary headers in their internal client. If you're writing a custom strategy that required a basic authentication header during the access token exchange, it might look like this:
require 'omniauth-oauth2'
module OmniAuth
module Strategies
class DemoStrategy < OmniAuth::Strategies::OAuth2
option :name, "demo"
option :client_options, {
site: 'https://api.example.org',
authorize_url: 'https://auth.example.org/oauth2/authorize',
token_url: 'https://auth.example.org/oauth2/token'
}
uid { raw_info['id'].to_s }
info do
{ email: raw_info['email'], image: raw_info['avatar_url'] }
end
extra do
{ raw_info: raw_info }
end
def raw_info
#raw_info ||= access_token.get('user').parsed
end
def build_access_token
options.token_params.merge!(headers: {
'Authorization' => special_auth_header
})
super
end
def basic_auth_header
"Basic " + Base64.strict_encode64("#{options[:demo_id]}:#{options[:demo_secret]}")
end
end
end
end
Here, build_access_token is overriding the superclass's standard constructor for the internal HTTP client, and injecting extra headers before handing it back up the stack. Internally that's handed off to the oauth2 gem, which in turn uses Faraday, so it's likely anything Faraday accepts is a valid option.
If you need additional information carried to the authentication server, it may be encoded in the redirect URL by the strategy. For example, the omniauth-google-oauth2 strategy is configurable to carry authentication scopes and email hints in the URL that lands on Google's authentication endpoint.
It is also common to include a XSRF state parameter, in conjunction with an encrypted session cookie, to protect against identity spoofing. Depending on the co-operation of the authentication server, some or all of this data may be reflected in the redirection back to your handler.
At it simplest, that is handled by the authorize_params method in the strategy subclass e.g.
def authorize_params
super.tap do |params|
params[:something] = 'my_extra_value'
end
end
However, the volume of code involved in setting up extended parameters may be quite substantial in practice. For a worked example of doing this with Omniauth I'd suggest taking a looking at the source code of the Google strategy, and again I'll draw your attention to the authorize_params method which is the entry point for this heavy lifting.
In the overall flow of things, those are the touch points where server-side code can actually influence matters. There's a fundamental expectation that the user's client/browser is participating by executing nothing but normal HTTPS request.

CSRF from JS library to Rails

My task is to write a feature in a javascript package that will be embedded in html sites for form validation. Validation will be done through API call to my server.
The question is how to transfer and validate CSRF token from JS to my Rails server.
I've tried doing this:
var token = function(xhr) {xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'))};
$.ajax({
url: 'http://localhost:3000/csrf-check',
type: 'POST',
beforeSend: token,
data: {
hey: 'hey'
},
success: function(response) {
console.log(response);
}
})
In my ValidatorController:
class ValidatorController < ApplicationController
protect_from_forgery
def csrf_check
if session[:_csrf_token]
render json: :ok
else
render json: :fail
end
end
end
How can I send CSRF token and validate it on Rails server?
First of all: I don't really understand why you'd want to do such a thing – see max' comment.
Either way, if you want to do some kind of manual check for the authenticity token, you need to disable Rails' own facilities for this particular endpoint and then use the logic behind it in your endpoint. While I haven't tested it, I'd assume something like this should work:
class CsrfTokenChecksController < ApplicationController
skip_forgery_protection
def show
head any_authenticity_token_valid? ? :ok : :bad_request
end
end
This uses the any_authenticity_token_valid? method (see https://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html#method-i-any_authenticity_token_valid-3F) which is used internally by Rails as part of its forgery protection filter.
Note: This method exists since Rails 5. If you're using Rails 4, you'd have to go with valid_authenticity_token?(session, request.headers['X-CSRF-Token']) instead. For Rails 3, it would be form_authenticity_token == request.headers['X-CSRF-Token'].
I would consider if CSRF protection is really even applicable in your case.
Rails CSRF protection serves to guard against cross site reference forgery where another page pretends to be your page by looking visually similar and fooling the user into performing a POST request to for example steal the cookies for a replay attack.
It only actually works in the case where Rails is actually rendering the HTML as it just gives a guarantee that the the request originated from the same server that rendered the form.
If you're making something cross site on purpose it just won't work or even fill any purpose. There are other mechanisms like API keys that can be used instead to verify that the request is legit.

Rails Rpec InvalidCrossOriginRequest

I have a test in rspec for a destroy that returns the following:
ActionController::InvalidCrossOriginRequest:
Security warning: an embedded <script> tag on another site requested protected JavaScript. If you know what you're doing, go ahead and disable forgery protection on this action to permit cross-origin JavaScript embedding.
I can run the destroy just fine thru the UI but when it gets tested I get the above warning. Why is that?
Is your destroy request supposed to be an AJAX request? If so, you can assign the xhr option to true.
get some_path, xhr: true

Is it possible to send javascript request using wiselinks?

By default rails (I am using rails 4) sends data-remote requests with the following header:
Accept:*/*;q=0.5, text/javascript, application/javascript, application/ecmascript, application/x-ecmascript
Later in rails code I respond to it using format.js.
I want to migrate to wiselinks, but all data-push requests try to render html.
Is it possible to have default rails behaviour with wiselinks?
I think that you don't unserstand correctly how Wiselinks work. If you use :data => { :remote => true }, then you simply don't need :data => { :push => true }.
Pushing should be used only if you need to update your URL (follow a link).
Ok, I rewritten my controllers to respond to html AJAX requsets instead of using JS requests.
This way I can use wiselinks and it works. Main reason I was using JS response is to set page title with javascript and some other details. But with wiselinks I can use special helper for that.
I think there may be a need in the future to implement some complex behaviour after AJAX request, which will require some JS execution on client side after the response. However this will be possible to implement with page:done event
Thank you all for the help!

"WARNING: Can't verify CSRF token authenticity" error - CORS with Devise and :token_authenticatable

I have a single page app that authenticates to another domain using CORS. All the requests are JSON requests.
My app can authenticates OK and can make GET requests OK. Authentication is using token_authenticatable. I.e. all requests append '?auth_token=whatever'
So, my actual problem is that when I try to do a PUT request I get a WARNING: Can't verify CSRF token authenticity message in the rails log as well as a CanCan::AccessDenied (You are not authorized to access this page.) exception.
Simply adding skip_before_filter :verify_authenticity_token to the rails controller fixes the issue.
Therefore I can only conclude that my ajax requests are sending an invalid or empty csrf_token.
I don't really understand how that can be, since I believe I am correctly sending the X-CSRF-Token header correctly with each ajax request.
Basically, my app authenticates and Devise sends back an auth_token and a csrf_token:
render :status => 200, :json => {
:auth_token => #user.authentication_token,
:csrf_token => form_authenticity_token
}
I then store those tokens in my ajax app, and using ajaxSend in jQuery, set it up so jQuery passes those tokens with each request:
initialize: ->
#bindTo $(document), 'ajaxSend', #appendTokensToRequest
appendTokensToRequest: (event, jqXHR, options) ->
if not #authToken? then return
if #csrfToken?
jqXHR.setRequestHeader 'X-CSRF-Token', #csrfToken
if options.type is 'POST'
options.data = options.data + (if options.data.match(/\=/) then '&' else '') +
$.param auth_token:#authToken
else
options.url = options.url + (if options.url.match(/\?/) then '&' else '?') +
$.param auth_token:#authToken
I can then see in the chrome network tab, that for each GET request the auth_token param is being sent, as well as the X-CSRF-Token header.
On PUT requests however it doesn't seem to be working though.
My theory is that CORS is stuffing things up. If you make a CORS request, your browser actually makes an additional OPTIONS request first just to check that you do have permission to access this resource.
I suspect that it is the OPTIONS request which is not passing the X-CSRF-Token header, thus rails immediately invalidates the csrf_token on the rails end. Then when jQuery makes the actual PUT request the csrf_token it passes is no longer valid.
Could this be the problem?
What can I do to prove that? Chrome doesn't seem to show me the OPTIONS requests in the network tab to help me debug the issue.
It's not a major issue, because I can just turn the CSRF stuff off. But I'd like to know why it's not working.
I think you'll need to handle the OPTIONS request, which should respond with the various headers that will allow the CORS request, IIRC they are the access-control-allow-method, access-control-allow-origin and access-control-allow-headers. Because the OPTIONS request is failing, the PUT request probably isn't occurring.
I just ran into the same issue. The problem is that the _session_id cookie cannot be sent in CORS. As a result, when Rails tries to verify the token, the session[:_csrf_token] is null and Rails generates a new one before comparison.
To solve the issue, you need to enable cookie sending in CORS. Here is the Mozilla Developer Network reference. Work is needed on both the server and client side to make it work.
Client
- Refer to your client technologies document.
Server
- Set the header Access-Control-Allow-Credentials to true (string) in the response to the preflight (HTTP OPTIONS) call.
In Rails every form submission need CSRF token authenticity.
It use to submit form securely.
The CSRF token(each time) will create newly in rails when we open our Application.
If the CSRF token not passing inside our controller this WARNING will show.
We need to pass this token in all form submissions.

Resources