api-auth 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|