api-auth 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile.lock +7 -1
- data/README.md +2 -1
- data/VERSION +1 -1
- data/api_auth.gemspec +3 -1
- data/lib/api_auth/base.rb +45 -29
- data/lib/api_auth/headers.rb +29 -8
- data/lib/api_auth/request_drivers/action_controller.rb +37 -14
- data/lib/api_auth/request_drivers/curb.rb +21 -13
- data/lib/api_auth/request_drivers/net_http.rb +30 -12
- data/lib/api_auth/request_drivers/rest_client.rb +36 -13
- data/spec/api_auth_spec.rb +196 -35
- data/spec/headers_spec.rb +73 -25
- data/spec/railtie_spec.rb +10 -1
- metadata +41 -3
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
api-auth (1.0.
|
4
|
+
api-auth (1.0.1)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: http://rubygems.org/
|
@@ -12,10 +12,13 @@ GEM
|
|
12
12
|
activeresource (2.3.14)
|
13
13
|
activesupport (= 2.3.14)
|
14
14
|
activesupport (2.3.14)
|
15
|
+
amatch (0.2.10)
|
16
|
+
tins (~> 0.3)
|
15
17
|
curb (0.8.1)
|
16
18
|
diff-lcs (1.1.3)
|
17
19
|
mime-types (1.17.2)
|
18
20
|
rack (1.1.3)
|
21
|
+
rake (0.9.2.2)
|
19
22
|
rest-client (1.6.7)
|
20
23
|
mime-types (>= 1.16)
|
21
24
|
rspec (2.4.0)
|
@@ -26,6 +29,7 @@ GEM
|
|
26
29
|
rspec-expectations (2.4.0)
|
27
30
|
diff-lcs (~> 1.1.2)
|
28
31
|
rspec-mocks (2.4.0)
|
32
|
+
tins (0.5.5)
|
29
33
|
|
30
34
|
PLATFORMS
|
31
35
|
ruby
|
@@ -34,7 +38,9 @@ DEPENDENCIES
|
|
34
38
|
actionpack (~> 2.3.2)
|
35
39
|
activeresource (~> 2.3.2)
|
36
40
|
activesupport (~> 2.3.2)
|
41
|
+
amatch
|
37
42
|
api-auth!
|
38
43
|
curb (~> 0.8.1)
|
44
|
+
rake
|
39
45
|
rest-client (~> 1.6.0)
|
40
46
|
rspec (~> 2.4.0)
|
data/README.md
CHANGED
@@ -36,7 +36,8 @@ SHA1 HMAC, using the client's private secret key.
|
|
36
36
|
request headers and the client's secret key, which is known to only
|
37
37
|
the client and the server but can be looked up on the server using the client's
|
38
38
|
access id that was attached in the header. The access id can be any integer or
|
39
|
-
string that uniquely identifies the client.
|
39
|
+
string that uniquely identifies the client. The signed request expires after 15
|
40
|
+
minutes in order to avoid replay attacks.
|
40
41
|
|
41
42
|
|
42
43
|
## References ##
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.0.
|
1
|
+
1.0.1
|
data/api_auth.gemspec
CHANGED
@@ -5,11 +5,13 @@ Gem::Specification.new do |s|
|
|
5
5
|
s.name = %q{api-auth}
|
6
6
|
s.summary = %q{Simple HMAC authentication for your APIs}
|
7
7
|
s.description = %q{Full HMAC auth implementation for use in your gems and Rails apps.}
|
8
|
-
s.homepage = %q{
|
8
|
+
s.homepage = %q{https://github.com/mgomes/api_auth}
|
9
9
|
s.version = File.read(File.join(File.dirname(__FILE__), 'VERSION'))
|
10
10
|
s.authors = ["Mauricio Gomes"]
|
11
11
|
s.email = "mauricio@edge14.com"
|
12
12
|
|
13
|
+
s.add_development_dependency "rake"
|
14
|
+
s.add_development_dependency "amatch"
|
13
15
|
s.add_development_dependency "rspec", "~> 2.4.0"
|
14
16
|
s.add_development_dependency "actionpack", "~> 2.3.2"
|
15
17
|
s.add_development_dependency "activesupport", "~> 2.3.2"
|
data/lib/api_auth/base.rb
CHANGED
@@ -1,81 +1,97 @@
|
|
1
1
|
# api-auth is Ruby gem designed to be used both in your client and server
|
2
|
-
# HTTP-based applications. It implements the same authentication methods (HMAC)
|
2
|
+
# HTTP-based applications. It implements the same authentication methods (HMAC)
|
3
3
|
# used by Amazon Web Services.
|
4
4
|
|
5
|
-
# The gem will sign your requests on the client side and authenticate that
|
6
|
-
# signature on the server side. If your server resources are implemented as a
|
7
|
-
# Rails ActiveResource, it will integrate with that. It will even generate the
|
5
|
+
# The gem will sign your requests on the client side and authenticate that
|
6
|
+
# signature on the server side. If your server resources are implemented as a
|
7
|
+
# Rails ActiveResource, it will integrate with that. It will even generate the
|
8
8
|
# secret keys necessary for your clients to sign their requests.
|
9
9
|
module ApiAuth
|
10
|
-
|
10
|
+
|
11
11
|
class << self
|
12
|
-
|
12
|
+
|
13
13
|
include Helpers
|
14
|
-
|
14
|
+
|
15
15
|
# Signs an HTTP request using the client's access id and secret key.
|
16
16
|
# Returns the HTTP request object with the modified headers.
|
17
17
|
#
|
18
|
-
# request: The request can be a Net::HTTP, ActionController::Request,
|
18
|
+
# request: The request can be a Net::HTTP, ActionController::Request,
|
19
19
|
# Curb (Curl::Easy) or a RestClient object.
|
20
20
|
#
|
21
21
|
# access_id: The public unique identifier for the client
|
22
22
|
#
|
23
|
-
# secret_key: assigned secret key that is known to both parties
|
23
|
+
# secret_key: assigned secret key that is known to both parties
|
24
24
|
def sign!(request, access_id, secret_key)
|
25
25
|
headers = Headers.new(request)
|
26
|
+
headers.calculate_md5
|
27
|
+
headers.set_date
|
26
28
|
headers.sign_header auth_header(request, access_id, secret_key)
|
27
29
|
end
|
28
|
-
|
30
|
+
|
29
31
|
# Determines if the request is authentic given the request and the client's
|
30
32
|
# secret key. Returns true if the request is authentic and false otherwise.
|
31
33
|
def authentic?(request, secret_key)
|
32
34
|
return false if secret_key.nil?
|
33
|
-
|
34
|
-
|
35
|
-
if match_data = parse_auth_header(headers.authorization_header)
|
36
|
-
hmac = match_data[2]
|
37
|
-
return hmac == hmac_signature(request, secret_key)
|
38
|
-
end
|
39
|
-
|
40
|
-
false
|
35
|
+
|
36
|
+
return !md5_mismatch?(request) && signatures_match?(request, secret_key) && !request_too_old?(request)
|
41
37
|
end
|
42
|
-
|
38
|
+
|
43
39
|
# Returns the access id from the request's authorization header
|
44
40
|
def access_id(request)
|
45
41
|
headers = Headers.new(request)
|
46
42
|
if match_data = parse_auth_header(headers.authorization_header)
|
47
43
|
return match_data[1]
|
48
44
|
end
|
49
|
-
|
45
|
+
|
50
46
|
nil
|
51
47
|
end
|
52
|
-
|
48
|
+
|
53
49
|
# Generates a Base64 encoded, randomized secret key
|
54
50
|
#
|
55
|
-
# Store this key along with the access key that will be used for
|
51
|
+
# Store this key along with the access key that will be used for
|
56
52
|
# authenticating the client
|
57
53
|
def generate_secret_key
|
58
54
|
random_bytes = OpenSSL::Random.random_bytes(512)
|
59
55
|
b64_encode(Digest::SHA2.new(512).digest(random_bytes))
|
60
56
|
end
|
61
|
-
|
57
|
+
|
62
58
|
private
|
63
|
-
|
59
|
+
|
60
|
+
def request_too_old?(request)
|
61
|
+
headers = Headers.new(request)
|
62
|
+
# 900 seconds is 15 minutes
|
63
|
+
Time.parse(headers.timestamp).utc < (Time.current.utc - 900)
|
64
|
+
end
|
65
|
+
|
66
|
+
def md5_mismatch?(request)
|
67
|
+
headers = Headers.new(request)
|
68
|
+
headers.md5_mismatch?
|
69
|
+
end
|
70
|
+
|
71
|
+
def signatures_match?(request, secret_key)
|
72
|
+
headers = Headers.new(request)
|
73
|
+
if match_data = parse_auth_header(headers.authorization_header)
|
74
|
+
hmac = match_data[2]
|
75
|
+
return hmac == hmac_signature(request, secret_key)
|
76
|
+
end
|
77
|
+
false
|
78
|
+
end
|
79
|
+
|
64
80
|
def hmac_signature(request, secret_key)
|
65
81
|
headers = Headers.new(request)
|
66
82
|
canonical_string = headers.canonical_string
|
67
83
|
digest = OpenSSL::Digest::Digest.new('sha1')
|
68
84
|
b64_encode(OpenSSL::HMAC.digest(digest, secret_key, canonical_string))
|
69
85
|
end
|
70
|
-
|
86
|
+
|
71
87
|
def auth_header(request, access_id, secret_key)
|
72
|
-
"APIAuth #{access_id}:#{hmac_signature(request, secret_key)}"
|
88
|
+
"APIAuth #{access_id}:#{hmac_signature(request, secret_key)}"
|
73
89
|
end
|
74
|
-
|
90
|
+
|
75
91
|
def parse_auth_header(auth_header)
|
76
92
|
Regexp.new("APIAuth ([^:]+):(.+)$").match(auth_header)
|
77
93
|
end
|
78
|
-
|
94
|
+
|
79
95
|
end # class methods
|
80
|
-
|
96
|
+
|
81
97
|
end # ApiAuth
|
data/lib/api_auth/headers.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
module ApiAuth
|
2
|
-
|
2
|
+
|
3
3
|
# Builds the canonical string given a request object.
|
4
4
|
class Headers
|
5
|
-
|
5
|
+
|
6
6
|
include RequestDrivers
|
7
|
-
|
7
|
+
|
8
8
|
def initialize(request)
|
9
9
|
@original_request = request
|
10
|
-
|
10
|
+
|
11
11
|
case request.class.to_s
|
12
12
|
when /Net::HTTP/
|
13
13
|
@request = NetHttpRequest.new(request)
|
@@ -31,6 +31,11 @@ module ApiAuth
|
|
31
31
|
true
|
32
32
|
end
|
33
33
|
|
34
|
+
# Returns the request timestamp
|
35
|
+
def timestamp
|
36
|
+
@request.timestamp
|
37
|
+
end
|
38
|
+
|
34
39
|
# Returns the canonical string computed from the request's headers
|
35
40
|
def canonical_string
|
36
41
|
[ @request.content_type,
|
@@ -39,12 +44,28 @@ module ApiAuth
|
|
39
44
|
@request.timestamp
|
40
45
|
].join(",")
|
41
46
|
end
|
42
|
-
|
47
|
+
|
43
48
|
# Returns the authorization header from the request's headers
|
44
49
|
def authorization_header
|
45
50
|
@request.authorization_header
|
46
51
|
end
|
47
|
-
|
52
|
+
|
53
|
+
def set_date
|
54
|
+
@request.set_date if @request.timestamp.blank?
|
55
|
+
end
|
56
|
+
|
57
|
+
def calculate_md5
|
58
|
+
@request.populate_content_md5 if @request.content_md5.blank?
|
59
|
+
end
|
60
|
+
|
61
|
+
def md5_mismatch?
|
62
|
+
if @request.content_md5.blank?
|
63
|
+
false
|
64
|
+
else
|
65
|
+
@request.md5_mismatch?
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
48
69
|
# Sets the request's authorization header with the passed in value.
|
49
70
|
# The header should be the ApiAuth HMAC signature.
|
50
71
|
#
|
@@ -53,7 +74,7 @@ module ApiAuth
|
|
53
74
|
def sign_header(header)
|
54
75
|
@request.set_auth_header header
|
55
76
|
end
|
56
|
-
|
77
|
+
|
57
78
|
end
|
58
|
-
|
79
|
+
|
59
80
|
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
module ApiAuth
|
2
|
-
|
2
|
+
|
3
3
|
module RequestDrivers # :nodoc:
|
4
|
-
|
4
|
+
|
5
5
|
class ActionControllerRequest # :nodoc:
|
6
|
-
|
6
|
+
|
7
7
|
include ApiAuth::Helpers
|
8
8
|
|
9
9
|
def initialize(request)
|
@@ -11,13 +11,36 @@ module ApiAuth
|
|
11
11
|
@headers = fetch_headers
|
12
12
|
true
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
def set_auth_header(header)
|
16
16
|
@request.env["Authorization"] = header
|
17
17
|
@headers = fetch_headers
|
18
18
|
@request
|
19
19
|
end
|
20
|
-
|
20
|
+
|
21
|
+
def calculated_md5
|
22
|
+
if @request.body
|
23
|
+
body = @request.body.read
|
24
|
+
else
|
25
|
+
body = ''
|
26
|
+
end
|
27
|
+
Digest::MD5.base64digest(body)
|
28
|
+
end
|
29
|
+
|
30
|
+
def populate_content_md5
|
31
|
+
if @request.put? || @request.post?
|
32
|
+
@request.env["Content-MD5"] = calculated_md5
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def md5_mismatch?
|
37
|
+
if @request.put? || @request.post?
|
38
|
+
calculated_md5 != content_md5
|
39
|
+
else
|
40
|
+
false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
21
44
|
def fetch_headers
|
22
45
|
capitalize_keys @request.env
|
23
46
|
end
|
@@ -28,7 +51,7 @@ module ApiAuth
|
|
28
51
|
end
|
29
52
|
|
30
53
|
def content_md5
|
31
|
-
value = find_header(%w(CONTENT-MD5 CONTENT_MD5))
|
54
|
+
value = find_header(%w(CONTENT-MD5 CONTENT_MD5 HTTP_CONTENT_MD5))
|
32
55
|
value.nil? ? "" : value
|
33
56
|
end
|
34
57
|
|
@@ -36,13 +59,13 @@ module ApiAuth
|
|
36
59
|
@request.request_uri
|
37
60
|
end
|
38
61
|
|
62
|
+
def set_date
|
63
|
+
@request.env['DATE'] = Time.now.utc.httpdate
|
64
|
+
end
|
65
|
+
|
39
66
|
def timestamp
|
40
67
|
value = find_header(%w(DATE HTTP_DATE))
|
41
|
-
|
42
|
-
value = Time.now.utc.httpdate
|
43
|
-
@request.env['DATE'] = value
|
44
|
-
end
|
45
|
-
value
|
68
|
+
value.nil? ? "" : value
|
46
69
|
end
|
47
70
|
|
48
71
|
def authorization_header
|
@@ -56,7 +79,7 @@ module ApiAuth
|
|
56
79
|
end
|
57
80
|
|
58
81
|
end
|
59
|
-
|
82
|
+
|
60
83
|
end
|
61
|
-
|
62
|
-
end
|
84
|
+
|
85
|
+
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
module ApiAuth
|
2
|
-
|
2
|
+
|
3
3
|
module RequestDrivers # :nodoc:
|
4
|
-
|
4
|
+
|
5
5
|
class CurbRequest # :nodoc:
|
6
|
-
|
6
|
+
|
7
7
|
include ApiAuth::Helpers
|
8
8
|
|
9
9
|
def initialize(request)
|
@@ -11,13 +11,21 @@ module ApiAuth
|
|
11
11
|
@headers = fetch_headers
|
12
12
|
true
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
def set_auth_header(header)
|
16
16
|
@request.headers.merge!({ "Authorization" => header })
|
17
17
|
@headers = fetch_headers
|
18
18
|
@request
|
19
19
|
end
|
20
|
-
|
20
|
+
|
21
|
+
def populate_content_md5
|
22
|
+
nil #doesn't appear to be possible
|
23
|
+
end
|
24
|
+
|
25
|
+
def md5_mismatch?
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
21
29
|
def fetch_headers
|
22
30
|
capitalize_keys @request.headers
|
23
31
|
end
|
@@ -36,13 +44,13 @@ module ApiAuth
|
|
36
44
|
@request.url
|
37
45
|
end
|
38
46
|
|
47
|
+
def set_date
|
48
|
+
@request.headers.merge!({ "DATE" => Time.now.utc.httpdate })
|
49
|
+
end
|
50
|
+
|
39
51
|
def timestamp
|
40
52
|
value = find_header(%w(DATE HTTP_DATE))
|
41
|
-
|
42
|
-
value = Time.now.utc.httpdate
|
43
|
-
@request.headers.merge!({ "DATE" => value })
|
44
|
-
end
|
45
|
-
value
|
53
|
+
value.nil? ? "" : value
|
46
54
|
end
|
47
55
|
|
48
56
|
def authorization_header
|
@@ -56,7 +64,7 @@ module ApiAuth
|
|
56
64
|
end
|
57
65
|
|
58
66
|
end
|
59
|
-
|
67
|
+
|
60
68
|
end
|
61
|
-
|
62
|
-
end
|
69
|
+
|
70
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module ApiAuth
|
2
|
-
|
2
|
+
|
3
3
|
module RequestDrivers # :nodoc:
|
4
|
-
|
4
|
+
|
5
5
|
class NetHttpRequest # :nodoc:
|
6
6
|
|
7
7
|
def initialize(request)
|
@@ -9,13 +9,31 @@ module ApiAuth
|
|
9
9
|
@headers = fetch_headers
|
10
10
|
true
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
def set_auth_header(header)
|
14
14
|
@request["Authorization"] = header
|
15
15
|
@headers = fetch_headers
|
16
16
|
@request
|
17
17
|
end
|
18
|
-
|
18
|
+
|
19
|
+
def calculated_md5
|
20
|
+
Digest::MD5.base64digest(@request.body || '')
|
21
|
+
end
|
22
|
+
|
23
|
+
def populate_content_md5
|
24
|
+
if @request.class::REQUEST_HAS_BODY
|
25
|
+
@request["Content-MD5"] = calculated_md5
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def md5_mismatch?
|
30
|
+
if @request.class::REQUEST_HAS_BODY
|
31
|
+
calculated_md5 != content_md5
|
32
|
+
else
|
33
|
+
false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
19
37
|
def fetch_headers
|
20
38
|
@request
|
21
39
|
end
|
@@ -34,13 +52,13 @@ module ApiAuth
|
|
34
52
|
@request.path
|
35
53
|
end
|
36
54
|
|
55
|
+
def set_date
|
56
|
+
@request["DATE"] = Time.now.utc.httpdate
|
57
|
+
end
|
58
|
+
|
37
59
|
def timestamp
|
38
60
|
value = find_header(%w(DATE HTTP_DATE))
|
39
|
-
|
40
|
-
value = Time.now.utc.httpdate
|
41
|
-
@request["DATE"] = value
|
42
|
-
end
|
43
|
-
value
|
61
|
+
value.nil? ? "" : value
|
44
62
|
end
|
45
63
|
|
46
64
|
def authorization_header
|
@@ -54,7 +72,7 @@ module ApiAuth
|
|
54
72
|
end
|
55
73
|
|
56
74
|
end
|
57
|
-
|
75
|
+
|
58
76
|
end
|
59
|
-
|
60
|
-
end
|
77
|
+
|
78
|
+
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
module ApiAuth
|
2
|
-
|
2
|
+
|
3
3
|
module RequestDrivers # :nodoc:
|
4
|
-
|
4
|
+
|
5
5
|
class RestClientRequest # :nodoc:
|
6
|
-
|
6
|
+
|
7
7
|
include ApiAuth::Helpers
|
8
8
|
|
9
9
|
def initialize(request)
|
@@ -11,13 +11,36 @@ module ApiAuth
|
|
11
11
|
@headers = fetch_headers
|
12
12
|
true
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
def set_auth_header(header)
|
16
16
|
@request.headers.merge!({ "Authorization" => header })
|
17
17
|
@headers = fetch_headers
|
18
18
|
@request
|
19
19
|
end
|
20
|
-
|
20
|
+
|
21
|
+
def calculated_md5
|
22
|
+
if @request.payload
|
23
|
+
body = @request.payload.read
|
24
|
+
else
|
25
|
+
body = ''
|
26
|
+
end
|
27
|
+
Digest::MD5.base64digest(body)
|
28
|
+
end
|
29
|
+
|
30
|
+
def populate_content_md5
|
31
|
+
if [:post, :put].include?(@request.method)
|
32
|
+
@request.headers["Content-MD5"] = calculated_md5
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def md5_mismatch?
|
37
|
+
if [:post, :put].include?(@request.method)
|
38
|
+
calculated_md5 != content_md5
|
39
|
+
else
|
40
|
+
false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
21
44
|
def fetch_headers
|
22
45
|
capitalize_keys @request.headers
|
23
46
|
end
|
@@ -36,13 +59,13 @@ module ApiAuth
|
|
36
59
|
@request.url
|
37
60
|
end
|
38
61
|
|
62
|
+
def set_date
|
63
|
+
@request.headers.merge!({ "DATE" => Time.now.utc.httpdate })
|
64
|
+
end
|
65
|
+
|
39
66
|
def timestamp
|
40
67
|
value = find_header(%w(DATE HTTP_DATE))
|
41
|
-
|
42
|
-
value = Time.now.utc.httpdate
|
43
|
-
@request.headers.merge!({ "DATE" => value })
|
44
|
-
end
|
45
|
-
value
|
68
|
+
value.nil? ? "" : value
|
46
69
|
end
|
47
70
|
|
48
71
|
def authorization_header
|
@@ -56,7 +79,7 @@ module ApiAuth
|
|
56
79
|
end
|
57
80
|
|
58
81
|
end
|
59
|
-
|
82
|
+
|
60
83
|
end
|
61
|
-
|
62
|
-
end
|
84
|
+
|
85
|
+
end
|
data/spec/api_auth_spec.rb
CHANGED
@@ -1,52 +1,77 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
2
|
|
3
3
|
describe "ApiAuth" do
|
4
|
-
|
4
|
+
|
5
5
|
describe "generating secret keys" do
|
6
|
-
|
6
|
+
|
7
7
|
it "should generate secret keys" do
|
8
8
|
ApiAuth.generate_secret_key
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
it "should generate secret keys that are 89 characters" do
|
12
12
|
ApiAuth.generate_secret_key.size.should be(89)
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
it "should generate keys that have a Hamming Distance of at least 65" do
|
16
16
|
key1 = ApiAuth.generate_secret_key
|
17
17
|
key2 = ApiAuth.generate_secret_key
|
18
18
|
Amatch::Hamming.new(key1).match(key2).should be > 65
|
19
19
|
end
|
20
|
-
|
20
|
+
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
describe "signing requests" do
|
24
|
-
|
24
|
+
|
25
25
|
def hmac(secret_key, request)
|
26
26
|
canonical_string = ApiAuth::Headers.new(request).canonical_string
|
27
27
|
digest = OpenSSL::Digest::Digest.new('sha1')
|
28
28
|
ApiAuth.b64_encode(OpenSSL::HMAC.digest(digest, secret_key, canonical_string))
|
29
29
|
end
|
30
|
-
|
30
|
+
|
31
31
|
before(:all) do
|
32
32
|
@access_id = "1044"
|
33
33
|
@secret_key = ApiAuth.generate_secret_key
|
34
34
|
end
|
35
|
-
|
35
|
+
|
36
36
|
describe "with Net::HTTP" do
|
37
|
-
|
37
|
+
|
38
38
|
before(:each) do
|
39
39
|
@request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
|
40
40
|
'content-type' => 'text/plain',
|
41
|
-
'content-md5' => '
|
42
|
-
'date' =>
|
41
|
+
'content-md5' => '1B2M2Y8AsgTpgAmY7PhCfg==',
|
42
|
+
'date' => Time.now.utc.httpdate)
|
43
43
|
@signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
|
44
44
|
end
|
45
|
-
|
45
|
+
|
46
46
|
it "should return a Net::HTTP object after signing it" do
|
47
47
|
ApiAuth.sign!(@request, @access_id, @secret_key).class.to_s.should match("Net::HTTP")
|
48
48
|
end
|
49
|
-
|
49
|
+
|
50
|
+
describe "md5 header" do
|
51
|
+
context "not already provided" do
|
52
|
+
it "should calculate for empty string" do
|
53
|
+
request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
|
54
|
+
'content-type' => 'text/plain',
|
55
|
+
'date' => "Mon, 23 Jan 1984 03:29:56 GMT")
|
56
|
+
signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
|
57
|
+
signed_request['Content-MD5'].should == Digest::MD5.base64digest('')
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should calculate for real content" do
|
61
|
+
request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
|
62
|
+
'content-type' => 'text/plain',
|
63
|
+
'date' => "Mon, 23 Jan 1984 03:29:56 GMT")
|
64
|
+
request.body = "hello\nworld"
|
65
|
+
signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
|
66
|
+
signed_request['Content-MD5'].should == Digest::MD5.base64digest("hello\nworld")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should leave the content-md5 alone if provided" do
|
71
|
+
@signed_request['Content-MD5'].should == '1B2M2Y8AsgTpgAmY7PhCfg=='
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
50
75
|
it "should sign the request" do
|
51
76
|
@signed_request['Authorization'].should == "APIAuth 1044:#{hmac(@secret_key, @request)}"
|
52
77
|
end
|
@@ -54,33 +79,78 @@ describe "ApiAuth" do
|
|
54
79
|
it "should authenticate a valid request" do
|
55
80
|
ApiAuth.authentic?(@signed_request, @secret_key).should be_true
|
56
81
|
end
|
57
|
-
|
82
|
+
|
58
83
|
it "should NOT authenticate a non-valid request" do
|
59
84
|
ApiAuth.authentic?(@signed_request, @secret_key+'j').should be_false
|
60
85
|
end
|
86
|
+
|
87
|
+
it "should NOT authenticate a mismatched content-md5 when body has changed" do
|
88
|
+
request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
|
89
|
+
'content-type' => 'text/plain',
|
90
|
+
'date' => "Mon, 23 Jan 1984 03:29:56 GMT")
|
91
|
+
request.body = "hello\nworld"
|
92
|
+
signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
|
93
|
+
signed_request.body = "goodbye"
|
94
|
+
ApiAuth.authentic?(signed_request, @secret_key).should be_false
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should NOT authenticate an expired request" do
|
98
|
+
@request['Date'] = 16.minutes.ago.utc.httpdate
|
99
|
+
signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
|
100
|
+
ApiAuth.authentic?(signed_request, @secret_key).should be_false
|
101
|
+
end
|
61
102
|
|
62
103
|
it "should retrieve the access_id" do
|
63
104
|
ApiAuth.access_id(@signed_request).should == "1044"
|
64
105
|
end
|
65
|
-
|
106
|
+
|
66
107
|
end
|
67
|
-
|
108
|
+
|
68
109
|
describe "with RestClient" do
|
69
|
-
|
110
|
+
|
70
111
|
before(:each) do
|
71
|
-
headers = { 'Content-MD5' => "
|
112
|
+
headers = { 'Content-MD5' => "1B2M2Y8AsgTpgAmY7PhCfg==",
|
72
113
|
'Content-Type' => "text/plain",
|
73
|
-
'Date' =>
|
114
|
+
'Date' => Time.now.utc.httpdate }
|
74
115
|
@request = RestClient::Request.new(:url => "/resource.xml?foo=bar&bar=foo",
|
75
116
|
:headers => headers,
|
76
117
|
:method => :put)
|
77
118
|
@signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
|
78
119
|
end
|
79
|
-
|
120
|
+
|
80
121
|
it "should return a RestClient object after signing it" do
|
81
122
|
ApiAuth.sign!(@request, @access_id, @secret_key).class.to_s.should match("RestClient")
|
82
123
|
end
|
83
|
-
|
124
|
+
|
125
|
+
describe "md5 header" do
|
126
|
+
context "not already provided" do
|
127
|
+
it "should calculate for empty string" do
|
128
|
+
headers = { 'Content-Type' => "text/plain",
|
129
|
+
'Date' => "Mon, 23 Jan 1984 03:29:56 GMT" }
|
130
|
+
request = RestClient::Request.new(:url => "/resource.xml?foo=bar&bar=foo",
|
131
|
+
:headers => headers,
|
132
|
+
:method => :put)
|
133
|
+
signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
|
134
|
+
signed_request.headers['Content-MD5'].should == Digest::MD5.base64digest('')
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should calculate for real content" do
|
138
|
+
headers = { 'Content-Type' => "text/plain",
|
139
|
+
'Date' => "Mon, 23 Jan 1984 03:29:56 GMT" }
|
140
|
+
request = RestClient::Request.new(:url => "/resource.xml?foo=bar&bar=foo",
|
141
|
+
:headers => headers,
|
142
|
+
:method => :put,
|
143
|
+
:payload => "hellow\nworld")
|
144
|
+
signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
|
145
|
+
signed_request.headers['Content-MD5'].should == Digest::MD5.base64digest("hellow\nworld")
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
it "should leave the content-md5 alone if provided" do
|
150
|
+
@signed_request.headers['Content-MD5'].should == "1B2M2Y8AsgTpgAmY7PhCfg=="
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
84
154
|
it "should sign the request" do
|
85
155
|
@signed_request.headers['Authorization'].should == "APIAuth 1044:#{hmac(@secret_key, @request)}"
|
86
156
|
end
|
@@ -88,33 +158,67 @@ describe "ApiAuth" do
|
|
88
158
|
it "should authenticate a valid request" do
|
89
159
|
ApiAuth.authentic?(@signed_request, @secret_key).should be_true
|
90
160
|
end
|
91
|
-
|
161
|
+
|
92
162
|
it "should NOT authenticate a non-valid request" do
|
93
163
|
ApiAuth.authentic?(@signed_request, @secret_key+'j').should be_false
|
94
164
|
end
|
165
|
+
|
166
|
+
it "should NOT authenticate a mismatched content-md5 when body has changed" do
|
167
|
+
headers = { 'Content-Type' => "text/plain",
|
168
|
+
'Date' => "Mon, 23 Jan 1984 03:29:56 GMT" }
|
169
|
+
request = RestClient::Request.new(:url => "/resource.xml?foo=bar&bar=foo",
|
170
|
+
:headers => headers,
|
171
|
+
:method => :put,
|
172
|
+
:payload => "hello\nworld")
|
173
|
+
signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
|
174
|
+
signed_request.instance_variable_set("@payload", RestClient::Payload.generate('goodbye'))
|
175
|
+
ApiAuth.authentic?(signed_request, @secret_key).should be_false
|
176
|
+
end
|
177
|
+
|
178
|
+
it "should NOT authenticate an expired request" do
|
179
|
+
@request.headers['Date'] = 16.minutes.ago.utc.httpdate
|
180
|
+
signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
|
181
|
+
ApiAuth.authentic?(signed_request, @secret_key).should be_false
|
182
|
+
end
|
95
183
|
|
96
184
|
it "should retrieve the access_id" do
|
97
185
|
ApiAuth.access_id(@signed_request).should == "1044"
|
98
186
|
end
|
99
|
-
|
187
|
+
|
100
188
|
end
|
101
|
-
|
189
|
+
|
102
190
|
describe "with Curb" do
|
103
|
-
|
191
|
+
|
104
192
|
before(:each) do
|
105
193
|
headers = { 'Content-MD5' => "e59ff97941044f85df5297e1c302d260",
|
106
194
|
'Content-Type' => "text/plain",
|
107
|
-
'Date' =>
|
195
|
+
'Date' => Time.now.utc.httpdate }
|
108
196
|
@request = Curl::Easy.new("/resource.xml?foo=bar&bar=foo") do |curl|
|
109
197
|
curl.headers = headers
|
110
198
|
end
|
111
199
|
@signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
|
112
200
|
end
|
113
|
-
|
201
|
+
|
114
202
|
it "should return a Curl::Easy object after signing it" do
|
115
203
|
ApiAuth.sign!(@request, @access_id, @secret_key).class.to_s.should match("Curl::Easy")
|
116
204
|
end
|
117
|
-
|
205
|
+
|
206
|
+
describe "md5 header" do
|
207
|
+
it "should not calculate and add the content-md5 header if not provided" do
|
208
|
+
headers = { 'Content-Type' => "text/plain",
|
209
|
+
'Date' => "Mon, 23 Jan 1984 03:29:56 GMT" }
|
210
|
+
request = Curl::Easy.new("/resource.xml?foo=bar&bar=foo") do |curl|
|
211
|
+
curl.headers = headers
|
212
|
+
end
|
213
|
+
signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
|
214
|
+
signed_request.headers['Content-MD5'].should == nil
|
215
|
+
end
|
216
|
+
|
217
|
+
it "should leave the content-md5 alone if provided" do
|
218
|
+
@signed_request.headers['Content-MD5'].should == "e59ff97941044f85df5297e1c302d260"
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
118
222
|
it "should sign the request" do
|
119
223
|
@signed_request.headers['Authorization'].should == "APIAuth 1044:#{hmac(@secret_key, @request)}"
|
120
224
|
end
|
@@ -122,17 +226,23 @@ describe "ApiAuth" do
|
|
122
226
|
it "should authenticate a valid request" do
|
123
227
|
ApiAuth.authentic?(@signed_request, @secret_key).should be_true
|
124
228
|
end
|
125
|
-
|
229
|
+
|
126
230
|
it "should NOT authenticate a non-valid request" do
|
127
231
|
ApiAuth.authentic?(@signed_request, @secret_key+'j').should be_false
|
128
232
|
end
|
233
|
+
|
234
|
+
it "should NOT authenticate an expired request" do
|
235
|
+
@request.headers['Date'] = 16.minutes.ago.utc.httpdate
|
236
|
+
signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
|
237
|
+
ApiAuth.authentic?(signed_request, @secret_key).should be_false
|
238
|
+
end
|
129
239
|
|
130
240
|
it "should retrieve the access_id" do
|
131
241
|
ApiAuth.access_id(@signed_request).should == "1044"
|
132
242
|
end
|
133
|
-
|
243
|
+
|
134
244
|
end
|
135
|
-
|
245
|
+
|
136
246
|
describe "with ActionController" do
|
137
247
|
|
138
248
|
before(:each) do
|
@@ -140,9 +250,9 @@ describe "ApiAuth" do
|
|
140
250
|
'PATH_INFO' => '/resource.xml',
|
141
251
|
'QUERY_STRING' => 'foo=bar&bar=foo',
|
142
252
|
'REQUEST_METHOD' => 'PUT',
|
143
|
-
'CONTENT_MD5' => '
|
253
|
+
'CONTENT_MD5' => '1B2M2Y8AsgTpgAmY7PhCfg==',
|
144
254
|
'CONTENT_TYPE' => 'text/plain',
|
145
|
-
'HTTP_DATE' =>
|
255
|
+
'HTTP_DATE' => Time.now.utc.httpdate)
|
146
256
|
@signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
|
147
257
|
end
|
148
258
|
|
@@ -150,6 +260,38 @@ describe "ApiAuth" do
|
|
150
260
|
ApiAuth.sign!(@request, @access_id, @secret_key).class.to_s.should match("ActionController::Request")
|
151
261
|
end
|
152
262
|
|
263
|
+
describe "md5 header" do
|
264
|
+
context "not already provided" do
|
265
|
+
it "should calculate for empty string" do
|
266
|
+
request = ActionController::Request.new(
|
267
|
+
'PATH_INFO' => '/resource.xml',
|
268
|
+
'QUERY_STRING' => 'foo=bar&bar=foo',
|
269
|
+
'REQUEST_METHOD' => 'PUT',
|
270
|
+
'CONTENT_TYPE' => 'text/plain',
|
271
|
+
'HTTP_DATE' => 'Mon, 23 Jan 1984 03:29:56 GMT')
|
272
|
+
signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
|
273
|
+
signed_request.env['Content-MD5'].should == Digest::MD5.base64digest('')
|
274
|
+
end
|
275
|
+
|
276
|
+
it "should calculate for real content" do
|
277
|
+
request = ActionController::Request.new(
|
278
|
+
'PATH_INFO' => '/resource.xml',
|
279
|
+
'QUERY_STRING' => 'foo=bar&bar=foo',
|
280
|
+
'REQUEST_METHOD' => 'PUT',
|
281
|
+
'CONTENT_TYPE' => 'text/plain',
|
282
|
+
'HTTP_DATE' => 'Mon, 23 Jan 1984 03:29:56 GMT',
|
283
|
+
'rack.input' => StringIO.new("hello\nworld"))
|
284
|
+
signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
|
285
|
+
signed_request.env['Content-MD5'].should == Digest::MD5.base64digest("hello\nworld")
|
286
|
+
end
|
287
|
+
|
288
|
+
end
|
289
|
+
|
290
|
+
it "should leave the content-md5 alone if provided" do
|
291
|
+
@signed_request.env['CONTENT_MD5'].should == '1B2M2Y8AsgTpgAmY7PhCfg=='
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
153
295
|
it "should sign the request" do
|
154
296
|
@signed_request.env['Authorization'].should == "APIAuth 1044:#{hmac(@secret_key, @request)}"
|
155
297
|
end
|
@@ -162,6 +304,25 @@ describe "ApiAuth" do
|
|
162
304
|
ApiAuth.authentic?(@signed_request, @secret_key+'j').should be_false
|
163
305
|
end
|
164
306
|
|
307
|
+
it "should NOT authenticate a mismatched content-md5 when body has changed" do
|
308
|
+
request = ActionController::Request.new(
|
309
|
+
'PATH_INFO' => '/resource.xml',
|
310
|
+
'QUERY_STRING' => 'foo=bar&bar=foo',
|
311
|
+
'REQUEST_METHOD' => 'PUT',
|
312
|
+
'CONTENT_TYPE' => 'text/plain',
|
313
|
+
'HTTP_DATE' => 'Mon, 23 Jan 1984 03:29:56 GMT',
|
314
|
+
'rack.input' => StringIO.new("hello\nworld"))
|
315
|
+
signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
|
316
|
+
signed_request.instance_variable_get("@env")["rack.input"] = StringIO.new("goodbye")
|
317
|
+
ApiAuth.authentic?(signed_request, @secret_key).should be_false
|
318
|
+
end
|
319
|
+
|
320
|
+
it "should NOT authenticate an expired request" do
|
321
|
+
@request.env['HTTP_DATE'] = 16.minutes.ago.utc.httpdate
|
322
|
+
signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
|
323
|
+
ApiAuth.authentic?(signed_request, @secret_key).should be_false
|
324
|
+
end
|
325
|
+
|
165
326
|
it "should retrieve the access_id" do
|
166
327
|
ApiAuth.access_id(@signed_request).should == "1044"
|
167
328
|
end
|
@@ -169,5 +330,5 @@ describe "ApiAuth" do
|
|
169
330
|
end
|
170
331
|
|
171
332
|
end
|
172
|
-
|
333
|
+
|
173
334
|
end
|
data/spec/headers_spec.rb
CHANGED
@@ -1,73 +1,100 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
2
|
|
3
3
|
describe "ApiAuth::Headers" do
|
4
|
-
|
4
|
+
|
5
5
|
CANONICAL_STRING = "text/plain,e59ff97941044f85df5297e1c302d260,/resource.xml?foo=bar&bar=foo,Mon, 23 Jan 1984 03:29:56 GMT"
|
6
6
|
|
7
7
|
describe "with Net::HTTP" do
|
8
|
-
|
8
|
+
|
9
9
|
before(:each) do
|
10
|
-
@request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
|
11
|
-
'content-type' => 'text/plain',
|
10
|
+
@request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
|
11
|
+
'content-type' => 'text/plain',
|
12
12
|
'content-md5' => 'e59ff97941044f85df5297e1c302d260',
|
13
13
|
'date' => "Mon, 23 Jan 1984 03:29:56 GMT")
|
14
14
|
@headers = ApiAuth::Headers.new(@request)
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
it "should generate the proper canonical string" do
|
18
18
|
@headers.canonical_string.should == CANONICAL_STRING
|
19
19
|
end
|
20
|
-
|
20
|
+
|
21
21
|
it "should set the authorization header" do
|
22
22
|
@headers.sign_header("alpha")
|
23
23
|
@headers.authorization_header.should == "alpha"
|
24
24
|
end
|
25
|
-
|
25
|
+
|
26
26
|
it "should set the DATE header if one is not already present" do
|
27
|
-
@request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
|
28
|
-
'content-type' => 'text/plain',
|
27
|
+
@request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
|
28
|
+
'content-type' => 'text/plain',
|
29
29
|
'content-md5' => 'e59ff97941044f85df5297e1c302d260')
|
30
30
|
ApiAuth.sign!(@request, "some access id", "some secret key")
|
31
31
|
@request['DATE'].should_not be_nil
|
32
32
|
end
|
33
|
-
|
33
|
+
|
34
|
+
it "should not set the DATE header just by asking for the canonical_string" do
|
35
|
+
request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
|
36
|
+
'content-type' => 'text/plain',
|
37
|
+
'content-md5' => 'e59ff97941044f85df5297e1c302d260')
|
38
|
+
headers = ApiAuth::Headers.new(request)
|
39
|
+
headers.canonical_string
|
40
|
+
request['DATE'].should be_nil
|
41
|
+
end
|
42
|
+
|
43
|
+
context "md5_mismatch?" do
|
44
|
+
it "is false if no md5 header is present" do
|
45
|
+
request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
|
46
|
+
'content-type' => 'text/plain')
|
47
|
+
headers = ApiAuth::Headers.new(request)
|
48
|
+
headers.md5_mismatch?.should be_false
|
49
|
+
end
|
50
|
+
end
|
34
51
|
end
|
35
|
-
|
52
|
+
|
36
53
|
describe "with RestClient" do
|
37
|
-
|
54
|
+
|
38
55
|
before(:each) do
|
39
56
|
headers = { 'Content-MD5' => "e59ff97941044f85df5297e1c302d260",
|
40
57
|
'Content-Type' => "text/plain",
|
41
58
|
'Date' => "Mon, 23 Jan 1984 03:29:56 GMT" }
|
42
|
-
@request = RestClient::Request.new(:url => "/resource.xml?foo=bar&bar=foo",
|
59
|
+
@request = RestClient::Request.new(:url => "/resource.xml?foo=bar&bar=foo",
|
43
60
|
:headers => headers,
|
44
61
|
:method => :put)
|
45
62
|
@headers = ApiAuth::Headers.new(@request)
|
46
63
|
end
|
47
|
-
|
64
|
+
|
48
65
|
it "should generate the proper canonical string" do
|
49
66
|
@headers.canonical_string.should == CANONICAL_STRING
|
50
67
|
end
|
51
|
-
|
68
|
+
|
52
69
|
it "should set the authorization header" do
|
53
70
|
@headers.sign_header("alpha")
|
54
71
|
@headers.authorization_header.should == "alpha"
|
55
72
|
end
|
56
|
-
|
73
|
+
|
57
74
|
it "should set the DATE header if one is not already present" do
|
58
75
|
headers = { 'Content-MD5' => "e59ff97941044f85df5297e1c302d260",
|
59
76
|
'Content-Type' => "text/plain" }
|
60
|
-
@request = RestClient::Request.new(:url => "/resource.xml?foo=bar&bar=foo",
|
77
|
+
@request = RestClient::Request.new(:url => "/resource.xml?foo=bar&bar=foo",
|
61
78
|
:headers => headers,
|
62
79
|
:method => :put)
|
63
80
|
ApiAuth.sign!(@request, "some access id", "some secret key")
|
64
81
|
@request.headers['DATE'].should_not be_nil
|
65
82
|
end
|
66
|
-
|
83
|
+
|
84
|
+
it "should not set the DATE header just by asking for the canonical_string" do
|
85
|
+
headers = { 'Content-MD5' => "e59ff97941044f85df5297e1c302d260",
|
86
|
+
'Content-Type' => "text/plain" }
|
87
|
+
request = RestClient::Request.new(:url => "/resource.xml?foo=bar&bar=foo",
|
88
|
+
:headers => headers,
|
89
|
+
:method => :put)
|
90
|
+
headers = ApiAuth::Headers.new(request)
|
91
|
+
headers.canonical_string
|
92
|
+
request.headers['DATE'].should be_nil
|
93
|
+
end
|
67
94
|
end
|
68
|
-
|
95
|
+
|
69
96
|
describe "with Curb" do
|
70
|
-
|
97
|
+
|
71
98
|
before(:each) do
|
72
99
|
headers = { 'Content-MD5' => "e59ff97941044f85df5297e1c302d260",
|
73
100
|
'Content-Type' => "text/plain",
|
@@ -77,16 +104,16 @@ describe "ApiAuth::Headers" do
|
|
77
104
|
end
|
78
105
|
@headers = ApiAuth::Headers.new(@request)
|
79
106
|
end
|
80
|
-
|
107
|
+
|
81
108
|
it "should generate the proper canonical string" do
|
82
109
|
@headers.canonical_string.should == CANONICAL_STRING
|
83
110
|
end
|
84
|
-
|
111
|
+
|
85
112
|
it "should set the authorization header" do
|
86
113
|
@headers.sign_header("alpha")
|
87
114
|
@headers.authorization_header.should == "alpha"
|
88
115
|
end
|
89
|
-
|
116
|
+
|
90
117
|
it "should set the DATE header if one is not already present" do
|
91
118
|
headers = { 'Content-MD5' => "e59ff97941044f85df5297e1c302d260",
|
92
119
|
'Content-Type' => "text/plain" }
|
@@ -96,9 +123,19 @@ describe "ApiAuth::Headers" do
|
|
96
123
|
ApiAuth.sign!(@request, "some access id", "some secret key")
|
97
124
|
@request.headers['DATE'].should_not be_nil
|
98
125
|
end
|
99
|
-
|
126
|
+
|
127
|
+
it "should not set the DATE header just by asking for the canonical_string" do
|
128
|
+
headers = { 'Content-MD5' => "e59ff97941044f85df5297e1c302d260",
|
129
|
+
'Content-Type' => "text/plain" }
|
130
|
+
request = Curl::Easy.new("/resource.xml?foo=bar&bar=foo") do |curl|
|
131
|
+
curl.headers = headers
|
132
|
+
end
|
133
|
+
headers = ApiAuth::Headers.new(request)
|
134
|
+
headers.canonical_string
|
135
|
+
request.headers['DATE'].should be_nil
|
136
|
+
end
|
100
137
|
end
|
101
|
-
|
138
|
+
|
102
139
|
describe "with ActionController" do
|
103
140
|
|
104
141
|
before(:each) do
|
@@ -132,6 +169,17 @@ describe "ApiAuth::Headers" do
|
|
132
169
|
@request.headers['DATE'].should_not be_nil
|
133
170
|
end
|
134
171
|
|
172
|
+
it "should not set the DATE header just by asking for the canonical_string" do
|
173
|
+
request = ActionController::Request.new(
|
174
|
+
'PATH_INFO' => '/resource.xml',
|
175
|
+
'QUERY_STRING' => 'foo=bar&bar=foo',
|
176
|
+
'REQUEST_METHOD' => 'PUT',
|
177
|
+
'CONTENT_MD5' => 'e59ff97941044f85df5297e1c302d260',
|
178
|
+
'CONTENT_TYPE' => 'text/plain')
|
179
|
+
headers = ApiAuth::Headers.new(request)
|
180
|
+
headers.canonical_string
|
181
|
+
request.headers['DATE'].should be_nil
|
182
|
+
end
|
135
183
|
end
|
136
184
|
|
137
185
|
end
|
data/spec/railtie_spec.rb
CHANGED
@@ -41,13 +41,22 @@ describe "Rails integration" do
|
|
41
41
|
|
42
42
|
it "should permit a request with properly signed headers" do
|
43
43
|
request = ActionController::TestRequest.new
|
44
|
-
request.env['DATE'] =
|
44
|
+
request.env['DATE'] = Time.now.utc.httpdate
|
45
45
|
request.action = 'index'
|
46
46
|
request.path = "/index"
|
47
47
|
ApiAuth.sign!(request, "1044", API_KEY_STORE["1044"])
|
48
48
|
TestController.new.process(request, ActionController::TestResponse.new).code.should == "200"
|
49
49
|
end
|
50
50
|
|
51
|
+
it "should forbid a request with properly signed headers but timestamp > 15 minutes" do
|
52
|
+
request = ActionController::TestRequest.new
|
53
|
+
request.env['DATE'] = "Mon, 23 Jan 1984 03:29:56 GMT"
|
54
|
+
request.action = 'index'
|
55
|
+
request.path = "/index"
|
56
|
+
ApiAuth.sign!(request, "1044", API_KEY_STORE["1044"])
|
57
|
+
TestController.new.process(request, ActionController::TestResponse.new).code.should == "401"
|
58
|
+
end
|
59
|
+
|
51
60
|
it "should insert a DATE header in the request when one hasn't been specified" do
|
52
61
|
request = ActionController::TestRequest.new
|
53
62
|
request.action = 'index'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: api-auth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,8 +9,40 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-11-30 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: amatch
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
14
46
|
- !ruby/object:Gem::Dependency
|
15
47
|
name: rspec
|
16
48
|
requirement: !ruby/object:Gem::Requirement
|
@@ -142,7 +174,7 @@ files:
|
|
142
174
|
- spec/railtie_spec.rb
|
143
175
|
- spec/spec_helper.rb
|
144
176
|
- spec/test_helper.rb
|
145
|
-
homepage:
|
177
|
+
homepage: https://github.com/mgomes/api_auth
|
146
178
|
licenses: []
|
147
179
|
post_install_message:
|
148
180
|
rdoc_options: []
|
@@ -154,12 +186,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
154
186
|
- - ! '>='
|
155
187
|
- !ruby/object:Gem::Version
|
156
188
|
version: '0'
|
189
|
+
segments:
|
190
|
+
- 0
|
191
|
+
hash: -1454681296772684747
|
157
192
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
158
193
|
none: false
|
159
194
|
requirements:
|
160
195
|
- - ! '>='
|
161
196
|
- !ruby/object:Gem::Version
|
162
197
|
version: '0'
|
198
|
+
segments:
|
199
|
+
- 0
|
200
|
+
hash: -1454681296772684747
|
163
201
|
requirements: []
|
164
202
|
rubyforge_project:
|
165
203
|
rubygems_version: 1.8.24
|