mauth-client 4.2.0 → 5.0.0

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.
@@ -5,6 +5,8 @@ common: &common
5
5
  mauth_api_version: v1
6
6
  app_uuid: <%= configured.mauth_app_uuid! || 'fb17460e-9868-11e1-8399-0090f5ccb4d3' %>
7
7
  private_key_file: config/mauth_key
8
+ v2_only_authenticate: <%= configured.v2_only_authenticate || 'false' %>
9
+ v2_only_sign_requests: <%= configured.v2_only_sign_requests || 'false' %>
8
10
 
9
11
  production:
10
12
  <<: *common
@@ -0,0 +1,29 @@
1
+ module MAuth
2
+ # mAuth client was unable to verify the authenticity of a signed object (this does NOT mean the
3
+ # object is inauthentic). typically due to a failure communicating with the mAuth service, in
4
+ # which case the error may include the attribute mauth_service_response - a response from
5
+ # the mauth service (if it was contactable at all), which may contain more information about
6
+ # the error.
7
+ class UnableToAuthenticateError < StandardError
8
+ # the response from the MAuth service encountered when attempting to retrieve authentication
9
+ attr_accessor :mauth_service_response
10
+ end
11
+
12
+ # used to indicate that an object was expected to be validly signed but its signature does not
13
+ # match its contents, and so is inauthentic.
14
+ class InauthenticError < StandardError; end
15
+
16
+ # Used when the incoming request does not contain any mAuth related information
17
+ class MAuthNotPresent < StandardError; end
18
+
19
+ # required information for signing was missing
20
+ class UnableToSignError < StandardError; end
21
+
22
+ # used when an object has the V1 headers but not the V2 headers and the
23
+ # V2_ONLY_AUTHENTICATE variable is set to true.
24
+ class MissingV2Error < StandardError; end
25
+
26
+ class Client
27
+ class ConfigurationError < StandardError; end
28
+ end
29
+ end
@@ -26,8 +26,10 @@ module MAuth
26
26
  def call(env)
27
27
  retval = if should_authenticate?(env)
28
28
  mauth_request = MAuth::Rack::Request.new(env)
29
+ env['mauth.protocol_version'] = mauth_request.protocol_version
30
+
29
31
  if self.class.is_authentic?
30
- @app.call(env.merge('mauth.app_uuid' => mauth_request.signature_app_uuid, 'mauth.authentic' => true))
32
+ @app.call(env.merge!('mauth.app_uuid' => mauth_request.signature_app_uuid, 'mauth.authentic' => true))
31
33
  else
32
34
  response_for_inauthentic_request(env)
33
35
  end
data/lib/mauth/faraday.rb CHANGED
@@ -36,9 +36,15 @@ module MAuth
36
36
  end
37
37
 
38
38
  def attributes_for_signing
39
- request_url = @request_env[:url].path
40
- request_url = '/' if request_url.empty?
41
- @attributes_for_signing ||= { verb: @request_env[:method].to_s.upcase, request_url: request_url, body: @request_env[:body] }
39
+ @attributes_for_signing ||= begin
40
+ request_url = @request_env[:url].path.empty? ? '/' : @request_env[:url].path
41
+ {
42
+ verb: @request_env[:method].to_s.upcase,
43
+ request_url: request_url,
44
+ body: @request_env[:body],
45
+ query_string: @request_env[:url].query
46
+ }
47
+ end
42
48
  end
43
49
 
44
50
  # takes a Hash of headers; returns an instance of this class whose
@@ -68,6 +74,14 @@ module MAuth
68
74
  def x_mws_authentication
69
75
  @response_env[:response_headers]['x-mws-authentication']
70
76
  end
77
+
78
+ def mcc_time
79
+ @response_env[:response_headers]['mcc-time']
80
+ end
81
+
82
+ def mcc_authentication
83
+ @response_env[:response_headers]['mcc-authentication']
84
+ end
71
85
  end
