mauth-client 4.2.1 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/mauth/rack.rb CHANGED
@@ -15,19 +15,26 @@ module MAuth
15
15
  # from this is false, the request is passed to the app with no authentication performed.
16
16
  class RequestAuthenticator < MAuth::Middleware
17
17
  def call(env)
18
- if should_authenticate?(env)
19
- mauth_request = MAuth::Rack::Request.new(env)
20
- begin
21
- if mauth_client.authentic?(mauth_request)
22
- @app.call(env.merge('mauth.app_uuid' => mauth_request.signature_app_uuid, 'mauth.authentic' => true))
23
- else
24
- response_for_inauthentic_request(env)
25
- end
26
- rescue MAuth::UnableToAuthenticateError
27
- response_for_unable_to_authenticate(env)
18
+ mauth_request = MAuth::Rack::Request.new(env)
19
+ env['mauth.protocol_version'] = mauth_request.protocol_version
20
+
21
+ return @app.call(env) unless should_authenticate?(env)
22
+
23
+ if mauth_client.v2_only_authenticate? && mauth_request.protocol_version == 1
24
+ return response_for_missing_v2(env)
25
+ end
26
+
27
+ begin
28
+ if mauth_client.authentic?(mauth_request)
29
+ @app.call(env.merge!(
30
+ 'mauth.app_uuid' => mauth_request.signature_app_uuid,
31
+ 'mauth.authentic' => true
32
+ ))
33
+ else
34
+ response_for_inauthentic_request(env)
28
35
  end
29
- else
30
- @app.call(env)
36
+ rescue MAuth::UnableToAuthenticateError
37
+ response_for_unable_to_authenticate(env)
31
38
  end
32
39
  end
33
40
 
@@ -61,6 +68,18 @@ module MAuth
61
68
  [500, { 'Content-Type' => 'application/json' }, [JSON.pretty_generate(body)]]
62
69
  end
63
70
  end
71
+
72
+ # response when the requests includes V1 headers but does not include V2
73
+ # headers and the V2_ONLY_AUTHENTICATE flag is set.
74
+ def response_for_missing_v2(env)
75
+ handle_head(env) do
76
+ body = {
77
+ 'type' => 'errors:mauth:missing_v2',
78
+ 'title' => 'This service requires mAuth v2 mcc-authentication header. Upgrade your mAuth library and configure it properly.'
79
+ }
80
+ [401, { 'Content-Type' => 'application/json' }, [JSON.pretty_generate(body)]]
81
+ end
82
+ end
64
83
  end
65
84
 
66
85
  # same as MAuth::Rack::RequestAuthenticator, but does not authenticate /app_status
@@ -70,12 +89,24 @@ module MAuth
70
89
  end
71
90
  end
72
91
 
73
- # signs outgoing responses
92
+ # signs outgoing responses with only the protocol used to sign the request.
74
93
  class ResponseSigner < MAuth::Middleware
75
94
  def call(env)
76
95
  unsigned_response = @app.call(env)
77
- signed_response = mauth_client.signed(MAuth::Rack::Response.new(*unsigned_response))
78
- signed_response.status_headers_body
96
+
97
+ method =
98
+ if env['mauth.protocol_version'] == 2
99
+ :signed_v2
100
+ elsif env['mauth.protocol_version'] == 1
101
+ :signed_v1
102
+ else
103
+ # if no protocol was supplied then use `signed` which either signs
104
+ # with both protocol versions (by default) or only v2 when the
105
+ # v2_only_sign_requests flag is set to true.
106
+ :signed
107
+ end
108
+ response = mauth_client.send(method, MAuth::Rack::Response.new(*unsigned_response))
109
+ response.status_headers_body
79
110
  end
80
111
  end
81
112
 
@@ -93,7 +124,12 @@ module MAuth
93
124
  env['rack.input'].rewind
94
125
  body = env['rack.input'].read
95
126
  env['rack.input'].rewind
