The EdgeCase Library Need Development? Check us out!

Securing your API using Rack Middleware and OAuth 1.0

Posted 24 February 2012

Author Tony Schneider

Source Github

One of the biggest challenges in web development is knowing when to break your monolithic application into services. If you segment too soon, you risk introducing unnecessary complexity. On the other hand, if you wait too long, your application is prone to becoming an unmaintainable ball of mud. I obviously can't give you a concrete answer to this problem. However, if you decide to break your application into services, there is one thing that remains constant; securing your services' endpoints. I've found Rack middleware to be an excellent tool for the job.

I had read quite a bit about Rack Middleware. I had seen a talk, watched a Railscast, and often used other peoples' middlewares in my day-to-day Rails development. However, if you're anything like me, sometimes things don't fully sink in until you have an opportunity to get your hands dirty. Today we're going to do just that!

That being said, although the majority of this code was extracted from a real project, the following code and accompanying repository exists for the sole purpose of demonstrating how easy it is to get started using Rack middleware to secure your APIs endpoints.

Since I'm not a huge fan of long boring blog posts, let's make it nerdy awesome by sprinkling some Star Wars on it. Feel free to follow along with the source on Github.

Our Mission

A long time ago, in a galaxy far, far away...

After the destruction of the first Death Star, the Imperial IT department has decided to outsource some development. Apparently one of their developers forgot to properly secure their API, allowing a small group of Rebel hackers to steal the plans for the Death Star. After some "management changes", it is now up to us to prevent the Rebels from exploiting the Imperial's service oriented architecture.

But, little do the Sith know, we were using a Jedi mind trick!

To successfully pull off this ruse, we'll be ensuring that only clients who sign their requests according to OAuth 1.0 RFC 5849 Section 3, a protocol unknown to even the most chatty protocol droids, are allowed access. However, instead of polluting the Imperial API with logic concerning validation of requests, we'll be wrapping it all in Rack Middleware, and distributing it as a gem!

Rack

When talking about Rack, it's a given that someone will mention Rack's simple API. It really is a thing of beauty. There's only a couple things you need to know before you can start hacking:

  1. You need to define an initialize method that accepts at least one argument, the Rack application. In other words, Rack Apps are to Rack middleware, as Jedi are to the Force.
  2. Your middleware needs to respond to call, accepting an env (request environment). Much like a Jedi senses disturbances in the Force, a Rack middleware uses the env to react to, um... disturbances in the request.

Now for some setup! Woo! Excitement!

Setup

mkdir -p forcefield/lib/forcefield forcefield/spec/lib/forcefield
cd forcefield

echo "require 'forcefield/middleware" > lib/forcefield.rb

touch lib/forcefield/middleware.rb
touch lib/forcefield/request.rb
touch forcefield.gemspec

