mauth-client 4.1.1 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module MAuth
2
- VERSION = '4.1.1'.freeze
2
+ VERSION = '5.0.0'.freeze
3
3
  end
@@ -32,4 +32,5 @@ Gem::Specification.new do |spec|
32
32
  spec.add_development_dependency 'rspec', '~> 3.8'
33
33
  spec.add_development_dependency 'simplecov', '~> 0.16'
34
34
  spec.add_development_dependency 'timecop', '~> 0.9'
35
+ spec.add_development_dependency 'benchmark-ips', '~> 2.7'
35
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.1.1
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Szenher
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: exe
13
13
  cert_chain: []
14
- date: 2019-06-26 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
@@ -201,6 +201,20 @@ dependencies:
201
201
  - - "~>"
202
202
  - !ruby/object:Gem::Version
203
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'
204
218
  description: Client for signing and authentication of requests and responses with
205
219
  mAuth authentication. Includes middleware for Rack and Faraday for incoming and
206
220
  outgoing requests and responses.
@@ -237,11 +251,17 @@ files:
237
251
  - lib/mauth-client.rb
238
252
  - lib/mauth/autoload.rb
239
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
240
259
  - lib/mauth/core_ext.rb
241
260
  - lib/mauth/dice_bag/mauth.rb.dice
242
261
  - lib/mauth/dice_bag/mauth.yml.dice
243
262
  - lib/mauth/dice_bag/mauth_key.dice
244
263
  - lib/mauth/dice_bag/mauth_templates.rb
264
+ - lib/mauth/errors.rb
245
265
  - lib/mauth/fake/rack.rb
246
266
  - lib/mauth/faraday.rb
247
267
  - lib/mauth/middleware.rb