Override the protect_from_forgery strategy in a controller - ruby-on-rails

I want to build a rails app with two different protect_from_forgery strategies: one for the web application, and one for the API.
In my application controller I have this line of code: protect_from_forgery with: :exception in order to prevent CSRF attacks, it works just fine.
In my API namespace, I created an api_controller that inherits from my application controller, and that is the parent class of all the other controllers in the API namespace, and I changed the code above with: protect_from_forgery with: :null_session.
Sadly, I have an error when trying to make POST request: "Can't verify CSRF token authenticity".
I don't want to skip the verify_authenticity_token method in my API controllers, I just want to have two distinct strategies in my app, so how do I override the protect_from_forgery strategy defined in my application controller ?
Edit: Ok, so I eventually did what I did not want to do in the first place: change the inheritance of my api_controller: it now inherits from ActionController::Base, and no longer from my application controller. It does work now but:
It does not answer my question i.e. overriding the protect_from_forgery strategy.
It is not DRY as I have to copy/past what was previously in my application_controller.
So if anyone has a real way to overwrite this method, I'd appreciate it.

What if you leave the protect_from_forgery with: :exception in the application controller but then you put the following in your API controller?
skip_before_action :protect_from_forgery
protect_from_forgery with: :null_session
That way, you still get the standard CSRF attack protection for all controllers in your web application but you also get the null session behavior for your API methods.

I am running an application with a similar structure - Web App + API. I solved the CSRF problem like this:
Apply protect_from_forgery only for non API requests
My API endpoint is api.example.com, so I used subdomain constraint to distinguish API and web app requests
Code:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception, if: :isWebRequest?
def isWebRequest?
request.subdomains[-1] != 'api'
end
end

Late to the party, but something like this can be done:
class YourCustomStrategy
def initialize(controller)
end
def handle_request
end
end
And in your ApplicationController or where you want:
class ApplicationController < ActionController::Base
protect_from_forgery with: YourCustomStrategy
end

Related

Why am I getting the CSRF token error when I skipped the action in a Rails application?

I have a Rails application where I'm using Vue (through webpacker) in some parts of the frontend.
From Vue I'm making a call to my server which needs to access the current_user (Devise), however, I'm getting the Can't verify CSRF token authenticity. error and the current user is not returned.
To avoid this I skipped the before_action like so:
skip_before_action :verify_authenticity_token, only: :stripe_vue
def stripe_vue
...
end
However, I'm still getting exactly the same issue.
It's kind of weird as I'm doing exactly the same thing in another controller and it works like charm.
Comment this line
protect_from_forgery with: :exception
and final code should look like this
skip_before_action :verify_authenticity_token
#protect_from_forgery with: :exception

Shouldn't protect_from_forgery with: :exception raise an error if I don't include an authenticity token?