bundle init (install bundle if you haven't already)

Edit your Gemfile as follows:

source "http://rubygems.org"
gemspec

Edit your gemspec to include the following dependencies:

gem.add_dependency 'rack', '~> 1.4'
gem.add_dependency 'simple_oauth'
gem.add_development_dependency 'rspec', '~> 2.7'

"bundle" it up and we're ready to start rockin'. (Please see source for Rakefile and spec_helper boilerplate)

Assumptions

Whoa there turbo! Before we get started, let's make a few assumptions. The Imperial's API already implemented an ImperialClient model that stores all the registered clients' consumer keys and secrets. Also, we're not checking OAuth nonces, leaving the Imperials open to replay attacks.

Little do they know, we've already emailed a set of Imperial credentials to the Rebels. Now we just have to make sure to help them implement the protocol by sending them helpful error messages in the response body.

Tests!

So, let's start by failing some tests (spec/lib/forcefield/middleware_spec.rb):

require 'spec_helper'

describe Forcefield::Middleware do
  let(:death_star) { lambda { |env| [200, {}, []] } }
  let(:middleware) { Forcefield::Middleware.new(death_star) }
  let(:mock_request) { Rack::MockRequest.new(middleware) }

  context "When incoming request has no Authorization header" do
    let(:resp) { mock_request.get("/") }

    it("returns a 401") { resp.status.should == 401 }

    it "notifies the client they are Unauthorized" do
      resp.body.should == "Unauthorized! You are part of the Rebel Alliance and ar Traitor!"
    end
  end
end

If you're unfamiliar with Rack, you may find the let(:death_star) line a little peculiar. Well, it's actually creating a mock Rack Application. This is because calling a Rack application results merely in an array containing a status code, HTTP response headers, and an array of strings that serve as the response body.

In combination with Rack::MockRequest, I've found this pattern to be incredibly useful when testing Rack Middleware.

With that said, we can now watch our tests fail, and begin to implement the Forcefield class.

Middleware Checklist

As stated above, we need to make sure we do the following:

  1. Defines initialize, taking a Rack application as an argument (see death_star in test)
  2. Responds to call, passing in the request environment.
# lib/forcefield/middleware.rb
module Forcefield
  class Middleare
    def initialize(app)
      @app = app
    end

    def call(env)
      if env["HTTP_AUTHORIZATION"]
        @app.call(env)
      else
        [401, {}, ["Unauthorized! You are part of the Rebel Alliance, and a Traitor!"]]
      end
    end
  end
end

That should be sufficient to make our tests pass, however, simply having an Authorization header isn't good enough. It has to be a valid OAuth header. When implementing a protocol, as you often are when working with Rack Middleware, it's nice to write specs against the RFC documentation. Here is an OAuth header adapted for our needs.

GET /plans?type=secret&tags=killswitch HTTP/1.1
  Host: api.deathstar.com
  Authorization: OAuth realm="Endor",
    oauth_consumer_key="9djdj82h48djs9d2",
    oauth_token="kkk9d7dh3k39sjv7",
    oauth_signature_method="HMAC-SHA1",
    oauth_timestamp="137131201",
    oauth_nonce="7d8f3e4a",
    oauth_signature="bYT5CMsGcbgUdFHObYMEfcx6bsw%3D"

Let's spec it out!

# ... previous specs above ...
context "when incoming request has a Authorization header" do
  context "but is missing an OAuth Authorization scheme" do
    let(:header_with_bad_scheme) {{ "HTTP_AUTHORIZATION" => "Force" }}
    let(:resp) { mock_request.get("/", header_with_bad_scheme) }

    it("returns a 401") { resp.status.should == 401 }

    it "notifies client that they sent the wrong Authorization scheme" do
      resp.body.should == "Unauthorized. Pst! You forgot to include the Auth scheme"
    end
  end

  context "but is missing an oauth_consumer_key" do
    let(:header_with_no_key) {{ "HTTP_AUTHORIZATION" => "OAuth realm=\"Endor\"" }}
    let(:resp) { mock_request.get("/", header_with_no_key) }

    it("returns a 401") { resp.status.should == 401 }

    it "notifies the client that they have not included a consumer key" do
      resp.body.should == "Unauthorized. Pst! You forgot the consumer key"
    end
  end

  context "but is missing an oauth_signature" do
    let(:header_without_sig) {{ "HTTP_AUTHORIZATION" => "OAuth realm=\"foo\", oauth_consumer_key=\"123\"" }}
    let(:resp) { mock_request.get("/", header_without_sig) }

    it("returns a 401") { resp.status.should == 401 }

    it "notifies the client that they have not signed the request" do
      resp.body.should == "Unauthorized. Pst! You forgot to sign the request."
    end
  end

  context "but is missing an oauth_signature_method" do
    let(:header_without_sig_method) do
      { "HTTP_AUTHORIZATION" => "OAuth realm=\"foo\", oauth_consumer_key=\"123\", oauth_signature=\"SIGNATURE\"" }
    end
    let(:resp) { mock_request.get("/", header_without_sig_method) }

    it("returns a 401") { resp.status.should == 401 }

    it "notifies the client that they haven't specified how they signed the request" do
      resp.body.should == "Unauthorized. Pst! You forgot to include the OAuth signature method."
    end
  end
end

We're going to need to mock out the ImperialClient model. I like to create mock classes for things like this in "spec/support".

# spec/support/imperial_client.rb
class ImperialClient < Struct.new(:consumer_key, :consumer_secret)
  DUMMY_KEY    = 'key'
  DUMMY_SECRET = 'shhhh'

  def self.find_by_consumer_key(key)
    if key == DUMMY_KEY
      new(key, DUMMY_SECRET)
    end
  end

end

Simple OAuth - It's simple!

In order to get these tests to pass, we need to seriously parse the Authorization header. Certainly there exist some tools that can make this much easier for us. After fooling around with various OAuth libraries (looking at you oauth-ruby), I came across a pleasant abstraction - simple_oauth.

Also, this seems like an excellent place to start thinking about encapsulation and separation of concerns. We should probably have an object that handles verifying the request. Luckily, we need not look any further than Rack itself. We'll just subclass Rack::Auth::AbstractRequest, as it already provides a handful of useful helper methods. As I learned, be sure to give the Rack docs a once over before you end up reinventing the wheel. The Rack library provides a lot of potentially reusable, battle-tested code.

# lib/forcefield/request
require 'rack/auth/abstract/request'

module Forcefield
  class Request < Rack::Auth::AbstractRequest

    # This method encapsulates the various checks we need to make against the requests
    # Authorization header before we deem it ready for verification.
    # Upon passing the checks, we yield to the block so that simple_oauth can determine
    # whether or not the request has been properly signed.
    #
    def with_valid_request
      if provided?
        if !oauth?
          [401, {}, ["Unauthorized. Pst! You forgot to include the Auth scheme"]]
        elsif params[:consumer_key].nil?
          [401, {}, ["Unauthorized. Pst! You forgot the consumer key"]]
        elsif params[:signature].nil?
          [401, {}, ["Unauthorized. Pst! You forgot to sign the request."]]
        elsif params[:signature_method].nil?
          [401, {}, ["Unauthorized. Pst! You forgot to include the OAuth signature method."]]
        else
          yield(request.env)
        end
      else
        [401, {}, ["Unauthorized. Pst! You forgot to include an Auth header."]]
      end
    end

    def verify_signature(client)
      return false unless client

      header = SimpleOAuth::Header.new(request.request_method, request.url, included_request_params, auth_header)
      header.valid?(:consumer_secret => client.consumer_secret)
    end

    def consumer_key
      params[:consumer_key]
    end

    private

    def params
      @params ||= SimpleOAuth::Header.parse(auth_header)
    end

    def oauth?
      scheme == :oauth
    end

    def auth_header
      @env[authorization_key]
    end

    # only include request params if Content-Type is set to application/x-www/form-urlencoded
    # (see http://tools.ietf.org/html/rfc5849#section-3.4.1)
    #
    def included_request_params
      request.content_type == "application/x-www-form-urlencoded" ? request.params : nil
    end
  end
end

Say What?

The astute reader may have noticed that I'm calling some methods that appear to have come from nowhere. As stated above, this is because we are inheriting methods from Rack::Auth::AbstractRequest. Let's take a second to document what those methods are doing:

  • #provided? - Checks to see that the request env has included an Authorization header. Similar to what we had been doing prior to our refactor.
  • #scheme - Parses the authorization scheme from the header.
  • #params - We've actually decided to overwrite this method to better fit our needs. We'll use it to strip apart the OAuth params from the Authorization header via simple_oauth.

Refactoring our existing code

require 'forcefield/request'

module Forcefield
  class Middleware

    def initialize(app)
      @app = app
    end

    def call(env)
      @request = Request.new(env)

      @request.with_valid_request do
        if client_verified?
          env["oauth_client"] = @client
          @app.call(env)
        else
          [401, {}, ["Unauthorized. You are part of the Rebel Alliance, and a Traitor!"]]
        end
      end
    end

    private

    def client_verified?
      @client = ImperialClient.find_by_consumer_key(@request.consumer_key)
      @request.verify_signature(@client)
    end

  end
end

In my opinion, this refactor is a much better separation of concern. Our Forcefield::Middleware class is now only responsible for one conditional to determine the validity of the request, delegating all the logic for handling incorrectly signed requests to our @request object.

# ... previous test code ...
context 'client makes request with sufficient, but incorrect OAuth header' do
  let(:test_uri) { "http://api.deathstar.com" }
  let(:incorrect_secret) { "!!badsecret!!" }
  let(:bad_consumer_credentials) {{ :consumer_key => ImperialClient::DUMMY_KEY, :consumer_secret => incorrect_secret }}
  let(:invalid_auth_header) {{ "HTTP_AUTHORIZATION" => SimpleOAuth::Header.new(:get, test_uri, {}, bad_consumer_credentials).to_s }}
  let(:resp) { mock_request.get(test_uri, invalid_auth_header) }
  let(:client_with_good_credentials) { ImperialClient.new(ImperialClient::DUMMY_KEY, ImperialClient::DUMMY_SECRET) }

  before { ImperialClient.stub(:find_by_consumer_key).and_return(client_with_good_credentials) }

  it('returns a status of 401') { resp.status.should == 401 }

  it "notifies the client that they have failed at thwarting the Imperials" do
    resp.body.should == "Unauthorized. You are part of the Rebel Alliance and a Traitor!"
  end
end

We now have separated our request parsing code from our middleware's core logic. This leaves us with a lean, readable entry point for our middleware. Some additional tests, oauth specific edgecases and niceties can be found in the source on Github.

Now that we've snuck our little gem into the Deathstar's API, the Rebels stand a chance!

Oh, and why not - May the force be with you!