Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,21 @@ into the sign! method like so:
@signed_request = ApiAuth.sign!(@request, @access_id, @secret_key, :override_http_method => "PUT")
```

If you want to use another digest existing in `OpenSSL::Digest`,
you can pass the http method as an option into the sign! method like so:

``` ruby
@signed_request = ApiAuth.sign!(@request, @access_id, @secret_key, :digest => 'sha256')
```

With the `digest` option, the `Authorization` header will be change from:

Authorization = APIAuth 'client access id':'signature'

to:

Authorization = APIAuth-HMAC-DIGEST_NAME 'client access id':'signature'

### ActiveResource Clients

ApiAuth can transparently protect your ActiveResource communications with a
Expand Down Expand Up @@ -154,6 +169,19 @@ To validate whether or not a request is authentic:
ApiAuth.authentic?(signed_request, secret_key)
```

The `authentic?` method uses the digest specified in the `Authorization` header.
For exemple SHA256 for:

Authorization = APIAuth-HMAC-SHA256 'client access id':'signature'

And by default SHA1 if the HMAC-DIGEST is not specified.

If you want to force the usage of another digest method, you should pass it as an option parameter:

``` ruby
ApiAuth.authentic?(signed_request, secret_key, :digest => 'sha256')
```

If your server is a Rails app, the signed request will be the `request` object.

In order to obtain the secret key for the client, you first need to look up the
Expand Down
24 changes: 15 additions & 9 deletions lib/api_auth/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class << self
#
# secret_key: assigned secret key that is known to both parties
def sign!(request, access_id, secret_key, options = {})
options = { :override_http_method => nil, :with_http_method => false }.merge(options)
options = { :override_http_method => nil, :with_http_method => false, :digest => 'sha1' }.merge(options)
headers = Headers.new(request)
headers.calculate_md5
headers.set_date
Expand All @@ -32,9 +32,11 @@ def sign!(request, access_id, secret_key, options = {})
# secret key. Returns true if the request is authentic and false otherwise.
def authentic?(request, secret_key, options = {})
return false if secret_key.nil?

options = { :override_http_method => nil }.merge(options)

headers = Headers.new(request)

if headers.md5_mismatch?
false
elsif !signatures_match?(headers, secret_key, options)
Expand All @@ -50,7 +52,7 @@ def authentic?(request, secret_key, options = {})
def access_id(request)
headers = Headers.new(request)
if match_data = parse_auth_header(headers.authorization_header)
return match_data[1]
return match_data[2]
end

nil
Expand All @@ -67,7 +69,7 @@ def generate_secret_key

private

AUTH_HEADER_PATTERN = /APIAuth ([^:]+):(.+)$/
AUTH_HEADER_PATTERN = /APIAuth(?:-HMAC-(MD[245]|SHA(?:1|224|256|384|512)*))? ([^:]+):(.+)$/

def request_too_old?(headers)
# 900 seconds is 15 minutes
Expand All @@ -81,11 +83,14 @@ def signatures_match?(headers, secret_key, options)
match_data = parse_auth_header(headers.authorization_header)
return false unless match_data

options = options.merge(:with_http_method => true)
digest = match_data[1].blank? ? 'SHA1' : match_data[1].upcase
raise InvalidRequestDigest if !options[:digest].nil? && !options[:digest].casecmp(digest).zero?

options = { :digest => digest }.merge(options)

header_sig = match_data[2]
calculated_sig_no_http = hmac_signature(headers, secret_key, {})
calculated_sig_with_http = hmac_signature(headers, secret_key, options)
header_sig = match_data[3]
calculated_sig_no_http = hmac_signature(headers, secret_key, options.merge(:with_http_method => false))
calculated_sig_with_http = hmac_signature(headers, secret_key, options.merge(:with_http_method => true))

header_sig == calculated_sig_with_http || header_sig == calculated_sig_no_http
end
Expand All @@ -96,12 +101,13 @@ def hmac_signature(headers, secret_key, options)
else
headers.canonical_string
end
digest = OpenSSL::Digest.new('sha1')
digest = OpenSSL::Digest.new(options[:digest])
b64_encode(OpenSSL::HMAC.digest(digest, secret_key, canonical_string))
end

def auth_header(headers, access_id, secret_key, options)
"APIAuth #{access_id}:#{hmac_signature(headers, secret_key, options)}"
hmac_string = "-HMAC-#{options[:digest].upcase}" unless options[:digest] == 'sha1'
"APIAuth#{hmac_string} #{access_id}:#{hmac_signature(headers, secret_key, options)}"
end

def parse_auth_header(auth_header)
Expand Down
3 changes: 3 additions & 0 deletions lib/api_auth/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ class ApiAuthError < StandardError; end

# Raised when the HTTP request object passed is not supported
class UnknownHTTPRequest < ApiAuthError; end

# Raised when the client request digest is not the same as the server
class InvalidRequestDigest < ApiAuthError; end
end
44 changes: 42 additions & 2 deletions spec/api_auth_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
end
end

def hmac(secret_key, request, canonical_string = nil)
def hmac(secret_key, request, canonical_string = nil, digest = 'sha1')
canonical_string ||= ApiAuth::Headers.new(request).canonical_string
digest = OpenSSL::Digest.new('sha1')
digest = OpenSSL::Digest.new(digest)
ApiAuth.b64_encode(OpenSSL::HMAC.digest(digest, secret_key, canonical_string))
end

Expand Down Expand Up @@ -72,6 +72,24 @@ def hmac(secret_key, request, canonical_string = nil)
expect(request['Authorization']).to eq("APIAuth 1044:#{signature}")
end
end

context 'when passed the hmac digest option' do
let(:request) do
Net::HTTP::Put.new('/resource.xml?foo=bar&bar=foo',
'content-type' => 'text/plain',
'content-md5' => '1B2M2Y8AsgTpgAmY7PhCfg==',
'date' => Time.now.utc.httpdate
)
end

let(:canonical_string) { ApiAuth::Headers.new(request).canonical_string_with_http_method }

it 'calculates the hmac_signature with http method' do
ApiAuth.sign!(request, '1044', '123', :with_http_method => true, :digest => 'sha256')
signature = hmac('123', request, canonical_string, 'sha256')
expect(request['Authorization']).to eq("APIAuth-HMAC-SHA256 1044:#{signature}")
end
end
end

describe '.authentic?' do
Expand Down Expand Up @@ -134,6 +152,28 @@ def hmac(secret_key, request, canonical_string = nil)
expect(ApiAuth.authentic?(request, '123')).to eq false
end
end

context 'when passed the hmac digest option' do
let(:request) do
new_request = Net::HTTP::Put.new('/resource.xml?foo=bar&bar=foo',
'content-type' => 'text/plain',
'content-md5' => '1B2M2Y8AsgTpgAmY7PhCfg==',
'date' => Time.now.utc.httpdate
)
canonical_string = ApiAuth::Headers.new(new_request).canonical_string_with_http_method
signature = hmac('123', new_request, canonical_string, 'sha256')
new_request['Authorization'] = "APIAuth-HMAC-SHA256 1044:#{signature}"
new_request
end

it 'validates for sha256 digest' do
expect(ApiAuth.authentic?(request, '123', :digest => 'sha256')).to eq true
end

it 'validates exception with wrong client digest' do
expect { ApiAuth.authentic?(request, '123', :digest => 'sha512') }.to raise_error(ApiAuth::InvalidRequestDigest)
end
end
end

describe '.access_id' do
Expand Down