72
86
 
73
87
  # add MAuth-Client's user-agent to a request
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.0'.freeze
2
+ VERSION = '5.0.0'.freeze
3
3
  end
data/mauth-client.gemspec CHANGED
@@ -18,10 +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'
24
- spec.add_dependency 'oj', '~> 3.0'
21
+ spec.add_dependency 'faraday', '~> 0.7'
22
+ spec.add_dependency 'faraday_middleware', '~> 0.9'
25
23
  spec.add_dependency 'term-ansicolor', '~> 1.0'
26
24
  spec.add_dependency 'coderay', '~> 1.0'
27
25
  spec.add_dependency 'rack'
@@ -34,4 +32,5 @@ Gem::Specification.new do |spec|
34
32
  spec.add_development_dependency 'rspec', '~> 3.8'
35
33
  spec.add_development_dependency 'simplecov', '~> 0.16'
36
34
  spec.add_development_dependency 'timecop', '~> 0.9'
35
+ spec.add_development_dependency 'benchmark-ips', '~> 2.7'
37
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.0
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Szenher
@@ -11,82 +11,36 @@ authors:
11
11
  autorequire:
12
12
  bindir: exe
13
13
  cert_chain: []
14
- date: 2021-06-16 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
- requirement: !ruby/object:Gem::Requirement
39
- requirements:
40
- - - ">="
41
- - !ruby/object:Gem::Version
42
- version: '0.9'
43
- - - "<"
44
- - !ruby/object:Gem::Version
45
- version: '2.0'
46
- type: :runtime
47
- prerelease: false
48
- version_requirements: !ruby/object:Gem::Requirement
49
- requirements:
50
- - - ">="
51
- - !ruby/object:Gem::Version
52
- 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
- - !ruby/object:Gem::Dependency
77
- name: oj
78
32
  requirement: !ruby/object:Gem::Requirement
79
33
  requirements:
80
34
  - - "~>"
81
35
  - !ruby/object:Gem::Version
82
- version: '3.0'
36
+ version: '0.9'
83
37
  type: :runtime
84
38
  prerelease: false
85
39
  version_requirements: !ruby/object:Gem::Requirement
86
40
  requirements:
87
41
  - - "~>"
88
42
  - !ruby/object:Gem::Version
89
- version: '3.0'
43
+ version: '0.9'
90
44
  - !ruby/object:Gem::Dependency
91
45
  name: term-ansicolor
92
46
  requirement: !ruby/object:Gem::Requirement
@@ -247,6 +201,20 @@ dependencies:
247
201
  - - "~>"
248
202
  - !ruby/object:Gem::Version
249
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'
250
218
  description: Client for signing and authentication of requests and responses with
251
219
  mAuth authentication. Includes middleware for Rack and Faraday for incoming and
252
220
  outgoing requests and responses.
@@ -283,11 +251,17 @@ files:
283
251
  - lib/mauth-client.rb
284
252
  - lib/mauth/autoload.rb
285
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
286
259
  - lib/mauth/core_ext.rb
287
260
  - lib/mauth/dice_bag/mauth.rb.dice
288
261
  - lib/mauth/dice_bag/mauth.yml.dice
289
262
  - lib/mauth/dice_bag/mauth_key.dice
290
263
  - lib/mauth/dice_bag/mauth_templates.rb
264
+ - lib/mauth/errors.rb
291
265
  - lib/mauth/fake/rack.rb
292
266
  - lib/mauth/faraday.rb
293
267
  - lib/mauth/middleware.rb
@@ -316,7 +290,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
316
290
  - !ruby/object:Gem::Version
317
291
  version: '0'
318
292
  requirements: []
319
- rubygems_version: 3.0.8
293
+ rubygems_version: 3.0.4
320
294
  signing_key:
321
295
  specification_version: 4
322
296
  summary: Sign and authenticate requests and responses with mAuth authentication.