mixlib-authentication 1.1.2 → 1.1.4.rc.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/Rakefile +1 -1
- data/lib/mixlib/authentication.rb +6 -0
- data/lib/mixlib/authentication/http_authentication_request.rb +87 -0
- data/lib/mixlib/authentication/signatureverification.rb +135 -51
- data/lib/mixlib/authentication/signedheaderauth.rb +8 -3
- data/spec/mixlib/authentication/http_authentication_request_spec.rb +129 -0
- data/spec/mixlib/authentication/mixlib_authentication_spec.rb +56 -7
- data/spec/spec_helper.rb +23 -0
- metadata +18 -8
data/Rakefile
CHANGED
@@ -0,0 +1,87 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Daniel DeLeo (<dan@opscode.com>)
|
3
|
+
# Copyright:: Copyright (c) 2010 Opscode, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
require 'mixlib/authentication'
|
20
|
+
|
21
|
+
module Mixlib
|
22
|
+
module Authentication
|
23
|
+
class HTTPAuthenticationRequest
|
24
|
+
|
25
|
+
MANDATORY_HEADERS = [:x_ops_sign, :x_ops_userid, :x_ops_timestamp, :host, :x_ops_content_hash]
|
26
|
+
|
27
|
+
attr_reader :request
|
28
|
+
|
29
|
+
def initialize(request)
|
30
|
+
@request = request
|
31
|
+
@request_signature = nil
|
32
|
+
validate_headers!
|
33
|
+
end
|
34
|
+
|
35
|
+
def headers
|
36
|
+
@headers ||= @request.env.inject({ }) { |memo, kv| memo[$2.gsub(/\-/,"_").downcase.to_sym] = kv[1] if kv[0] =~ /^(HTTP_)(.*)/; memo }
|
37
|
+
end
|
38
|
+
|
39
|
+
def http_method
|
40
|
+
@request.method.to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
def path
|
44
|
+
@request.path.to_s
|
45
|
+
end
|
46
|
+
|
47
|
+
def signing_description
|
48
|
+
headers[:x_ops_sign].chomp
|
49
|
+
end
|
50
|
+
|
51
|
+
def user_id
|
52
|
+
headers[:x_ops_userid].chomp
|
53
|
+
end
|
54
|
+
|
55
|
+
def timestamp
|
56
|
+
headers[:x_ops_timestamp].chomp
|
57
|
+
end
|
58
|
+
|
59
|
+
def host
|
60
|
+
headers[:host].chomp
|
61
|
+
end
|
62
|
+
|
63
|
+
def content_hash
|
64
|
+
headers[:x_ops_content_hash].chomp
|
65
|
+
end
|
66
|
+
|
67
|
+
def request_signature
|
68
|
+
unless @request_signature
|
69
|
+
@request_signature = headers.find_all { |h| h[0].to_s =~ /^x_ops_authorization_/ }.sort { |x,y| x.to_s <=> y.to_s}.map { |i| i[1] }.join("\n")
|
70
|
+
Mixlib::Authentication::Log.debug "Reconstituted (user-supplied) request signature: #{@request_signature}"
|
71
|
+
end
|
72
|
+
@request_signature
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
def validate_headers!
|
77
|
+
missing_headers = MANDATORY_HEADERS - headers.keys
|
78
|
+
unless missing_headers.empty?
|
79
|
+
missing_headers.map! { |h| h.to_s.upcase }
|
80
|
+
raise MissingAuthenticationHeader, "missing required authentication header(s) '#{missing_headers.join("', '")}'"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -17,19 +17,54 @@
|
|
17
17
|
# limitations under the License.
|
18
18
|
#
|
19
19
|
|
20
|
-
require 'ostruct'
|
21
20
|
require 'net/http'
|
21
|
+
require 'forwardable'
|
22
22
|
require 'mixlib/authentication'
|
23
|
+
require 'mixlib/authentication/http_authentication_request'
|
23
24
|
require 'mixlib/authentication/signedheaderauth'
|
24
25
|
|
25
26
|
module Mixlib
|
26
27
|
module Authentication
|
28
|
+
SignatureResponse = Struct.new(:name)
|
29
|
+
|
27
30
|
class SignatureVerification
|
31
|
+
extend Forwardable
|
32
|
+
|
33
|
+
def_delegator :@auth_request, :http_method
|
34
|
+
|
35
|
+
def_delegator :@auth_request, :path
|
36
|
+
|
37
|
+
def_delegator :auth_request, :signing_description
|
38
|
+
|
39
|
+
def_delegator :@auth_request, :user_id
|
40
|
+
|
41
|
+
def_delegator :@auth_request, :timestamp
|
42
|
+
|
43
|
+
def_delegator :@auth_request, :host
|
44
|
+
|
45
|
+
def_delegator :@auth_request, :request_signature
|
46
|
+
|
47
|
+
def_delegator :@auth_request, :content_hash
|
48
|
+
|
49
|
+
def_delegator :@auth_request, :request
|
28
50
|
|
29
51
|
include Mixlib::Authentication::SignedHeaderAuth
|
30
|
-
|
31
|
-
attr_reader :hashed_body, :timestamp, :http_method, :path, :user_id
|
32
52
|
|
53
|
+
attr_reader :auth_request
|
54
|
+
|
55
|
+
def initialize(request=nil)
|
56
|
+
@auth_request = HTTPAuthenticationRequest.new(request) if request
|
57
|
+
|
58
|
+
@valid_signature, @valid_timestamp, @valid_content_hash = false, false, false
|
59
|
+
|
60
|
+
@hashed_body = nil
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
def authenticate_user_request(request, user_lookup, time_skew=(15*60))
|
65
|
+
@auth_request = HTTPAuthenticationRequest.new(request)
|
66
|
+
authenticate_request(user_lookup, time_skew)
|
67
|
+
end
|
33
68
|
# Takes the request, boils down the pieces we are interested in,
|
34
69
|
# looks up the user, generates a signature, and compares to
|
35
70
|
# the signature in the request
|
@@ -40,35 +75,105 @@ module Mixlib
|
|
40
75
|
# X-Ops-Timestamp:
|
41
76
|
# X-Ops-Content-Hash:
|
42
77
|
# X-Ops-Authorization-#{line_number}
|
43
|
-
def
|
78
|
+
def authenticate_request(user_secret, time_skew=(15*60))
|
44
79
|
Mixlib::Authentication::Log.debug "Initializing header auth : #{request.inspect}"
|
45
|
-
|
46
|
-
|
47
|
-
|
80
|
+
|
81
|
+
@user_secret = user_secret
|
82
|
+
@allowed_time_skew = time_skew # in seconds
|
48
83
|
|
49
84
|
begin
|
50
|
-
@
|
51
|
-
@http_method = request.method.to_s
|
52
|
-
@path = request.path.to_s
|
53
|
-
@signing_description = headers[:x_ops_sign].chomp
|
54
|
-
@user_id = headers[:x_ops_userid].chomp
|
55
|
-
@timestamp = headers[:x_ops_timestamp].chomp
|
56
|
-
@host = headers[:host].chomp
|
57
|
-
@content_hash = headers[:x_ops_content_hash].chomp
|
58
|
-
@user_secret = user_lookup
|
59
|
-
|
60
|
-
# The authorization header is a Base64-encoded version of an RSA signature.
|
61
|
-
# The client sent it on multiple header lines, starting at index 1 -
|
62
|
-
# X-Ops-Authorization-1, X-Ops-Authorization-2, etc. Pull them out and
|
63
|
-
# concatenate.
|
64
|
-
|
65
|
-
# if there are 11 headers, the sort breaks - it becomes lexicographic sort rather than numeric [cb]
|
66
|
-
@request_signature = headers.find_all { |h| h[0].to_s =~ /^x_ops_authorization_/ }.sort { |x,y| x.to_s <=> y.to_s}.map { |i| i[1] }.join("\n")
|
67
|
-
Mixlib::Authentication::Log.debug "Reconstituted request signature: #{@request_signature}"
|
85
|
+
@auth_request
|
68
86
|
|
69
|
-
#
|
70
|
-
|
71
|
-
|
87
|
+
#BUGBUG Not doing anything with the signing description yet [cb]
|
88
|
+
parse_signing_description
|
89
|
+
|
90
|
+
verify_signature
|
91
|
+
verify_timestamp
|
92
|
+
verify_content_hash
|
93
|
+
|
94
|
+
rescue StandardError=>se
|
95
|
+
raise AuthenticationError,"Failed to authenticate user request. Check your client key and clock: #{se.message}", se.backtrace
|
96
|
+
end
|
97
|
+
|
98
|
+
if valid_request?
|
99
|
+
SignatureResponse.new(user_id)
|
100
|
+
else
|
101
|
+
nil
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def valid_signature?
|
106
|
+
@valid_signature
|
107
|
+
end
|
108
|
+
|
109
|
+
def valid_timestamp?
|
110
|
+
@valid_timestamp
|
111
|
+
end
|
112
|
+
|
113
|
+
def valid_content_hash?
|
114
|
+
@valid_content_hash
|
115
|
+
end
|
116
|
+
|
117
|
+
def valid_request?
|
118
|
+
valid_signature? && valid_timestamp? && valid_content_hash?
|
119
|
+
end
|
120
|
+
|
121
|
+
# The authorization header is a Base64-encoded version of an RSA signature.
|
122
|
+
# The client sent it on multiple header lines, starting at index 1 -
|
123
|
+
# X-Ops-Authorization-1, X-Ops-Authorization-2, etc. Pull them out and
|
124
|
+
# concatenate.
|
125
|
+
def headers
|
126
|
+
@headers ||= request.env.inject({ }) { |memo, kv| memo[$2.gsub(/\-/,"_").downcase.to_sym] = kv[1] if kv[0] =~ /^(HTTP_)(.*)/; memo }
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def assert_required_headers_present
|
132
|
+
MANDATORY_HEADERS.each do |header|
|
133
|
+
unless headers.key?(header)
|
134
|
+
raise MissingAuthenticationHeader, "required authentication header #{header.to_s.upcase} missing"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def verify_signature
|
140
|
+
candidate_block = canonicalize_request
|
141
|
+
request_decrypted_block = @user_secret.public_decrypt(Base64.decode64(request_signature))
|
142
|
+
@valid_signature = (request_decrypted_block == candidate_block)
|
143
|
+
|
144
|
+
# Keep the debug messages lined up so it's easy to scan them
|
145
|
+
Mixlib::Authentication::Log.debug("Verifying request signature:")
|
146
|
+
Mixlib::Authentication::Log.debug(" Expected Block is: '#{candidate_block}'")
|
147
|
+
Mixlib::Authentication::Log.debug("Decrypted block is: '#{request_decrypted_block}'")
|
148
|
+
Mixlib::Authentication::Log.debug("Signatures match? : '#{@valid_signature}'")
|
149
|
+
|
150
|
+
@valid_signature
|
151
|
+
rescue => e
|
152
|
+
Mixlib::Authentication::Log.debug("Failed to verify request signature: #{e.class.name}: #{e.message}")
|
153
|
+
@valid_signature = false
|
154
|
+
end
|
155
|
+
|
156
|
+
def verify_timestamp
|
157
|
+
@valid_timestamp = timestamp_within_bounds?(Time.parse(timestamp), Time.now)
|
158
|
+
end
|
159
|
+
|
160
|
+
def verify_content_hash
|
161
|
+
@valid_content_hash = (content_hash == hashed_body)
|
162
|
+
|
163
|
+
# Keep the debug messages lined up so it's easy to scan them
|
164
|
+
Mixlib::Authentication::Log.debug("Expected content hash is: '#{hashed_body}'")
|
165
|
+
Mixlib::Authentication::Log.debug(" Request Content Hash is: '#{content_hash}'")
|
166
|
+
Mixlib::Authentication::Log.debug(" Hashes match?: #{@valid_content_hash}")
|
167
|
+
|
168
|
+
@valid_content_hash
|
169
|
+
end
|
170
|
+
|
171
|
+
|
172
|
+
# The request signature is based on any file attached, if any. Otherwise
|
173
|
+
# it's based on the body of the request.
|
174
|
+
def hashed_body
|
175
|
+
unless @hashed_body
|
176
|
+
# TODO: tim: 2009-112-28: It'd be nice to remove this special case, and
|
72
177
|
# always hash the entire request body. In the file case it would just be
|
73
178
|
# expanded multipart text - the entire body of the POST.
|
74
179
|
#
|
@@ -106,31 +211,10 @@ module Mixlib
|
|
106
211
|
Mixlib::Authentication::Log.debug "Digesting body: '#{body}'"
|
107
212
|
@hashed_body = digester.hash_string(body)
|
108
213
|
end
|
109
|
-
|
110
|
-
Mixlib::Authentication::Log.debug "Authenticating user : #{user_id}, User secret is : #{@user_secret}, Request signature is :\n#{@request_signature}, Hashed Body is : #{@hashed_body}"
|
111
|
-
|
112
|
-
#BUGBUG Not doing anything with the signing description yet [cb]
|
113
|
-
parse_signing_description
|
114
|
-
candidate_block = canonicalize_request
|
115
|
-
request_decrypted_block = @user_secret.public_decrypt(Base64.decode64(@request_signature))
|
116
|
-
signatures_match = (request_decrypted_block == candidate_block)
|
117
|
-
timeskew_is_acceptable = timestamp_within_bounds?(Time.parse(timestamp), Time.now)
|
118
|
-
hashes_match = @content_hash == hashed_body
|
119
|
-
rescue StandardError=>se
|
120
|
-
raise StandardError,"Failed to authenticate user request. Most likely missing a necessary header: #{se.message}", se.backtrace
|
121
|
-
end
|
122
|
-
|
123
|
-
Mixlib::Authentication::Log.debug "Candidate Block is: '#{candidate_block}'\nRequest decrypted block is: '#{request_decrypted_block}'\nCandidate content hash is: #{hashed_body}\nRequest Content Hash is: '#{@content_hash}'\nSignatures match: #{signatures_match}, Allowed Time Skew: #{timeskew_is_acceptable}, Hashes match?: #{hashes_match}\n"
|
124
|
-
|
125
|
-
if signatures_match and timeskew_is_acceptable and hashes_match
|
126
|
-
OpenStruct.new(:name=>user_id)
|
127
|
-
else
|
128
|
-
nil
|
129
214
|
end
|
215
|
+
@hashed_body
|
130
216
|
end
|
131
|
-
|
132
|
-
private
|
133
|
-
|
217
|
+
|
134
218
|
# Compare the request timestamp with boundary time
|
135
219
|
#
|
136
220
|
#
|
@@ -19,13 +19,13 @@
|
|
19
19
|
|
20
20
|
require 'time'
|
21
21
|
require 'base64'
|
22
|
-
require 'ostruct'
|
23
22
|
require 'digest/sha1'
|
24
23
|
require 'mixlib/authentication'
|
25
24
|
require 'mixlib/authentication/digester'
|
26
25
|
|
27
26
|
module Mixlib
|
28
27
|
module Authentication
|
28
|
+
|
29
29
|
module SignedHeaderAuth
|
30
30
|
|
31
31
|
SIGNING_DESCRIPTION = 'version=1.0'
|
@@ -34,7 +34,7 @@ module Mixlib
|
|
34
34
|
# with the simple OpenStruct extended with the auth functions
|
35
35
|
class << self
|
36
36
|
def signing_object(args={ })
|
37
|
-
|
37
|
+
SigningObject.new(args[:http_method], args[:path], args[:body], args[:host], args[:timestamp], args[:user_id], args[:file])
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
@@ -107,7 +107,7 @@ module Mixlib
|
|
107
107
|
# ====Parameters
|
108
108
|
#
|
109
109
|
def parse_signing_description
|
110
|
-
parts =
|
110
|
+
parts = signing_description.strip.split(";").inject({ }) do |memo, part|
|
111
111
|
field_name, field_value = part.split("=")
|
112
112
|
memo[field_name.to_sym] = field_value.strip
|
113
113
|
memo
|
@@ -122,5 +122,10 @@ module Mixlib
|
|
122
122
|
private :canonical_time, :canonical_path, :parse_signing_description, :digester
|
123
123
|
|
124
124
|
end
|
125
|
+
|
126
|
+
class SigningObject < Struct.new(:http_method, :path, :body, :host, :timestamp, :user_id, :file)
|
127
|
+
include SignedHeaderAuth
|
128
|
+
end
|
129
|
+
|
125
130
|
end
|
126
131
|
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# Author:: Daniel DeLeo (<dan@opscode.com>)
|
2
|
+
# Copyright:: Copyright (c) 2010 Opscode, Inc.
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..','..','spec_helper'))
|
19
|
+
|
20
|
+
require 'mixlib/authentication'
|
21
|
+
require 'mixlib/authentication/http_authentication_request'
|
22
|
+
require 'ostruct'
|
23
|
+
require 'pp'
|
24
|
+
|
25
|
+
describe Mixlib::Authentication::HTTPAuthenticationRequest do
|
26
|
+
before do
|
27
|
+
request = Struct.new(:env, :method, :path)
|
28
|
+
|
29
|
+
@timestamp_iso8601 = "2009-01-01T12:00:00Z"
|
30
|
+
@x_ops_content_hash = "DFteJZPVv6WKdQmMqZUQUumUyRs="
|
31
|
+
@user_id = "spec-user"
|
32
|
+
@http_x_ops_lines = [
|
33
|
+
"jVHrNniWzpbez/eGWjFnO6lINRIuKOg40ZTIQudcFe47Z9e/HvrszfVXlKG4",
|
34
|
+
"NMzYZgyooSvU85qkIUmKuCqgG2AIlvYa2Q/2ctrMhoaHhLOCWWoqYNMaEqPc",
|
35
|
+
"3tKHE+CfvP+WuPdWk4jv4wpIkAz6ZLxToxcGhXmZbXpk56YTmqgBW2cbbw4O",
|
36
|
+
"IWPZDHSiPcw//AYNgW1CCDptt+UFuaFYbtqZegcBd2n/jzcWODA7zL4KWEUy",
|
37
|
+
"9q4rlh/+1tBReg60QdsmDRsw/cdO1GZrKtuCwbuD4+nbRdVBKv72rqHX9cu0",
|
38
|
+
"utju9jzczCyB+sSAQWrxSsXB/b8vV2qs0l4VD2ML+w=="]
|
39
|
+
@merb_headers = {
|
40
|
+
# These are used by signatureverification. An arbitrary sampling of non-HTTP_*
|
41
|
+
# headers are in here to exercise that code path.
|
42
|
+
"HTTP_HOST"=>"127.0.0.1",
|
43
|
+
"HTTP_X_OPS_SIGN"=>"version=1.0",
|
44
|
+
"HTTP_X_OPS_REQUESTID"=>"127.0.0.1 1258566194.85386",
|
45
|
+
"HTTP_X_OPS_TIMESTAMP"=>@timestamp_iso8601,
|
46
|
+
"HTTP_X_OPS_CONTENT_HASH"=>@x_ops_content_hash,
|
47
|
+
"HTTP_X_OPS_USERID"=>@user_id,
|
48
|
+
"HTTP_X_OPS_AUTHORIZATION_1"=>@http_x_ops_lines[0],
|
49
|
+
"HTTP_X_OPS_AUTHORIZATION_2"=>@http_x_ops_lines[1],
|
50
|
+
"HTTP_X_OPS_AUTHORIZATION_3"=>@http_x_ops_lines[2],
|
51
|
+
"HTTP_X_OPS_AUTHORIZATION_4"=>@http_x_ops_lines[3],
|
52
|
+
"HTTP_X_OPS_AUTHORIZATION_5"=>@http_x_ops_lines[4],
|
53
|
+
"HTTP_X_OPS_AUTHORIZATION_6"=>@http_x_ops_lines[5],
|
54
|
+
|
55
|
+
# Random sampling
|
56
|
+
"REMOTE_ADDR"=>"127.0.0.1",
|
57
|
+
"PATH_INFO"=>"/organizations/local-test-org/cookbooks",
|
58
|
+
"REQUEST_PATH"=>"/organizations/local-test-org/cookbooks",
|
59
|
+
"CONTENT_TYPE"=>"multipart/form-data; boundary=----RubyMultipartClient6792ZZZZZ",
|
60
|
+
"CONTENT_LENGTH"=>"394",
|
61
|
+
}
|
62
|
+
@request = request.new(@merb_headers, "POST", '/nodes')
|
63
|
+
@http_authentication_request = Mixlib::Authentication::HTTPAuthenticationRequest.new(@request)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "normalizes the headers to lowercase symbols" do
|
67
|
+
expected = {:host=>"127.0.0.1",
|
68
|
+
:x_ops_sign=>"version=1.0",
|
69
|
+
:x_ops_requestid=>"127.0.0.1 1258566194.85386",
|
70
|
+
:x_ops_timestamp=>"2009-01-01T12:00:00Z",
|
71
|
+
:x_ops_content_hash=>"DFteJZPVv6WKdQmMqZUQUumUyRs=",
|
72
|
+
:x_ops_userid=>"spec-user",
|
73
|
+
:x_ops_authorization_1=>"jVHrNniWzpbez/eGWjFnO6lINRIuKOg40ZTIQudcFe47Z9e/HvrszfVXlKG4",
|
74
|
+
:x_ops_authorization_2=>"NMzYZgyooSvU85qkIUmKuCqgG2AIlvYa2Q/2ctrMhoaHhLOCWWoqYNMaEqPc",
|
75
|
+
:x_ops_authorization_3=>"3tKHE+CfvP+WuPdWk4jv4wpIkAz6ZLxToxcGhXmZbXpk56YTmqgBW2cbbw4O",
|
76
|
+
:x_ops_authorization_4=>"IWPZDHSiPcw//AYNgW1CCDptt+UFuaFYbtqZegcBd2n/jzcWODA7zL4KWEUy",
|
77
|
+
:x_ops_authorization_5=>"9q4rlh/+1tBReg60QdsmDRsw/cdO1GZrKtuCwbuD4+nbRdVBKv72rqHX9cu0",
|
78
|
+
:x_ops_authorization_6=>"utju9jzczCyB+sSAQWrxSsXB/b8vV2qs0l4VD2ML+w=="}
|
79
|
+
@http_authentication_request.headers.should == expected
|
80
|
+
end
|
81
|
+
|
82
|
+
it "raises an error when not all required headers are given" do
|
83
|
+
@merb_headers.delete("HTTP_X_OPS_SIGN")
|
84
|
+
exception = Mixlib::Authentication::MissingAuthenticationHeader
|
85
|
+
auth_req = Mixlib::Authentication::HTTPAuthenticationRequest.new(@request)
|
86
|
+
lambda {auth_req.validate_headers!}.should raise_error(exception)
|
87
|
+
end
|
88
|
+
|
89
|
+
it "extracts the path from the request" do
|
90
|
+
@http_authentication_request.path.should == '/nodes'
|
91
|
+
end
|
92
|
+
|
93
|
+
it "extracts the request method from the request" do
|
94
|
+
@http_authentication_request.http_method.should == 'POST'
|
95
|
+
end
|
96
|
+
|
97
|
+
it "extracts the signing description from the request headers" do
|
98
|
+
@http_authentication_request.signing_description.should == 'version=1.0'
|
99
|
+
end
|
100
|
+
|
101
|
+
it "extracts the user_id from the request headers" do
|
102
|
+
@http_authentication_request.user_id.should == 'spec-user'
|
103
|
+
end
|
104
|
+
|
105
|
+
it "extracts the timestamp from the request headers" do
|
106
|
+
@http_authentication_request.timestamp.should == "2009-01-01T12:00:00Z"
|
107
|
+
end
|
108
|
+
|
109
|
+
it "extracts the host from the request headers" do
|
110
|
+
@http_authentication_request.host.should == "127.0.0.1"
|
111
|
+
end
|
112
|
+
|
113
|
+
it "extracts the content hash from the request headers" do
|
114
|
+
@http_authentication_request.content_hash.should == "DFteJZPVv6WKdQmMqZUQUumUyRs="
|
115
|
+
end
|
116
|
+
|
117
|
+
it "rebuilds the request signature from the headers" do
|
118
|
+
expected=<<-SIG
|
119
|
+
jVHrNniWzpbez/eGWjFnO6lINRIuKOg40ZTIQudcFe47Z9e/HvrszfVXlKG4
|
120
|
+
NMzYZgyooSvU85qkIUmKuCqgG2AIlvYa2Q/2ctrMhoaHhLOCWWoqYNMaEqPc
|
121
|
+
3tKHE+CfvP+WuPdWk4jv4wpIkAz6ZLxToxcGhXmZbXpk56YTmqgBW2cbbw4O
|
122
|
+
IWPZDHSiPcw//AYNgW1CCDptt+UFuaFYbtqZegcBd2n/jzcWODA7zL4KWEUy
|
123
|
+
9q4rlh/+1tBReg60QdsmDRsw/cdO1GZrKtuCwbuD4+nbRdVBKv72rqHX9cu0
|
124
|
+
utju9jzczCyB+sSAQWrxSsXB/b8vV2qs0l4VD2ML+w==
|
125
|
+
SIG
|
126
|
+
@http_authentication_request.request_signature.should == expected.chomp
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
@@ -17,9 +17,7 @@
|
|
17
17
|
# limitations under the License.
|
18
18
|
#
|
19
19
|
|
20
|
-
|
21
|
-
$:.unshift File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "..", "mixlib-log", "lib")) # mixlib-log/log
|
22
|
-
|
20
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..','..','spec_helper'))
|
23
21
|
require 'rubygems'
|
24
22
|
|
25
23
|
require 'ostruct'
|
@@ -64,6 +62,7 @@ class MockFile
|
|
64
62
|
end
|
65
63
|
|
66
64
|
# Uncomment this to get some more info from the methods we're testing.
|
65
|
+
#Mixlib::Authentication::Log.logger = Logger.new(STDERR)
|
67
66
|
#Mixlib::Authentication::Log.level :debug
|
68
67
|
|
69
68
|
describe "Mixlib::Authentication::SignedHeaderAuth" do
|
@@ -152,11 +151,28 @@ describe "Mixlib::Authentication::SignatureVerification" do
|
|
152
151
|
mock_request = MockRequest.new(PATH, request_params, PASSENGER_HEADERS, "")
|
153
152
|
Time.should_receive(:now).at_least(:once).and_return(TIMESTAMP_OBJ)
|
154
153
|
|
155
|
-
|
156
|
-
res =
|
154
|
+
auth_req = Mixlib::Authentication::SignatureVerification.new
|
155
|
+
res = auth_req.authenticate_user_request(mock_request, @user_private_key)
|
157
156
|
res.should_not be_nil
|
158
157
|
end
|
159
158
|
|
159
|
+
it "shouldn't authenticate if an Authorization header is missing" do
|
160
|
+
headers = MERB_HEADERS.clone
|
161
|
+
headers.delete("HTTP_X_OPS_SIGN")
|
162
|
+
|
163
|
+
mock_request = MockRequest.new(PATH, MERB_REQUEST_PARAMS, headers, BODY)
|
164
|
+
Time.stub!(:now).and_return(TIMESTAMP_OBJ)
|
165
|
+
|
166
|
+
auth_req = Mixlib::Authentication::SignatureVerification.new
|
167
|
+
lambda {auth_req.authenticate_user_request(mock_request, @user_private_key)}.should raise_error(Mixlib::Authentication::AuthenticationError)
|
168
|
+
|
169
|
+
auth_req.should_not be_a_valid_request
|
170
|
+
auth_req.should_not be_a_valid_timestamp
|
171
|
+
auth_req.should_not be_a_valid_signature
|
172
|
+
auth_req.should_not be_a_valid_content_hash
|
173
|
+
end
|
174
|
+
|
175
|
+
|
160
176
|
it "shouldn't authenticate if Authorization header is wrong" do
|
161
177
|
headers = MERB_HEADERS.clone
|
162
178
|
headers["HTTP_X_OPS_CONTENT_HASH"] += "_"
|
@@ -164,9 +180,42 @@ describe "Mixlib::Authentication::SignatureVerification" do
|
|
164
180
|
mock_request = MockRequest.new(PATH, MERB_REQUEST_PARAMS, headers, BODY)
|
165
181
|
Time.should_receive(:now).at_least(:once).and_return(TIMESTAMP_OBJ)
|
166
182
|
|
167
|
-
|
168
|
-
res =
|
183
|
+
auth_req = Mixlib::Authentication::SignatureVerification.new
|
184
|
+
res = auth_req.authenticate_user_request(mock_request, @user_private_key)
|
185
|
+
res.should be_nil
|
186
|
+
|
187
|
+
auth_req.should_not be_a_valid_request
|
188
|
+
auth_req.should be_a_valid_timestamp
|
189
|
+
auth_req.should be_a_valid_signature
|
190
|
+
auth_req.should_not be_a_valid_content_hash
|
191
|
+
end
|
192
|
+
|
193
|
+
it "shouldn't authenticate if the timestamp is not within bounds" do
|
194
|
+
mock_request = MockRequest.new(PATH, MERB_REQUEST_PARAMS, MERB_HEADERS, BODY)
|
195
|
+
Time.should_receive(:now).at_least(:once).and_return(TIMESTAMP_OBJ - 1000)
|
196
|
+
|
197
|
+
auth_req = Mixlib::Authentication::SignatureVerification.new
|
198
|
+
res = auth_req.authenticate_user_request(mock_request, @user_private_key)
|
199
|
+
res.should be_nil
|
200
|
+
auth_req.should_not be_a_valid_request
|
201
|
+
auth_req.should_not be_a_valid_timestamp
|
202
|
+
auth_req.should be_a_valid_signature
|
203
|
+
auth_req.should be_a_valid_content_hash
|
204
|
+
end
|
205
|
+
|
206
|
+
it "shouldn't authenticate if the signature is wrong" do
|
207
|
+
headers = MERB_HEADERS.dup
|
208
|
+
headers["HTTP_X_OPS_AUTHORIZATION_1"] = "epicfail"
|
209
|
+
mock_request = MockRequest.new(PATH, MERB_REQUEST_PARAMS, headers, BODY)
|
210
|
+
Time.should_receive(:now).at_least(:once).and_return(TIMESTAMP_OBJ)
|
211
|
+
|
212
|
+
auth_req = Mixlib::Authentication::SignatureVerification.new
|
213
|
+
res = auth_req.authenticate_user_request(mock_request, @user_private_key)
|
169
214
|
res.should be_nil
|
215
|
+
auth_req.should_not be_a_valid_request
|
216
|
+
auth_req.should_not be_a_valid_signature
|
217
|
+
auth_req.should be_a_valid_timestamp
|
218
|
+
auth_req.should be_a_valid_content_hash
|
170
219
|
end
|
171
220
|
|
172
221
|
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Tim Hinderliter (<tim@opscode.com>)
|
3
|
+
# Author:: Christopher Walters (<cw@opscode.com>)
|
4
|
+
# Copyright:: Copyright (c) 2009, 2010 Opscode, Inc.
|
5
|
+
# License:: Apache License, Version 2.0
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
#
|
19
|
+
|
20
|
+
$:.unshift File.expand_path(File.join(File.dirname(__FILE__), "..", "lib")) # lib in mixlib-authentication
|
21
|
+
$:.unshift File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "mixlib-log", "lib")) # mixlib-log/log
|
22
|
+
|
23
|
+
require 'rubygems'
|
metadata
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mixlib-authentication
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
prerelease:
|
4
|
+
prerelease: true
|
5
5
|
segments:
|
6
6
|
- 1
|
7
7
|
- 1
|
8
|
-
-
|
9
|
-
|
8
|
+
- 4
|
9
|
+
- rc
|
10
|
+
- 1
|
11
|
+
version: 1.1.4.rc.1
|
10
12
|
platform: ruby
|
11
13
|
authors:
|
12
14
|
- Opscode, Inc.
|
@@ -14,13 +16,14 @@ autorequire: mixlib-authentication
|
|
14
16
|
bindir: bin
|
15
17
|
cert_chain: []
|
16
18
|
|
17
|
-
date: 2010-
|
19
|
+
date: 2010-07-23 00:00:00 -07:00
|
18
20
|
default_executable:
|
19
21
|
dependencies:
|
20
22
|
- !ruby/object:Gem::Dependency
|
21
23
|
name: mixlib-log
|
22
24
|
prerelease: false
|
23
25
|
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
24
27
|
requirements:
|
25
28
|
- - ">="
|
26
29
|
- !ruby/object:Gem::Version
|
@@ -45,11 +48,14 @@ files:
|
|
45
48
|
- Rakefile
|
46
49
|
- NOTICE
|
47
50
|
- lib/mixlib/authentication/digester.rb
|
51
|
+
- lib/mixlib/authentication/http_authentication_request.rb
|
48
52
|
- lib/mixlib/authentication/signatureverification.rb
|
49
53
|
- lib/mixlib/authentication/signedheaderauth.rb
|
50
54
|
- lib/mixlib/authentication.rb
|
55
|
+
- spec/mixlib/authentication/http_authentication_request_spec.rb
|
51
56
|
- spec/mixlib/authentication/mixlib_authentication_spec.rb
|
52
57
|
- spec/spec.opts
|
58
|
+
- spec/spec_helper.rb
|
53
59
|
has_rdoc: true
|
54
60
|
homepage: http://www.opscode.com
|
55
61
|
licenses: []
|
@@ -60,6 +66,7 @@ rdoc_options: []
|
|
60
66
|
require_paths:
|
61
67
|
- lib
|
62
68
|
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
63
70
|
requirements:
|
64
71
|
- - ">="
|
65
72
|
- !ruby/object:Gem::Version
|
@@ -67,16 +74,19 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
74
|
- 0
|
68
75
|
version: "0"
|
69
76
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
70
78
|
requirements:
|
71
|
-
- - "
|
79
|
+
- - ">"
|
72
80
|
- !ruby/object:Gem::Version
|
73
81
|
segments:
|
74
|
-
-
|
75
|
-
|
82
|
+
- 1
|
83
|
+
- 3
|
84
|
+
- 1
|
85
|
+
version: 1.3.1
|
76
86
|
requirements: []
|
77
87
|
|
78
88
|
rubyforge_project:
|
79
|
-
rubygems_version: 1.3.
|
89
|
+
rubygems_version: 1.3.7
|
80
90
|
signing_key:
|
81
91
|
specification_version: 3
|
82
92
|
summary: Mixes in simple per-request authentication
|