96
- { verb: env['REQUEST_METHOD'], request_url: env['PATH_INFO'], body: body }
127
+ {
128
+ verb: env['REQUEST_METHOD'],
129
+ request_url: env['PATH_INFO'],
130
+ body: body,
131
+ query_string: env['QUERY_STRING']
132
+ }
97
133
  end
98
134
  end
99
135
 
@@ -104,6 +140,14 @@ module MAuth
104
140
  def x_mws_authentication
105
141
  @env['HTTP_X_MWS_AUTHENTICATION']
106
142
  end
143
+
144
+ def mcc_time
145
+ @env['HTTP_MCC_TIME']
146
+ end
147
+
148
+ def mcc_authentication
149
+ @env['HTTP_MCC_AUTHENTICATION']
150
+ end
107
151
  end
108
152
 
109
153
  # representation of a response composed from a rack response (status, headers, body) which
@@ -4,7 +4,7 @@ module MAuth
4
4
  # module which composes a string to sign.
5
5
  #
6
6
  # includer must provide
7
- # - SIGNATURE_COMPONENTS constant - array of keys to get from #attributes_for_signing
7
+ # - SIGNATURE_COMPONENTS OR SIGNATURE_COMPONENTS_V2 constant - array of keys to get from
8
8
  # - #attributes_for_signing
9
9
  # - #merge_headers (takes a Hash of headers; returns an instance of includer's own class whose
10
10
  # headers have been updated with the argument headers)
@@ -12,15 +12,89 @@ module MAuth
12
12
  # composes a string suitable for private-key signing from the SIGNATURE_COMPONENTS keys of
13
13
  # attributes for signing, which are themselves taken from #attributes_for_signing and
14
14
  # the given argument more_attributes
15
- def string_to_sign(more_attributes)
15
+
16
+ # the string to sign for V1 protocol will be (where LF is line feed character)
17
+ # for requests:
18
+ # string_to_sign =
19
+ # http_verb + <LF> +
20
+ # resource_url_path (no host, port or query string; first "/" is included) + <LF> +
21
+ # request_body + <LF> +
22
+ # app_uuid + <LF> +
23
+ # current_seconds_since_epoch
24
+ #
25
+ # for responses:
26
+ # string_to_sign =
27
+ # status_code_string + <LF> +
28
+ # response_body_digest + <LF> +
29
+ # app_uuid + <LF> +
30
+ # current_seconds_since_epoch
31
+ def string_to_sign_v1(more_attributes)
16
32
  attributes_for_signing = self.attributes_for_signing.merge(more_attributes)
17
33
  missing_attributes = self.class::SIGNATURE_COMPONENTS.select { |key| !attributes_for_signing.key?(key) || attributes_for_signing[key].nil? }
18
34
  missing_attributes.delete(:body) # body may be omitted
19
35
  if missing_attributes.any?
20
36
  raise(UnableToSignError, "Missing required attributes to sign: #{missing_attributes.inspect}\non object to sign: #{inspect}")
21
37
  end
