API authentication
27th Sep 2011I have started developing an API service for an application I'm working on and one of the requisites is that all the requests need to be authenticated through the server in order to control the applications that access to it. I did some research to learn which mechanisms are using other services to authorize third party applications and faced there are plenty of them, like for example, OAuth, basic digest or sending the auth tokens inside the request headers - one interesting project I think is worth to have a look at is Fog, a ruby library that provides a common interface to access to different cloud computing providers.
Pat Allan recently wrote a very well explained article about how to implement the authentication through the headers. The only problem I see with this approach is that the application tokens are exposed in plain text inside the request and could be read by a sniffer, so using SSL would be recommended. In case you can't or don't want to use a secure connection at the endpoint, you can always add some extra security layers without much effort like sending a signed header in the request(by the way, no solution is 100% perfect):
Say we have the following spec:
users_controller_spec.rbrequire File.expand_path('../../spec_helper', __FILE__) describe UsersController do let(:api_key) { "myapp" } let(:api_token) { "12345" } let(:app) { mock(Application, :api_key => api_key, :api_token => api_token) } def signature(key, data) digest = OpenSSL::Digest::Digest.new('sha256') Base64.encode64(OpenSSL::HMAC.digest(digest, key, data)).chomp end it "returns a 403 http code when authentication fails" do get :index, :format => :json response.response_code.should == 403 end it "authenticates the requests through the headers" do Application.stub(:find_by_api_key).and_return(app) now = Time.now.rfc2822 request.env['X-Auth-ApiKey'] = api_key request.env['X-Auth-Signature'] = signature(api_token, now) request.env['X-Auth-Date'] = now get :index, :format => :json response.response_code.should == 200 end end
What we are doing here is to use the api token as the key to encode a text, in this case, the current date and time. Then, in our controller we will obtain the registered application with the given api key, get its token and sign the value given in the X-Auth-Date header using the same algorithm we used on the client. If the value set in X-Auth-Signature is equal to the new signed value, the access will be granted:
class ApplicationController < ActionController::Base protect_from_forgery before_filter :authorize private def authorize api_key = request.headers['X-Auth-ApiKey'] signature = request.headers['X-Auth-Signature'] date = request.headers['X-Auth-Date'] app = Application.find_by_api_key(api_key) render :text => "Forbidden access", :status => 403 unless app && signature == sign_request(app.api_token, date) end def sign_request(api_token, date) digest = OpenSSL::Digest::Digest.new('sha256') Base64.encode64(OpenSSL::HMAC.digest(digest, api_token, date)).chomp end end
note: the headers names are totally arbitrary
This approach has the problem that a valid signature could get stolen and be used by unauthorized users on successive requests. One possible solution could be to set an expiration time limit and use the passed date to control it:
it "denies access to requests more than five minutes old" do Application.stub(:find_by_api_key).and_return(app) now = (Time.now - 5.minutes - 1.second).rfc2822 request.env['X-Auth-ApiKey'] = api_key request.env['X-Auth-Signature'] = signature(api_token, now) request.env['X-Auth-Date'] = now get :index, :format => :json response.response_code.should == 403 end
class ApplicationController < ActionController::Base protect_from_forgery before_filter :authorize private def authorize api_key = request.headers['X-Auth-ApiKey'] signature = request.headers['X-Auth-Signature'] date = request.headers['X-Auth-Date'] app = Application.find_by_api_key(api_key) render :text => "Forbidden access", :status => 403 unless app && valid_date(date) && signature == sign_request(app.api_token, date) end def sign_request(api_token, date) digest = OpenSSL::Digest::Digest.new('sha256') Base64.encode64(OpenSSL::HMAC.digest(digest, api_token, date)).chomp end def valid_date(request_date) now = Time.now request_date = Time.parse(request_date) expiration_date = now - 5.minutes request_date >= expiration_date && request_date <= now end end
Eventually, we could go even further and check that the same signature isn't used twice within the allowed time frame maybe by saving them in a key-value datastore like Redis, which allows to set their own timeouts so that they expire automatically: http://redis.io/commands/expire


