mgomes / api_auth

HMAC authentication for Rails and HTTP Clients
MIT License
480 stars 147 forks source link

RSpec and signing headers #50

Closed JWesorick closed 10 years ago

JWesorick commented 10 years ago

I'm using RSpec in a Rails app. Right now I'm signing the headers just before the reqeust (get :index) but it appears the headers are getting changed somewhere. Where can I put the call to ApiAuth.sign! to get it to work?

Also here is what I have in my before(:each).

before(:each) do
    @request.host = "api.example.com"
    @request.env['REMOTE_ADDR'] = '1.2.3.4'
    @user = FactoryGirl.create(:user)
    @request.headers['X-User-Token'] = @user.authentication_token
    @request.headers['X-User-Email'] = @user.email
    apiKey = APIKey.create! given_to: 'test'
    @request = ApiAuth.sign!(@request, apiKey.key, apiKey.secret)
  end
kjg commented 10 years ago

I'm guessing ActionController::TestRequest must be changing something along the way. I honestly don't know off hand where you'd have to call ApiAuth.sign! so that ActionController::TestRequest works with it. Let me know if you end up digging into it and figuring it out.

Depending on what you're trying to test though, it might make sense to stub out the authentication check instead of trying to sign the request headers. For example you could do something like controller.stub(api_authenticate: true) or ApiAuth.stub('authentic?' => true) if you're just to pass the authentication to test other things.

JWesorick commented 10 years ago

So for now I'm using

controller.class.skip_before_filter :api_authenticate

and I'll continue to work on figuring out how to test that the api_authenticate method is working.

kjg commented 10 years ago

Great. Ideally the api_auth specs should verify that authentication is working correctly. An individual app would only need to test that the app is calling to ApiAuth correctly / at the right time.

themoffatt commented 10 years ago

We worked around this by mocking the 'fullpath' attribute of the TestRequest object. The problem was the request_uri was not present when #sign! was called, but it was present when #authentic? was called. So the hash being built was different on both sides. The content_type also needs to be there before calling #sign! in the test. Hope this helps.

     # controller_path is whatever :index routes to. e.g. '/home'
      allow_any_instance_of(ActionDispatch::TestRequest).to receive(:fullpath).and_return(controller_path)
      request.env["HTTP_ACCEPT"] = "application/json"
      ApiAuth.sign!(request, 'my_access_id', ApiConfig.secret_key)
      get :index
JWesorick commented 10 years ago

Awesome, that works. Thanks!

evgeniy-trebin commented 8 years ago

How can I use it in specs with type request, where object request is missing?

DaKaZ commented 7 years ago

@kjg do you know the answer to above? I too am running Rails 5.1 and trying to write rspec tests for my API which I just secured with ApiAuth, but the request tests do no have direct access to the request object. It is possible we could sign manually, but I am not loving that idea...

DaKaZ commented 7 years ago

If anyone runs into this, I solved this by creating a custom API helper (don't forget to include in your spec_helper.rb file).

Here is my custom /spec/support/request_helper.rb file

module Requests
  module ApiHelpers
    include Rack::Test::Methods

    def json
      JSON.parse(last_response.body).deep_symbolize_keys
    end

    def result
      json[:result]
    end

    def api_request(api_account, path, type = :get, data = nil)
      sign_request_raw(api_account.id, api_account.secret_key, path, type, data)
      case type
      when :get
        get path
      when :put
        put path, data
      when :post
        post path, data
      when :delete
        delete path
      else
        raise 'Unknown method'
      end
      expect(json.has_key?(:messages)).to be_truthy
      expect(json[:messages].keys).to match_array [:info, :warn, :error]
      if last_response.status >= 200 && last_response.status < 300
        expect(json.has_key?(:result)).to be_truthy
      end
      last_response
    end

    private

    def sign_request_raw(id, secret_key, path, type, data)
      allow_any_instance_of(ActionDispatch::TestRequest).to receive(:fullpath).and_return(path)

      timestamp = Time.now.utc.httpdate
      content_type = "application/json"
      content_md5 = ''

      header "ACCEPT", content_type
      header "CONTENT_TYPE", content_type
      header "DATE", timestamp
      unless data.nil?
        content_md5 = Digest::MD5.hexdigest(data)
        header "CONTENT_MD5", content_md5
      end

      c_string = canonical_string(type, content_type, content_md5, path, timestamp)
      digest = OpenSSL::Digest.new('sha1')

      sig = Base64.strict_encode64(OpenSSL::HMAC.digest(digest, secret_key, c_string))
      header "AUTHORIZATION", "APIAuth #{id}:#{sig}"
    end

    def canonical_string(request_method, content_type, content_md5, path, timestamp)
       [request_method.upcase,
       content_type,
       content_md5,
       path,
       timestamp].join(',')
    end
  end
end

At the end of your spec_helpers.rb file add this: config.include Requests::ApiHelpers, type: :request