I am trying to make rails web app along with rails API for mobile app. For this purpose I am using Devise along with Devise token auth.
I configured routes as it is written in Devise token auth gem so as I could have routes for regular Devise and Devise auth token.
I have 2 problems:
When I add include DeviseTokenAuth::Concerns::SetUserByToken to application_controller it overwrites Devise authenticate_user! and on web side I am being aunthenticated with token.
Possible solution: I created separet ApiApplicationController from which API controllers inherit.
class ApiApplicationController < ActionController::Base
include DeviseTokenAuth::Concerns::SetUserByToken
protect_from_forgery with: :null_session
end
For each POST request which I do in curl to my API I need to add CSRF token.
Possible solution: I could add to both ApplictionController and ApiApplicationController if: Proc.new { |c| c.request.format == 'application/json' } after protect_from_forgery with: :null_session
I used to get the same problem to yours, my solution which is currently working:
# application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :null_session, if: ->{request.format.json?}
end
# api_application_controller.rb
class ApiApplicationController < ActionController::Base
include DeviseTokenAuth::Concerns::SetUserByToken
before_action :authenticate_user!
end
Related
I am using existing rails application, where we currently use devise for authentication and Pundit for authorization. My requirement is to skip all policies scope authorization for all action in this existing rails application. How Can I do this?
I have tried below code but not worked:-
class ApplicationController < ActionController::Base
include Pundit
protect_from_forgery with: :exception
before_action :authenticate_user!, :set_default_timezone#, :modify_search_params
before_action :set_current_user
skip_after_action :verify_policy_scoped
#.....
end
Thanks in advance :)
Pundit provides skip_authorization to skip his authorization. Executing it before all actions of the controller will make it work for your requirement.
class ApplicationController < ActionController::Base
# ...
before_action :skip_all_authorization
private
def skip_all_authorization
skip_authorization
end
# ...
end
You need to skip both action authorization with an object (which are called by using authorize(object) and with a policy scope (called with policy_scope).
You can skip the hooks on your base class:
skip_after_action :verify_policy_scoped
skip_after_action :verify_authorized
Or just add another hook to skip them on your controller (my preferred approach)
after_action :skip_all_authorization
private
def skip_all_authorization
skip_policy_scope
skip_authorization
end
But by the way, you shouldn't need this unless you're ensuring the policy is called by adding the appropriate hooks.
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.
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
I am using devise to sign up/in.
routes
get 'profile' => 'profile#get_profile'
post 'profile' => 'profile#create_profile'
and profile_controller
def get_profile
render json: {user: current_user}, status: :ok
end
def create_profile
render json: {user: current_user}, status: :ok
end
GET: http://localhost:3000/user/profile returns the expected output. However,
POST request throws an error saying:
ActionController::InvalidAuthenticityToken in User::ProfileController#create_profile.
Please demystify this behavior.
To disable CSRF protection you can edit your ApplicationControllerlike this:
class ApplicationController < ActionController::Base
protect_from_forgery with: :null_session
# ...
end
or disable the CSRF protection for specific controller:
class ProfilesController < ApplicationController
skip_before_action :verify_authenticity_token
# ...
end
:null_session strategy empties the session instead of raising an exception which is perfect for an API. Because the session is empty, you can't use current_user method or othes helpers that refer to the session.
IMPORTANT:
protect_from_forgery with: :null_session must be used only in specific
cases, for example to allow API request (POST/PUT/PATCH/DELETE) without html form
With protect_from_forgery with: :null_session you must restrict access to your data with an authorization system because every one could do request against your API endpoint
Don't remove protect_from_forgery with: :exception for requests that are done through html form, is dangerous! (read here http://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf)
To handle both standard requests (through html form) and API requests generally you have to set up two different controller for the same resource. Example:
Routes
Rails.application.routes.draw do
resources :profiles
namespace :api do
namespace :v1 do
resources :profiles
end
end
end
ApplicationController
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
end
ProfilesController
(standard controller for html requests)
# app/controllers/profiles_controller.rb
class ProfilesController < ApplicationController
# POST yoursites.com/profiles
def create
end
end
Api::V1::ProfilesController
(controller for API requests)
# app/controllers/api/v1/profiles_controller.rb
module Api
module V1
class ProfilesController < ApplicationController
# To allow only json request
protect_from_forgery with: :null_session, if: Proc.new {|c| c.request.format.json? }
# POST yoursites.com/api/v1/profiles
def create
end
end
end
end
refereces:
http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection/ClassMethods.html#method-i-protect_from_forgery
Get requests don't have an authenticity token.
You will have to add the request forgery stuff to your forms using this
<%= csrf_meta_tag %>
And address via javascript
$('meta[name="csrf-token"]')
In ApplicationController (or another controller your controllers inherit from) there's a line:
protect_from_forgery with: :exception
Remove it and CSRF checks will be disabled.
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