22
- string = self.class::SIGNATURE_COMPONENTS.map { |k| attributes_for_signing[k].to_s }.join("\n")
23
- Digest::SHA512.hexdigest(string)
38
+ self.class::SIGNATURE_COMPONENTS.map { |k| attributes_for_signing[k].to_s }.join("\n")
39
+ end
40
+
41
+ # the string to sign for V2 protocol will be (where LF is line feed character)
42
+ # for requests:
43
+ # string_to_sign =
44
+ # http_verb + <LF> +
45
+ # resource_url_path (no host, port or query string; first "/" is included) + <LF> +
46
+ # request_body_digest + <LF> +
47
+ # app_uuid + <LF> +
48
+ # current_seconds_since_epoch + <LF> +
49
+ # encoded_query_params
50
+ #
51
+ # for responses:
52
+ # string_to_sign =
53
+ # status_code_string + <LF> +
54
+ # response_body_digest + <LF> +
55
+ # app_uuid + <LF> +
56
+ # current_seconds_since_epoch
57
+ def string_to_sign_v2(override_attrs)
58
+ attrs_with_overrides = self.attributes_for_signing.merge(override_attrs)
59
+
60
+ # memoization of body_digest to avoid hashing three times when we call
61
+ # string_to_sign_v2 three times in client#signature_valid_v2!
62
+ # note that if :body is nil we hash an empty string ("")
63
+ attrs_with_overrides[:body_digest] ||= Digest::SHA512.hexdigest(attrs_with_overrides[:body].to_s)
64
+ attrs_with_overrides[:encoded_query_params] = encode_query_string(attrs_with_overrides[:query_string].to_s)
65
+
66
+ missing_attributes = self.class::SIGNATURE_COMPONENTS_V2.reject do |key|
67
+ attrs_with_overrides.dig(key)
68
+ end
69
+
70
+ missing_attributes.delete(:body_digest) # body may be omitted
71
+ missing_attributes.delete(:encoded_query_params) # query_string may be omitted
72
+ if missing_attributes.any?
73
+ raise(UnableToSignError, "Missing required attributes to sign: #{missing_attributes.inspect}\non object to sign: #{inspect}")
74
+ end
75
+
76
+ self.class::SIGNATURE_COMPONENTS_V2.map do |k|
77
+ attrs_with_overrides[k].to_s.force_encoding('UTF-8')
78
+ end.join("\n")
79
+ end
80
+
81
+ # sorts query string parameters by codepoint, uri encodes keys and values,
82
+ # and rejoins parameters into a query string
83
+ def encode_query_string(q_string)
84
+ q_string.split('&').sort.map do |part|
85
+ k, e, v = part.partition('=')
86
+ uri_escape(k) + e + uri_escape(v)
87
+ end.join('&')
88
+ end
89
+
90
+ # percent encodes special characters, preserving character encoding.
91
+ # encodes space as '%20'
92
+ # does not encode A-Z, a-z, 0-9, hyphen ( - ), underscore ( _ ), period ( . ),
93
+ # or tilde ( ~ )
94
+ # NOTE the CGI.escape spec changed in 2.5 to not escape tildes. we gsub
95
+ # tilde encoding back to tildes to account for older Rubies
96
+ def uri_escape(string)
97
+ CGI.escape(string).gsub(/\+|%7E/, '+' => '%20', '%7E' => '~')
24
98
  end
25
99
 
26
100
  def initialize(attributes_for_signing)
@@ -35,13 +109,27 @@ module MAuth
35
109
  # methods for an incoming object which is expected to have a signature.
36
110
  #
37
111
  # includer must provide
112
+ # - #mcc_authentication which returns that header's value
113
+ # - #mcc_time
114
+ # OR
38
115
  # - #x_mws_authentication which returns that header's value
39
116
  # - #x_mws_time
40
117
  module Signed
41
- # returns a hash with keys :token, :app_uuid, and :signature parsed from the X-MWS-Authentication header
118
+ # mauth_client will authenticate with the highest protocol version present and ignore other
119
+ # protocol versions.
120
+ # returns a hash with keys :token, :app_uuid, and :signature parsed from the MCC-Authentication header
121
+ # if it is present and if not then the X-MWS-Authentication header if it is present.
122
+ # Note MWSV2 protocol no longer allows more than one space between the token and app uuid.
42
123
  def signature_info
43
124
  @signature_info ||= begin
