Providing Unauthenticated API Access To An Authenticated/Authorized Controller In Rails 3, With Devise And CanCan


My current Ruby on Rails app defaults to every page and controller action in the system requiring authentication. If you’re not logged in, you don’t get to do anything other than see the login page. Once you are logged in, you have to be authorized to do anything. It’s a fairly tight security setup because we’re dealing with medical data and patient health care information… HIPAA and all that jazz…

Security: Devise And CanCan

To do the authentication, we’re using Devise. This makes it very easy to require all users be authenticated. We only needed to add this before filter to our ApplicationController:

before_filter :authenticate_account!

authenticate_account! is a controller helper method that Devise provides, and it ensures we have an authenticated user. If the user is not authenticated, they get sent back to the login screen.

To do the authorization, we’re using CanCan. This makes it very easy to provide authorization rules around resources as well as any other general rules we need. To secure an entire controller and the resource it represents, we only needed to add this to a controller that needs it:

authorize_resource

CanCan is smart enough to look at the controller name and figure out the resource to authorize against. You can also specify the class to authorize explicitly, etc… but that’s another blog post.

I Need A Publicly Available API To Return A JSON Document

I have a list of insurance companies in my app. It’s just a name, address and phone number for the insurance company so there is nothing sensitive in this information. I also have a need to provide this list to a few third parties, including another company and the end user’s browser via some JQuery calls.

My general route is /insurance_companies and I need to provide a search mechanism to return the companies in json format. This is easy enough with a route like this: /insurance_companies/search/(:search_term). This lets me hit “/insurance_companies/search/bcbs.json” and return all companies that contains the letters “bcbs” in a json document format… except that you have to be authenticated / authorized to do that search and get the results back.

Here’s the basic rules that I need to have in place:

  • If a user hits the search url without the .json extension, they must be authenticated / authorized to see the page that tries to render
  • if a request is made to the search url with the .json extension, no authentication / authorization should be done because this is just a search / json document being returned.

To allow this api to work, I would either have to provide a way for the caller to authenticate / authorize each request for each request, use a separate controller that didn’t require the authentication / authorization, or find a way around the security for this one method.

The Easy Way… :except filters

Fortunately, i found an easy way to use the same controller and search method to handle all of my needs. Here’s what the controller previously looked like:

class InsuranceCompaniesController < ApplicationController
  authorize_resource

  def search
    @search_results = InsuranceCompany.search params[:text]
    respond_to do |format|
      format.json { render :json => @search_results }
      format.html { render :index }
    end
  end
end

class ApplicationController < ActionController::Base
  before_filter :authenticate_account!
end

To meet my needs in this case, I need to put some :except filters into the authorize_resource and authenticate_account! calls. Easy enough… and, copying the :authenticate_account! filter into the InsuranceCompaniesController also overrides the behavior of the base class, meaning I don’t have to write a bunch of funky code in the base controller to know when / when not to modify the rule. Here’s what my controller looks like, now:

class InsuranceCompaniesController < ApplicationController
  authorize_resource :except => :search
  before_filter :authenticate_account!, :except => :search

  def search
    @search_results = InsuranceCompany.search params[:text]
    respond_to do |format|
      format.json { render :json => @search_results }
      format.html {
        authenticate_account!
        authorize! :manage, InsuranceCompany
        render :index
      }
    end
  end
end

The big changes here, are the :except filters being applied to both the authorize_resource and before_filter calls. This tells cancan and devise to ignore the search action. Then in the search action itself, when I’m responding to json format, I just format the results and send them on their way. However, when I’m responding to the html format, I add the authentication and authorization calls explicitly.

It’s a fairly small set of changes, and it does duplicate a few lines of code by manually calling the authentication and authorization methods, but I’m happy with the results. It let me keep the same controller and action for my API while providing the context and behavioral changes that I needed.

Pardon our dust… working on restoring comments