Reputation: 8240
I have a Rails API that's authenticated with an http-only cookie, and as such I require CSRF protection. From what I can tell, the Rails community seems to prefer storing jwt auth tokens in local storage rather than in a cookie. This avoids the need for CSRF but exposes you to XSS, which is why we chose to use cookies + csrf.
It seems that CSRF protection is disabled by default due to the community preference for local storage. I am trying to enable it with limited success. Here is how I'm attempting to handle it:
module V1
class ApplicationController < ::ApplicationController
include Concerns::Authentication
include ActionController::RequestForgeryProtection
protect_from_forgery
protected
def handle_unverified_request
raise 'Invalid CSRF token'
end
after_action :set_csrf_cookie
def set_csrf_cookie
if current_user
cookies['X-CSRF-Token'] = form_authenticity_token
end
end
end
end
On the client side, I can see that the token comes back in the cookie. When I make a request, I also see that the token is present in the X-CSRF-Token
header. All looks well so far.
However, the verified_request?
method returns false, so handle_unverified_request
gets invoked. Stepping through the Rails code, I see that my token is present in request.x_csrf_token
, but the token appears to fail verification when it's checked against the session
. One thing I'm wondering here is if I need to enable something to get the session
to work correctly, as I understand that session management isn't turned on be default in API mode. However, if that were the case I would sort of expect attempts to access the session
object to blow up, and they don't, so I'm not sure.
Have I made an error, or is there some other middleware I need to turn on? Or do I need a different approach altogether to enable CSRF with this scheme?
Upvotes: 4
Views: 3240
Reputation: 8240
I realize that this was a case of overthinking the problem. I really don't need Rails's forgery protection to do anything for me, or to check the value against the session
, because the value of my token is already a cookie. Here's how I solved it:
First, the base controller sets the csrf cookie. This would be skipped for logout or any public endpoints, if there were any.
module V1
class ApplicationController < ::ApplicationController
include Concerns::Authentication
include ActionController::RequestForgeryProtection
after_action :set_csrf_cookie
protected
def set_csrf_cookie
if current_user
cookies['X-CSRF-Token'] = form_authenticity_token
end
end
end
end
Then my authenticated endpoints inherit from an AuthenticatedController
that checks the auth token and the csrf token:
module V1
class AuthenticatedController < ApplicationController
before_action :authenticate!
def authenticate!
raise AuthenticationRequired unless current_user && csrf_token_valid?
end
rescue_from AuthenticationRequired do |e|
render json: { message: 'Authentication Required', code: :authentication_required }, status: 403
end
rescue_from AuthTokenExpired do |e|
render json: { message: 'Session Expired', code: :session_expired }, status: 403
end
private
def csrf_token_valid?
Rails.env != 'production' || request.headers['X-CSRF-Token'] === cookies['X-CSRF-Token']
end
end
end
Hope this helps someone else trying to use CSRF + cookies in a Rails 5 API!
Upvotes: 7