44
- match = x_mws_authentication && x_mws_authentication.match(/\A([^ ]+) *([^:]+):([^:]+)\z/)
125
+ match = if mcc_authentication
126
+ mcc_authentication.match(
127
+ /\A(#{MAuth::Client::MWSV2_TOKEN}) ([^:]+):([^:]+)#{MAuth::Client::AUTH_HEADER_DELIMITER}\z/
128
+ )
129
+ elsif x_mws_authentication
130
+ x_mws_authentication.match(/\A([^ ]+) *([^:]+):([^:]+)\z/)
131
+ end
132
+
45
133
  match ? { token: match[1], app_uuid: match[2], signature: match[3] } : {}
46
134
  end
47
135
  end
@@ -57,17 +145,36 @@ module MAuth
57
145
  def signature
58
146
  signature_info[:signature]
59
147
  end
148
+
149
+ def protocol_version
150
+ if !mcc_authentication.to_s.strip.empty?
151
+ 2
152
+ elsif !x_mws_authentication.to_s.strip.empty?
153
+ 1
154
+ end
155
+ end
60
156
  end
61
157
 
62
158
  # virtual base class for signable requests
63
159
  class Request
64
- SIGNATURE_COMPONENTS = [:verb, :request_url, :body, :app_uuid, :time].freeze
160
+ SIGNATURE_COMPONENTS = %i[verb request_url body app_uuid time].freeze
161
+ SIGNATURE_COMPONENTS_V2 =
162
+ %i[
163
+ verb
164
+ request_url
165
+ body_digest
166
+ app_uuid
167
+ time
168
+ encoded_query_params
169
+ ].freeze
170
+
65
171
  include Signable
66
172
  end
67
173
 
68
174
  # virtual base class for signable responses
69
175
  class Response
70
- SIGNATURE_COMPONENTS = [:status_code, :body, :app_uuid, :time].freeze
176
+ SIGNATURE_COMPONENTS = %i[status_code body app_uuid time].freeze
177
+ SIGNATURE_COMPONENTS_V2 = %i[status_code body_digest app_uuid time].freeze
71
178
  include Signable
72
179
  end
73
180
  end
data/lib/mauth/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module MAuth
2
- VERSION = '4.2.1'.freeze
2
+ VERSION = '5.0.0'.freeze
3
3
  end
data/mauth-client.gemspec CHANGED
@@ -18,9 +18,8 @@ Gem::Specification.new do |spec|
18
18
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
19
  spec.require_paths = ['lib']
20
20
 
21
- spec.add_dependency 'faraday', '>= 0.17', '< 1.0'
22
- spec.add_dependency 'faraday_middleware', '>= 0.9', '< 2.0'
23
- spec.add_dependency 'faraday-http-cache', '>= 2.0', '< 3.0'
21
+ spec.add_dependency 'faraday', '~> 0.7'
22
+ spec.add_dependency 'faraday_middleware', '~> 0.9'
24
23
  spec.add_dependency 'term-ansicolor', '~> 1.0'
25
24
  spec.add_dependency 'coderay', '~> 1.0'
26
25
  spec.add_dependency 'rack'
@@ -33,4 +32,5 @@ Gem::Specification.new do |spec|
33
32
  spec.add_development_dependency 'rspec', '~> 3.8'
34
33
  spec.add_development_dependency 'simplecov', '~> 0.16'
35
34
  spec.add_development_dependency 'timecop', '~> 0.9'
35
+ spec.add_development_dependency 'benchmark-ips', '~> 2.7'
36
36
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mauth-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.2.1
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Szenher
@@ -11,68 +11,36 @@ authors:
11
11
  autorequire:
12
12
  bindir: exe
13
13
  cert_chain: []
14
- date: 2021-09-27 00:00:00.000000000 Z
14
+ date: 2019-07-23 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: faraday
18
18
  requirement: !ruby/object:Gem::Requirement
19
19
  requirements:
20
- - - ">="
21
- - !ruby/object:Gem::Version
22
- version: '0.17'
23
- - - "<"
20
+ - - "~>"
24
21
  - !ruby/object:Gem::Version
25
- version: '1.0'
22
+ version: '0.7'
26
23
  type: :runtime
27
24
  prerelease: false
28
25
  version_requirements: !ruby/object:Gem::Requirement
29
26
  requirements:
30
- - - ">="
31
- - !ruby/object:Gem::Version
32
- version: '0.17'
33
- - - "<"
27
+ - - "~>"
34
28
  - !ruby/object:Gem::Version
35
- version: '1.0'
29
+ version: '0.7'
36
30
  - !ruby/object:Gem::Dependency
37
31
  name: faraday_middleware
38
32
  requirement: !ruby/object:Gem::Requirement
39
33
  requirements:
40
- - - ">="
34
+ - - "~>"
41
35
  - !ruby/object:Gem::Version
42
36
  version: '0.9'
43
- - - "<"
44
- - !ruby/object:Gem::Version
45
- version: '2.0'
46
37
  type: :runtime
47
38
  prerelease: false
48
39
  version_requirements: !ruby/object:Gem::Requirement
49
40
  requirements:
50
- - - ">="
41
+ - - "~>"
51
42
  - !ruby/object:Gem::Version
52
43
  version: '0.9'
53
- - - "<"
54
- - !ruby/object:Gem::Version
55
- version: '2.0'
56
- - !ruby/object:Gem::Dependency
57
- name: faraday-http-cache
58
- requirement: !ruby/object:Gem::Requirement
59
- requirements:
60
- - - ">="
61
- - !ruby/object:Gem::Version
62
- version: '2.0'
63
- - - "<"
64
- - !ruby/object:Gem::Version
65
- version: '3.0'
66
- type: :runtime
67
- prerelease: false
68
- version_requirements: !ruby/object:Gem::Requirement
69
- requirements:
70
- - - ">="
71
- - !ruby/object:Gem::Version
72
- version: '2.0'
73
- - - "<"
74
- - !ruby/object:Gem::Version
75
- version: '3.0'
76
44
  - !ruby/object:Gem::Dependency
77
45
  name: term-ansicolor
78
46
  requirement: !ruby/object:Gem::Requirement
@@ -233,6 +201,20 @@ dependencies:
233
201
  - - "~>"
234
202
  - !ruby/object:Gem::Version
235
203
  version: '0.9'
204
+ - !ruby/object:Gem::Dependency
205
+ name: benchmark-ips
206
+ requirement: !ruby/object:Gem::Requirement
207
+ requirements:
208
+ - - "~>"
209
+ - !ruby/object:Gem::Version
210
+ version: '2.7'
211
+ type: :development
212
+ prerelease: false
213
+ version_requirements: !ruby/object:Gem::Requirement
214
+ requirements:
215
+ - - "~>"
216
+ - !ruby/object:Gem::Version
217
+ version: '2.7'
236
218
  description: Client for signing and authentication of requests and responses with
237
219
  mAuth authentication. Includes middleware for Rack and Faraday for incoming and
238
220
  outgoing requests and responses.
@@ -269,11 +251,17 @@ files:
269
251
  - lib/mauth-client.rb
270
252
  - lib/mauth/autoload.rb
271
253
  - lib/mauth/client.rb
254
+ - lib/mauth/client/authenticator_base.rb
255
+ - lib/mauth/client/local_authenticator.rb
256
+ - lib/mauth/client/remote_authenticator.rb
257
+ - lib/mauth/client/security_token_cacher.rb
258
+ - lib/mauth/client/signer.rb
272
259
  - lib/mauth/core_ext.rb
273
260
  - lib/mauth/dice_bag/mauth.rb.dice
274
261
  - lib/mauth/dice_bag/mauth.yml.dice
275
262
  - lib/mauth/dice_bag/mauth_key.dice
276
263
  - lib/mauth/dice_bag/mauth_templates.rb
264
+ - lib/mauth/errors.rb
277
265
  - lib/mauth/fake/rack.rb
278
266
  - lib/mauth/faraday.rb
279
267
  - lib/mauth/middleware.rb
@@ -302,7 +290,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
302
290
  - !ruby/object:Gem::Version
303
291
  version: '0'
304
292
  requirements: []
305
- rubygems_version: 3.0.8
293
+ rubygems_version: 3.0.4
306
294
  signing_key:
307
295
  specification_version: 4
308
296
  summary: Sign and authenticate requests and responses with mAuth authentication.