Skip to content

Commit

Permalink
supercharge OAuth middleware
Browse files Browse the repository at this point in the history
Changes:

 - existing "Authorization" header not overriden
 - per-request oauth config via env[:request][:oauth]
 - TomDoc
 - write ALL THE TESTS
 - backwards-compatible!
  • Loading branch information
mislav committed Jan 24, 2012
1 parent a9d67d2 commit c247609
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 35 deletions.
52 changes: 45 additions & 7 deletions lib/faraday_middleware/request/oauth.rb
@@ -1,23 +1,61 @@
require 'faraday'

module FaradayMiddleware
# Public: Uses the simple_oauth library to sign requests according the
# OAuth protocol.
#
# The options for this middleware are forwarded to SimpleOAuth::Header:
# :consumer_key, :consumer_secret, :token, :token_secret. All these
# parameters are optional.
#
# The signature is added to the "Authorization" HTTP request header. If the
# value for this header already exists, it is not overriden.
#
# For requests that have parameters in the body, such as POST, this
# middleware expects them to be in Hash form, i.e. not encoded to string.
# This means this middleware has to be positioned on the stack before any
# encoding middleware such as UrlEncoded.
class OAuth < Faraday::Middleware
dependency 'simple_oauth'

AUTH_HEADER = 'Authorization'.freeze

def initialize(app, options)
super(app)
@options = options
end

def call(env)
params = env[:body] || {}
env[:request_headers][AUTH_HEADER] ||= oauth_header(env).to_s if sign_request?(env)
@app.call(env)
end

signature_params = params.reject{ |k,v| v.respond_to?(:content_type) }
def oauth_header(env)
SimpleOAuth::Header.new env[:method],
env[:url].to_s,
signature_params(body_params(env)),
oauth_options(env)
end

header = SimpleOAuth::Header.new(env[:method], env[:url].to_s, signature_params, @options)
def sign_request?(env)
!!env[:request].fetch(:oauth, true)
end

env[:request_headers]['Authorization'] = header.to_s
def oauth_options(env)
if extra = env[:request][:oauth] and extra.is_a? Hash and !extra.empty?
@options.merge extra
else
@options
end
end

@app.call(env)
def body_params(env)
env[:body] || {}
end

def initialize(app, options)
@app, @options = app, options
def signature_params(params)
params.empty? ? params :
params.reject {|k,v| v.respond_to?(:content_type) }
end
end
end
109 changes: 81 additions & 28 deletions spec/oauth_spec.rb
Expand Up @@ -3,46 +3,99 @@
require 'uri'

describe FaradayMiddleware::OAuth do
OAUTH_HEADER_REGEX = /^OAuth oauth_consumer_key=\"\d{4}\", oauth_nonce=\".+\", oauth_signature=\".+\", oauth_signature_method=\"HMAC-SHA1\", oauth_timestamp=\"\d{10}\", oauth_token=\"\d{4}\", oauth_version=\"1\.0\"/

let(:config) do
{
:consumer_key => '1234',
:consumer_secret => '1234',
:token => '1234',
:token_secret => '1234'
def auth_header(env)
env[:request_headers]['Authorization']
end

def auth_values(env)
if auth = auth_header(env)
raise "invalid header: #{auth.inspect}" unless auth.sub!('OAuth ', '')
Hash[*auth.split(/, |=/)]
end
end

def perform(oauth_options = {}, headers = {})
env = {
:url => URI('http://example.com/'),
:request_headers => Faraday::Utils::Headers.new.update(headers),
:request => {}
}
unless oauth_options.is_a? Hash and oauth_options.empty?
env[:request][:oauth] = oauth_options
end
app = make_app
app.call(env)
end

context 'when used' do
let(:oauth) { described_class.new(lambda {|env| env}, config) }
def make_app
described_class.new(lambda{|env| env}, *Array(options))
end

context "invalid options" do
let(:options) { nil }

let(:env) do
{ :request_headers => {}, :url => URI('http://www.github.com') }
it "should error out" do
expect { make_app }.to raise_error(ArgumentError)
end
end

context "empty options" do
let(:options) { [{}] }

it "should sign request" do
auth = auth_values(perform)
expected_keys = %w[ oauth_nonce
oauth_signature oauth_signature_method
oauth_timestamp oauth_version ]

it 'should add the access token to the header' do
request = oauth.call(env)
request[:request_headers]["Authorization"].should match OAUTH_HEADER_REGEX
auth.keys.should eq(expected_keys)
end
end

context "configured with consumer and token" do
let(:options) do
[{ :consumer_key => 'CKEY', :consumer_secret => 'CSECRET',
:token => 'TOKEN', :token_secret => 'TSECRET'
}]
end

it "adds auth info to the header" do
auth = auth_values(perform)
expected_keys = %w[ oauth_consumer_key oauth_nonce
oauth_signature oauth_signature_method
oauth_timestamp oauth_token oauth_version ]

auth.keys.should eq(expected_keys)
auth['oauth_version'].should eq(%("1.0"))
auth['oauth_signature_method'].should eq(%("HMAC-SHA1"))
auth['oauth_consumer_key'].should eq(%("CKEY"))
auth['oauth_token'].should eq(%("TOKEN"))
end

context 'integration test' do
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
let(:connection) do
Faraday::Connection.new do |builder|
builder.use described_class, config
builder.adapter :test, stubs
end
it "doesn't override existing header" do
request = perform({}, "Authorization" => "iz me!")
auth_header(request).should eq("iz me!")
end

# Sadly we can not check the headers in this integration test, but this will
# confirm that the middleware doesn't break the stack
it 'should add the access token to the query string' do
stubs.get('/me') {[200, {}, 'sferik']}
me = connection.get('http://www.github.com/me')
me.body.should == 'sferik'
it "can override oauth options per-request" do
auth = auth_values(perform(:consumer_key => 'CKEY2'))

auth['oauth_consumer_key'].should eq(%("CKEY2"))
auth['oauth_token'].should eq(%("TOKEN"))
end

it "can turn off oauth signing per-request" do
auth_header(perform(false)).should be_nil
end
end

context "configured without token" do
let(:options) { [{ :consumer_key => 'CKEY', :consumer_secret => 'CSECRET' }] }

it "adds auth info to the header" do
auth = auth_values(perform)
auth.should include('oauth_consumer_key')
auth.should_not include('oauth_token')
end
end
end

0 comments on commit c247609

Please sign in to comment.