It's my understanding that protect_from_forgery with: :exception which is the default in Rails, will cause an error if forms are submitted and they don't have the authenticity_token input.
However it seems like this is not the case anymore. This is a Rails 5 app, and I've mostly done Rails 4 in the past, so I wonder if something has changed.
In application_controller.rb I have protect_from_forgery with: :exception
My form is like this (using slim)
form#spreadsheet-form{
action='/submit_spreadsheet'
}
textarea.spreadsheet-input{
name='instructions'
style="width: 200px; height: 200px"
}
br
input.spreadsheet-submit{
type="submit"
value="submit"
}
The main issue in my eyes is why this doesn't raise an error. In the past I've had to include a hidden input with form_authencicity_token as the value.
I believe it is something to do with the Rails 5 changed the protect_from_forgery execution order. From this blog
What
If we generate a brand new Rails application in Rails 4.x then application_controller will look like this.
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
Looking it at the code it does not look like protect_from_forgery is a before_action call but in reality that’s what it is. Since protect_from_forgery is a before_action call it should follow the order of how other before_action are executed. But this one is special in the sense that protect_from_forgery is executed first in the series of before_action no matter where protect_from_forgery is mentioned. Let’s see an example.
class ApplicationController < ActionController::Base
before_action :load_user
protect_from_forgery with: :exception
end
In the above case even though protect_from_forgery call is made after load_user, the protection execution happens first. And we can’t do anything about it. We can’t pass any option to stop Rails from doing this.
Rails 5 changes this behavior by introducing a boolean option called prepend. Default value of this option is false. What it means is, now protect_from_forgery gets executed in order of call. Of course, this can be overridden by passing prepend: true as shown below and now protection call will happen first just like Rails 4.x.
class ApplicationController < ActionController::Base
before_action :load_user
protect_from_forgery with: :exception, prepend: true
end
Why
There isn’t any real advantage in forcing protect_from_forgery to be the first filter in the chain of filters to be executed. On the flip side, there are cases where output of other before_action should decide the execution of protect_from_forgery. Let’s see an example.
class ApplicationController < ActionController::Base
before_action :authenticate
protect_from_forgery unless: -> { #authenticated_by.oauth? }
private
def authenticate
if oauth_request?
# authenticate with oauth
#authenticated_by = 'oauth'.inquiry
else
# authenticate with cookies
#authenticated_by = 'cookie'.inquiry
end
end
end
Above code would fail in Rails 4.x, as protect_from_forgery, though called after :authenticate, actually gets executed before it. Due to which we would not have #authenticated_by set properly.
Whereas in Rails 5, protect_from_forgery gets executed after :authenticate and gets skipped if authentication is oauth
Is the authenticity token present on your form when you inspect it? Which point is protect_from_forgery inserted in your application?
Rails 4=>5 changed the default behaviour to be inserted wherever in the chain it is called, opposed to the previous where it was called first. If you want it to be called before any other action, try using the prepend: true flag.

verify_authenticity_token has not been defined

Ruby version 2.2.4, Rails version 5.0.0.1.
I'm getting stuck at a part of a tutorial where you test login with curl. I get an error
ArgumentError (Before process_action callback: verify_authenticity_token has not been defined).
I used this code in sessions_controller:
skip_before_action :verify_authenticity_token, :if => Proc.new { |c| c.request.format == 'application/json' }
Does somebody know the answer?
Check if your ApplicationController has a call to protect_from_forgery as follows:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
Essentially calling protect_from_forgery adds verify_authenticity_token to the before_filter list, which you can skip in the other controllers.
Refer to protect_from_forgery documentation for more info.
After upgrading to Rails 5, I hit this error. I discovered that if skip_before_action is called multiple times with the same method name, this exception will be raised.
My fix was to add the parameter raise: false, to the second time skip_before_filter was called.
So it would look like this in the Application Controller:
skip_before_action :your_method_name, raise: false
After upgrading to Rails 5 and deploying to Heroku I hit this error. I could not reproduce it in development.
In the API docs the example is given
class ApplicationController < ActionController::Base
protect_from_forgery unless: -> { request.format.json? }
end
For me, adding the unless: -> { request.format.json? } like the above fixed my stack bomb. raise: false unfortunately did not.
I fixed it by setting:
config.action_controller.default_protect_from_forgery = true in config/application.rb
Just ran into a similar issue and none of these solutions worked for me.
Background:
I was re-naming models and controllers to keep the code DRY, I reviewed the files and found that I missed re-naming one of the controllers i.e. the class name was updated but not the file name.
Solution:
Update the controller name to match the new class name.
Moral of the story:
Cross to 't's and dot you 'i's.

Rails 5 devise_token_auth Can't verify CSRF token authenticity

I am working on a Rails 5 api project which is used by mobile client with gem devise_token_auth for authorization.
I am clear about what the warning means.
1st Question: CSRF protect should be turned OFF for api(JSON/XML)respond, correct?
I searched some on web it seems CSRF just happens on web application with cookie. But i read this from rails api document:
It's important to remember that XML or JSON requests are also affected >and if you're building an API you should change forgery protection >method in ApplicationController (by default: :exception):
class ApplicationController < ActionController::Base
protect_from_forgery unless: -> { request.format.json? }
end
So i still get the warning by adding like this:
class ApplicationController < ActionController::Base
protect_from_forgery unless: -> { request.format.json? }
include DeviseTokenAuth::Concerns::SetUserByToken
end
2nd Question: If API doesn't need CSRF protection, why
protect_from_forgery unless: -> { request.format.json? }
doesn't work?
Not sure if i understood something wrong. Thank you!
the code should be:
protect_from_forgery with: :null_session, if: ->{request.format.json?}
You might have to use null_session for API, it provides an empty session during request but doesn't reset it completely. Used as default if :with option is not specified.

Rails: What does exception and null_session mean in protect_from_forgery

I'm trying to implement token-based API and saw these snippets by google
However, it's hard to understand the meaning by the literal meaning.
Any direction or basic knowledge about this, Thanks ~~
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception, if: Proc.new { |c| c.request.format != 'application/json' }
protect_from_forgery with: :null_session, if: Proc.new { |c| c.request.format == 'application/json' }
end
Rails's document about null_session is here http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection/ProtectionMethods/NullSession.html#method-i-handle_unverified_request, if you check the source code of it's handle_unverified_request method:
def handle_unverified_request
request = #controller.request
request.session = NullSessionHash.new(request.env)
request.env['action_dispatch.request.flash_hash'] = nil
request.env['rack.session.options'] = { skip: true }
request.env['action_dispatch.cookies'] = NullCookieJar.build(request)
end
that means if the request dose't get through the verify_authenticity_token ,then rails will not fetch the session by cookie data, but create a new session for this request. And that session is a instance of NullSessionHash, so "null_session".
I had this concern when working on a Rails 6 API only application.
By default, Rails applies Cross-Site Request Forgery (CSRF) protection to all controllers that subclass from ApplicationController using the following line:
protect_from_forgery with: :exception
But are there instances where you’d want to respond differently, or even ignore CSRF protection altogether? The answer, of course, is “yes”.
If your project or a portion of your project uses an alternative method for authentication such as API tokens or any other “stateless” authentication, then you can safely remove the protect_from_forgery line from whatever base class those controllers inherit.
On the other hand, if your project uses stateful authentication and APIs, such as those projects with lots of AJAX requests, it can be advantageous to use :null_session with protect_from_forgery like so:
protect_from_forgery with: :null_session
That means the user won’t be logged in anymore for that action and can’t perform the change (if the action requires a signed-in user). However, after the action, the session values will be back and the session ID will be the same, so the user will be logged in.
Rather than throwing an exception, which your JavaScript may not be able to handle, it instead sets the session value to nil for the duration of the action. By doing this, any authorization or action scoped to the current user will result in errors that your JavaScript can more easily work with.
So, if you're working on a Rails API only application, you can use this to protect against Cross-Site Request Forgery (CSRF) attacks. This suffices for both cases when either of them arises:
class ApplicationController < ActionController::API
include ActionController::RequestForgeryProtection
if Proc.new { |c| c.request.format != 'application/json' }
protect_from_forgery with: :exception
end
if Proc.new { |c| c.request.format == 'application/json' }
protect_from_forgery with: :null_session
end
end
This implements protect_from_forgery with: :exception when your requests are not of the application/json format, and protect_from_forgery with: :null_session when your requests are of the application/json format.
Note: protect_from_forgery is a class method included in ActionController::RequestForgeryProtection, which is why include ActionController::RequestForgeryProtection was included.
Resources:
CSRF Protection and Ruby on Rails
Undefined method protect_from_forgery for Clearance::SessionsController:Class
That's all.
I hope this helps

Resources