Wim Haanstra
Wim Haanstra

Reputation: 5998

Protect API with CSRF token or other authentication

In a Rails project I am setting up my own API, which I would like to call from the same site with some jQuery calls.

For example, I have a route POST /api/v1/employee.

I want this API call to be available for users of my site, but also for an external client that is calling this API with (for example) basic authentication.

So I would like to protect my API against CSRF when the call is coming from my own site and when it is from an external client I want it to check for the username and password in the header of the request.

I set up jQuery to add the csrf token specified in the meta tags of my page, to the every request it does to the API.

$.ajaxSetup({
      beforeSend: function(xhr) {
          xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'));
      }
});

But now I am not sure where to go from here.

In my controller I added the following line that enabled my client to connect to the API without problems, but then I have no protection.

protect_from_forgery with: :null_session, if: Proc.new { |c| c.request.format == 'application/json' }

If I set up a before_filter for this controller it always checks for the username and password, even when a csrf token is available.

before_filter :authentication_check

^ authentication_check is a method I created which does a simple basic authentication check (just to get this going).

I also tried to do the following:

protect_from_forgery with: :exception, :unless => :valid_user?

But this probably doesn't do what I think. valid_user? is a method that checks the basic authentication and returns the user if correct.

I somehow want to combine these 2 things. If there is no session (so it's and external client connecting) I want to authenticate on username/password and otherwise just use the normal CSRF protection.

UPDATE November 25th 2015

Ok I now have the following solution for POST requests. Still need to add GET, etc, but here it is.

In my controller I have this:

protect_from_forgery with: :exception, unless: :valid_apikey

My :valid_apikey thing method is defined like this:

def valid_apikey

    result = authenticate_or_request_with_http_token do |token, options|
        !User.find_by_token(token).nil?
    end
    result == true

end

So, if I am correct, the CSRF checking is skipped when the request supplies a token that is in my database. When the token is not in my database the authenticate_or_request_with_http_token method returns an Array (which is different than true).

Upvotes: 0

Views: 760

Answers (2)

Wim Haanstra
Wim Haanstra

Reputation: 5998

Ok, got this solved for I think. I also want my GET requests to check for the CSRF token.

In my controller I do the following:

def verified_request?
    if !request.headers["X-API-KEY"].nil?
        !ApiClient.find_by_token(request.headers["X-API-KEY"]).nil?
    else
        !protect_against_forgery? ||
            valid_authenticity_token?(session, params[request_forgery_protection_token]) ||
            valid_authenticity_token?(session, request.headers['X-CSRF-Token'])
    end
end

This overrides the verified_request? method of the inherited ApplicationController. I can't just call super, because then my GET requests will not get checked.

Upvotes: 0

Harry Bomrah
Harry Bomrah

Reputation: 1668

I suggest you dont have to put an exception for your protect_from_forgery

You can simply do this

class ApplicationController < ActionController::Base
  protect_from_forgery

  def handle_unverified_request
    # your logic to handle request coming from outside your domain
    # if user is not valid then
    raise(ActionController::InvalidAuthenticityToken)
    # else allow to bypass
  end 

This way u can control and also for normal requests from your domain, you dont ve to go through if conditions and hitting db everytime.

Upvotes: 1

Related Questions