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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f525040d66da5d6e6d3b5bf59e3d84267efad0d5f91598cc42291c972532c0b7
4
- data.tar.gz: bb573ed893f4f385f1f5c7847c79002d09257dd04d7e848968020245ae5fb82a
3
+ metadata.gz: fb397d4c368ae894af1012c2305891bdfe4c85269e75e54167d4a7482a034f5a
4
+ data.tar.gz: 8dc255a280b8304360b305be500b00bf1a8205293a7061af866a99f2eb69ca10
5
5
  SHA512:
6
- metadata.gz: 8ee4aea0f49aa76ff0f70fce3afb8190edf5fce30b9d2750ab80a7297f7ae0f5ca639e0d3e8b89e2cdd932222b7ab164ac32ccf655ab0c791b356efd99016694
7
- data.tar.gz: 45aa7a70ddf934913348f063b287af103f32cafdc440aeaa00ec95430b883b1a726863aabf2055eb4e3216e7703f7e1a76fc2e511d24a0d11e4e047e89db8825
6
+ metadata.gz: 8e519a163cba44ca112f31d91a2da679601cebeac172eac15704f0ff93cc3ea5a3eba6e654d15cce3cbd2c587cc5bc3e6ae88a1042b84bdaa77f3e71fda01a96
7
+ data.tar.gz: 18e5b9888c5c56ffa2f78197cdca887958e220f6f63e71807393d043508b5cd013ded15031d29230262cdfeb0de86f1f71f18cffd62a64c5719da030a59915d8
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /log
8
8
 
9
9
  /Gemfile.lock
10
+ .byebug_history
@@ -1,3 +1,9 @@
1
+ ## v5.0.0
2
+ - Add support for MWSV2 protocol.
3
+ - Change request signing to sign with both V1 and V2 protocols by default.
4
+ - Update log message for authentication request to include protocol version used.
5
+ - Added `benchmark` rake task to benchmark request signing and authentication.
6
+
1
7
  ## v4.1.1
2
8
  - Use warning level instead of error level for logs about missing mauth header.
3
9
 
@@ -18,3 +18,11 @@ Next, run the tests:
18
18
  ```
19
19
  bundle exec rspec
20
20
  ```
21
+
22
+ ## Running Benchmark
23
+
24
+ If you make changes which could affect performance, please run the benchmark before and after the change as a sanity check.
25
+
26
+ ```
27
+ bundle exec rake benchmark
28
+ ```
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
- # MAuth Client
1
+ # MAuth-Client
2
2
  [![Build Status](https://travis-ci.org/mdsol/mauth-client-ruby.svg?branch=master)](https://travis-ci.org/mdsol/mauth-client-ruby)
3
3
 
4
4
  This gem consists of MAuth::Client, a class to manage the information needed to both sign and authenticate requests
5
5
  and responses, and middlewares for Rack and Faraday which leverage the client's capabilities.
6
6
 
7
- MAuth Client exists in a variety of languages (.Net, Go, R etc.), see the [implementations list](doc/implementations.md) for more info.
7
+ MAuth-Client exists in a variety of languages (.Net, Go, R etc.), see the [implementations list](doc/implementations.md) for more info.
8
8
 
9
9
  ## Installation
10
10
 
@@ -47,12 +47,19 @@ Remote authentication therefore requires more time than local authentication.
47
47
  You will not be able to sign your responses without an `app_uuid` and a private key, so `MAuth::Rack::ResponseSigner` cannot be used.
48
48
 
49
49
  The `mauth_baseurl` and `mauth_api_version` are required in mauth.yml.
50
- These tell the MAuth Client where and how to communicate with the MAuth service.
50
+ These tell the MAuth-Client where and how to communicate with the MAuth service.
51
51
 
52
+ The `v2_only_sign_requests` and `v2_only_authenticate` flags were added to facilitate conversion from the MAuth V1 protocol to the MAuth
53
+ V2 protocol. By default both of these flags are false. See [Protocol Versions](#protocol-versions) below for more information about the different versions.
54
+
55
+ | | v2_only_sign_requests | v2_only_authenticate |
56
+ |-------|------------------------------------|--------------------------------------------------------------------------------------|
57
+ | true | requests are signed with only V2 | requests and responses are authenticated with only V2 |
58
+ | false | requests are signed with V1 and V2 | requests and responses are authenticated with the highest available protocol version |
52
59
 
53
60
  ## Rack Middleware Usage
54
61
 
55
- MAuth Client provides a middleware for request authentication and response verification in mauth/rack.
62
+ MAuth-Client provides a middleware for request authentication and response verification in mauth/rack.
56
63
 
57
64
  ```ruby
58
65
  require 'mauth/rack'
@@ -212,13 +219,13 @@ Create a `MAuth::Request` object from the information in your HTTP request, what
212
219
 
213
220
  ```ruby
214
221
  require 'mauth/request_and_response'
215
- request = MAuth::Request.new(verb: my_verb, request_url: my_request_url, body: my_body)
222
+ request = MAuth::Request.new(verb: my_verb, request_url: my_request_url, body: my_body, query_string: my_query_string)
216
223
  ```
217
224
  `mauth_client.signed_headers(request)` will then return mauth headers which you can apply to your request.
218
225
 
219
226
  ## Local Authentication
220
227
 
221
- When doing local authentication, the mauth client will periodically fetch and cache public keys from MAuth.
228
+ When doing local authentication, the MAuth-Client will periodically fetch and cache public keys from MAuth.
222
229
  Each public key will be cached locally for 60 seconds.
223
230
  Applications which connect frequently to the app will benefit most from this caching strategy.
224
231
  When fetching public keys from MAuth, the following rules apply:
@@ -233,3 +240,8 @@ When fetching public keys from MAuth, the following rules apply:
233
240
  During development classes are typically not cached in Rails applications.
234
241
  If this is the case, be aware that the MAuth-Client middleware object will be instantiated anew for each request;
235
242
  this will cause applications performing local authentication to fetch public keys before each request is authenticated.
243
+
244
+ ## Protocol Versions
245
+
246
+ The mauth V2 protocol was added as of v5.0.0. This protocol updates the string_to_sign to include query parameters, uses different authentication header names, and has a few other changes. See this document for more information: (DOC?). By default MAuth-Client will authenticate incoming requests with only the highest version of the protocol present, and sign their outgoing responses with only the version used to authenticate the request. By default MAuth-Client will sign outgoing requests with both the V1 and V2 protocols, and authenticate their incoming responses with only the highest version of the protocol present.
247
+ If the `v2_only_sign_requests` flag is true all outgoing requests will be signed with only the V2 protocol (outgoing responses will still be signed with whatever protocol used to authenticate the request). If the `v2_only_authenticate` flag is true then MAuth-Client will reject any incoming request or incoming response that does not use the V2 protocol.
data/Rakefile CHANGED
@@ -1,6 +1,113 @@
1
1
  require 'bundler/gem_tasks'
2
2
  require 'rspec/core/rake_task'
3
+ require 'mauth/request_and_response'
4
+ require 'mauth/client'
5
+ require 'securerandom'
6
+ require 'benchmark/ips'
7
+ require 'faraday'
8
+ require 'rspec/mocks/standalone'
3
9
 
4
10
  RSpec::Core::RakeTask.new(:spec)
5
11
 
6
12
  task default: :spec
13
+
14
+ class TestSignableRequest < MAuth::Request
15
+ include MAuth::Signed
16
+ attr_accessor :headers
17
+
18
+ def merge_headers(headers)
19
+ self.class.new(@attributes_for_signing).tap{|r| r.headers = (@headers || {}).merge(headers) }
20
+ end
21
+
22
+ def x_mws_time
23
+ headers['X-MWS-Time']
24
+ end
25
+
26
+ def x_mws_authentication
27
+ headers['X-MWS-Authentication']
28
+ end
29
+
30
+ def mcc_time
31
+ headers['MCC-Time']
32
+ end
33
+
34
+ def mcc_authentication
35
+ headers['MCC-Authentication']
36
+ end
37
+ end
38
+
39
+ desc 'Runs benchmarks for the library.'
40
+ task :benchmark do
41
+ mc = MAuth::Client.new(
42
+ private_key: OpenSSL::PKey::RSA.generate(2048),
43
+ app_uuid: SecureRandom.uuid,
44
+ v2_only_sign_requests: false
45
+ )
46
+ authenticating_mc = MAuth::Client.new(mauth_baseurl: 'http://whatever', mauth_api_version: 'v1')
47
+
48
+ stubs = Faraday::Adapter::Test::Stubs.new
49
+ test_faraday = ::Faraday.new do |builder|
50
+ builder.adapter(:test, stubs)
51
+ end
52
+ stubs.post('/mauth/v1/authentication_tickets.json') { [204, {}, []] }
53
+ allow(Faraday).to receive(:new).and_return(test_faraday)
54
+
55
+ short_body = 'Somewhere in La Mancha, in a place I do not care to remember'
56
+ average_body = short_body * 1_000
57
+ huge_body = average_body * 100
58
+
59
+ qs = 'don=quixote&quixote=don'
60
+
61
+ puts <<-MSG
62
+
63
+ A short request has a body of 60 chars.
64
+ An average request has a body of 60,000 chars.
65
+ A huge request has a body of 6,000,000 chars.
66
+ A qs request has a body of 60 chars and a query string with two k/v pairs.
67
+
68
+ MSG
69
+
70
+ short_request = TestSignableRequest.new(verb: 'PUT', request_url: '/', body: short_body)
71
+ qs_request = TestSignableRequest.new(verb: 'PUT', request_url: '/', body: short_body, query_string: qs)
72
+ average_request = TestSignableRequest.new(verb: 'PUT', request_url: '/', body: average_body)
73
+ huge_request = TestSignableRequest.new(verb: 'PUT', request_url: '/', body: huge_body)
74
+
75
+ v1_short_signed_request = mc.signed_v1(short_request)
76
+ v1_average_signed_request = mc.signed_v1(average_request)
77
+ v1_huge_signed_request = mc.signed_v1(huge_request)
78
+
79
+ v2_short_signed_request = mc.signed_v2(short_request)
80
+ v2_qs_signed_request = mc.signed_v1(qs_request)
81
+ v2_average_signed_request = mc.signed_v2(average_request)
82
+ v2_huge_signed_request = mc.signed_v1(huge_request)
83
+
84
+ Benchmark.ips do |bm|
85
+ bm.report('v1-sign-short') { mc.signed_v1(short_request) }
86
+ bm.report('v2-sign-short') { mc.signed_v2(short_request) }
87
+ bm.report('both-sign-short') { mc.signed(short_request) }
88
+ bm.report('v2-sign-qs') { mc.signed_v2(qs_request) }
89
+ bm.report('both-sign-qs') { mc.signed(qs_request) }
90
+ bm.report('v1-sign-average') { mc.signed_v1(average_request) }
91
+ bm.report('v2-sign-average') { mc.signed_v2(average_request) }
92
+ bm.report('both-sign-average') { mc.signed(average_request) }
93
+ bm.report('v1-sign-huge') { mc.signed_v1(huge_request) }
94
+ bm.report('v2-sign-huge') { mc.signed_v2(huge_request) }
95
+ bm.report('both-sign-huge') { mc.signed(huge_request) }
96
+ bm.compare!
97
+ end
98
+
99
+ puts "i/s means the number of signatures of a message per second.\n\n\n"
100
+
101
+ Benchmark.ips do |bm|
102
+ bm.report('v1-authenticate-short') { authenticating_mc.authentic?(v1_short_signed_request) }
103
+ bm.report('v2-authenticate-short') { authenticating_mc.authentic?(v2_short_signed_request) }
104
+ bm.report('v2-authenticate-qs') { authenticating_mc.authentic?(v2_qs_signed_request) }
105
+ bm.report('v1-authenticate-average') { authenticating_mc.authentic?(v1_average_signed_request) }
106
+ bm.report('v2-authenticate-average') { authenticating_mc.authentic?(v2_average_signed_request) }
107
+ bm.report('v1-authenticate-huge') { authenticating_mc.authentic?(v1_huge_signed_request) }
108
+ bm.report('v2-authenticate-huge') { authenticating_mc.authentic?(v2_huge_signed_request) }
109
+ bm.compare!
110
+ end
111
+
112
+ puts 'i/s means the number of authentication checks of signatures per second.'
113
+ end
@@ -11,7 +11,7 @@ require 'mauth/faraday'
11
11
  require 'yaml'
12
12
  require 'term/ansicolor'
13
13
 
14
- # OPTION PARSER
14
+ # OPTION PARSER
15
15
 
16
16
  require 'optparse'
17
17
 
@@ -53,10 +53,10 @@ end
53
53
  opt_parser.parse!
54
54
  abort(opt_parser.help) unless (2..3).include?(ARGV.size)
55
55
 
56
- # FIND MAUTH CONFIG
56
+ # FIND MAUTH CONFIG
57
57
 
58
58
  possible_mauth_config_files = [
59
- # whoops, I called this MAUTH_CONFIG_YML in one place and MAUTH_CONFIG_YAML in another. supporting both for now.
59
+ # whoops, I called this MAUTH_CONFIG_YML in one place and MAUTH_CONFIG_YAML in another. supporting both for now.
60
60
  ENV['MAUTH_CONFIG_YML'],
61
61
  ENV['MAUTH_CONFIG_YAML'],
62
62
  '~/.mauth_config.yml',
@@ -76,14 +76,14 @@ end
76
76
 
77
77
  mauth_config = MAuth::Client.default_config(:mauth_config_yml => File.expand_path(mauth_config_yml))
78
78
 
79
- # INSTANTIATE MAUTH CLIENT
79
+ # INSTANTIATE MAUTH CLIENT
80
80
 
81
81
  logger = Logger.new(STDERR)
82
82
  mauth_client = MAuth::Client.new(mauth_config.merge('logger' => logger))
83
83
 
84
- # OUTPUTTERS FOR FARADAY THAT SHOULD MOVE TO A LIB SOMEWHERE
84
+ # OUTPUTTERS FOR FARADAY THAT SHOULD MOVE TO A LIB SOMEWHERE
85
85
 
86
- # outputs the response body to the given output device (defaulting to STDOUT)
86
+ # outputs the response body to the given output device (defaulting to STDOUT)
87
87
  class FaradayOutputter < Faraday::Middleware
88
88
  def initialize(app, outdev=STDOUT)
89
89
  @app=app
@@ -97,12 +97,12 @@ class FaradayOutputter < Faraday::Middleware
97
97
  end
98
98
  end
99
99
 
100
- # this is to approximate `curl -v`s output. but it's all faked, whereas curl gives you
101
- # the real text written and read for request and response. whatever, close enough.
100
+ # this is to approximate `curl -v`s output. but it's all faked, whereas curl gives you
101
+ # the real text written and read for request and response. whatever, close enough.
102
102
  class FaradayCurlVOutputter < FaradayOutputter
103
103
 
104
- # defines a method with the given name, applying coloring defined by any additional arguments.
105
- # if $options[:color] is set, respects that; otherwise, applies color if the output device is a tty.
104
+ # defines a method with the given name, applying coloring defined by any additional arguments.
105
+ # if $options[:color] is set, respects that; otherwise, applies color if the output device is a tty.
106
106
  def self.color(name, *color_args)
107
107
  define_method(name) do |arg|
108
108
  if color?
@@ -159,8 +159,8 @@ class FaradayCurlVOutputter < FaradayOutputter
159
159
  $options[:color].nil? ? @outdev.tty? : $options[:color]
160
160
  end
161
161
 
162
- # a mapping for each registered CodeRay scanner to the Media Types which represent
163
- # that language. extremely incomplete!
162
+ # a mapping for each registered CodeRay scanner to the Media Types which represent
163
+ # that language. extremely incomplete!
164
164
  CodeRayForMediaTypes = {
165
165
  :c => [],
166
166
  :cpp => [],
@@ -184,7 +184,7 @@ class FaradayCurlVOutputter < FaradayOutputter
184
184
  }
185
185
 
186
186
  # takes a body and a content type; returns the body, with coloring (ansi colors for terminals)
187
- # possibly added, if it's a recognized content type and #color? is true
187
+ # possibly added, if it's a recognized content type and #color? is true
188
188
  def color_body_by_content_type(body, content_type)
189
189
  if body && color?
190
190
  # kinda hacky way to get the media_type. faraday should supply this ...
@@ -207,7 +207,7 @@ class FaradayCurlVOutputter < FaradayOutputter
207
207
  end
208
208
  end
209
209
 
210
- # CONFIGURE THE FARADAY CONNECTION
210
+ # CONFIGURE THE FARADAY CONNECTION
211
211
  faraday_options = {}
212
212
  if $options[:no_ssl_verify]
213
213
  faraday_options[:ssl] = {:verify => false}
@@ -233,8 +233,8 @@ if $options[:content_type]
233
233
  headers['Content-Type'] = $options[:content_type]
234
234
  else
235
235
  if body
236
- # I'd rather not have a default content-type, but if none is set then the HTTP adapter sets this to
237
- # application/x-www-form-urlencoded anyway. application/json is a better default for our purposes.
236
+ # I'd rather not have a default content-type, but if none is set then the HTTP adapter sets this to
237
+ # application/x-www-form-urlencoded anyway. application/json is a better default for our purposes.
238
238
  headers['Content-Type'] = 'application/json'
239
239
  end
240
240
  end
@@ -251,10 +251,10 @@ end
251
251
 
252
252
  begin
253
253
  response = connection.run_request(httpmethod.downcase.to_sym, url, body, headers)
254
- rescue MAuth::InauthenticError, MAuth::UnableToAuthenticateError => e
254
+ rescue MAuth::InauthenticError, MAuth::UnableToAuthenticateError, MAuth::MAuthNotPresent, MAuth::MissingV2Error => e
255
255
  if $options[:color].nil? ? STDERR.tty? : $options[:color]
256
- class_color = Term::ANSIColor.method(e.is_a?(MAuth::InauthenticError) ? :intense_red : :intense_yellow)
257
- message_color = Term::ANSIColor.method(e.is_a?(MAuth::InauthenticError) ? :red : :yellow)
256
+ class_color = Term::ANSIColor.method(e.is_a?(MAuth::UnableToAuthenticateError) ? :intense_yellow : :intense_red)
257
+ message_color = Term::ANSIColor.method(e.is_a?(MAuth::UnableToAuthenticateError) ? :yellow : :red)
258
258
  else
259
259
  class_color = proc{|s| s }
260
260
  message_color = proc{|s| s }
@@ -7,137 +7,98 @@ require 'mauth/core_ext'
7
7
  require 'mauth/autoload'
8
8
  require 'mauth/dice_bag/mauth_templates'
9
9
  require 'mauth/version'
10
+ require 'mauth/client/authenticator_base'
11
+ require 'mauth/client/local_authenticator'
12
+ require 'mauth/client/remote_authenticator'
13
+ require 'mauth/client/signer'
14
+ require 'mauth/errors'
10
15
 
11
16
  module MAuth
17
+ # does operations which require a private key and corresponding app uuid. this is primarily:
18
+ # - signing outgoing requests and responses
19
+ # - authenticating incoming requests and responses, which may require retrieving the appropriate
20
+ # public key from mAuth (which requires a request to mAuth which is signed using the private
21
+ # key)
22
+ #
23
+ # this nominally operates on request and response objects, but really the only requirements are
24
+ # that the object responds to the methods of MAuth::Signable and/or MAuth::Signed (as
25
+ # appropriate)
12
26
  class Client
13
- class << self
14
- # returns a configuration (to be passed to MAuth::Client.new) which is configured from information stored in
15
- # standard places. all of which is overridable by options in case some defaults do not apply.
16
- #
17
- # options (may be symbols or strings) - any or all may be omitted where your usage conforms to the defaults.
18
- # - root: the path relative to which this method looks for configuration yaml files. defaults to Rails.root
19
- # if ::Rails is defined, otherwise ENV['RAILS_ROOT'], ENV['RACK_ROOT'], ENV['APP_ROOT'], or '.'
20
- # - environment: the environment, pertaining to top-level keys of the configuration yaml files. by default,
21
- # tries Rails.environment, ENV['RAILS_ENV'], and ENV['RACK_ENV'], and falls back to 'development' if none
22
- # of these are set.
23
- # - mauth_config - MAuth configuration. defaults to load this from a yaml file (see mauth_config_yml option)
24
- # which is assumed to be keyed with the environment at the root. if this is specified, no yaml file is
25
- # loaded, and the given config is passed through with any other defaults applied. at the moment, the only
26
- # other default is to set the logger.
27
- # - mauth_config_yml - specifies where a mauth configuration yaml file can be found. by default checks
28
- # ENV['MAUTH_CONFIG_YML'] or a file 'config/mauth.yml' relative to the root.
29
- # - logger - by default checks ::Rails.logger
30
- def default_config(options = {})
31
- options = options.stringify_symbol_keys
27
+ MWS_TOKEN = 'MWS'.freeze
28
+ MWSV2_TOKEN = 'MWSV2'.freeze
29
+ AUTH_HEADER_DELIMITER = ';'.freeze
32
30
 
33
- # find the app_root (relative to which we look for yaml files). note that this
34
- # is different than MAuth::Client.root, the root of the mauth-client library.
35
- app_root = options['root'] || begin
36
- if Object.const_defined?('Rails') && ::Rails.respond_to?(:root) && ::Rails.root
37
- Rails.root
38
- else
39
- ENV['RAILS_ROOT'] || ENV['RACK_ROOT'] || ENV['APP_ROOT'] || '.'
40
- end
41
- end
31
+ include AuthenticatorBase
32
+ include Signer
42
33
 
43
- # find the environment (with which yaml files are keyed)
44
- env = options['environment'] || begin
45
- if Object.const_defined?('Rails') && ::Rails.respond_to?(:environment)
46
- Rails.environment
47
- else
48
- ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
49
- end
34
+ # returns a configuration (to be passed to MAuth::Client.new) which is configured from information stored in
35
+ # standard places. all of which is overridable by options in case some defaults do not apply.
36
+ #
37
+ # options (may be symbols or strings) - any or all may be omitted where your usage conforms to the defaults.
38
+ # - root: the path relative to which this method looks for configuration yaml files. defaults to Rails.root
39
+ # if ::Rails is defined, otherwise ENV['RAILS_ROOT'], ENV['RACK_ROOT'], ENV['APP_ROOT'], or '.'
40
+ # - environment: the environment, pertaining to top-level keys of the configuration yaml files. by default,
41
+ # tries Rails.environment, ENV['RAILS_ENV'], and ENV['RACK_ENV'], and falls back to 'development' if none
42
+ # of these are set.
43
+ # - mauth_config - MAuth configuration. defaults to load this from a yaml file (see mauth_config_yml option)
44
+ # which is assumed to be keyed with the environment at the root. if this is specified, no yaml file is
45
+ # loaded, and the given config is passed through with any other defaults applied. at the moment, the only
46
+ # other default is to set the logger.
47
+ # - mauth_config_yml - specifies where a mauth configuration yaml file can be found. by default checks
48
+ # ENV['MAUTH_CONFIG_YML'] or a file 'config/mauth.yml' relative to the root.
49
+ # - logger - by default checks ::Rails.logger
50
+ def self.default_config(options = {})
51
+ options = options.stringify_symbol_keys
52
+
53
+ # find the app_root (relative to which we look for yaml files). note that this
54
+ # is different than MAuth::Client.root, the root of the mauth-client library.
55
+ app_root = options['root'] || begin
56
+ if Object.const_defined?('Rails') && ::Rails.respond_to?(:root) && ::Rails.root
57
+ Rails.root
58
+ else
59
+ ENV['RAILS_ROOT'] || ENV['RACK_ROOT'] || ENV['APP_ROOT'] || '.'
50
60
  end
61
+ end
51
62
 
52
- # find mauth config, given on options, or in a file at
53
- # ENV['MAUTH_CONFIG_YML'] or config/mauth.yml in the app_root
54
- mauth_config = options['mauth_config'] || begin
55
- mauth_config_yml = options['mauth_config_yml']
56
- mauth_config_yml ||= ENV['MAUTH_CONFIG_YML']
57
- default_loc = 'config/mauth.yml'
58
- default_yml = File.join(app_root, default_loc)
59
- mauth_config_yml ||= default_yml if File.exist?(default_yml)
60
- if mauth_config_yml && File.exist?(mauth_config_yml)
61
- whole_config = ConfigFile.load(mauth_config_yml)
62
- errmessage = "#{mauth_config_yml} config has no key #{env} - it has keys #{whole_config.keys.inspect}"
63
- whole_config[env] || raise(MAuth::Client::ConfigurationError, errmessage)
64
- else
65
- raise MAuth::Client::ConfigurationError, "could not find mauth config yaml file. this file may be " \
66
- "placed in #{default_loc}, specified with the mauth_config_yml option, or specified with the " \
67
- "MAUTH_CONFIG_YML environment variable."
68
- end
63
+ # find the environment (with which yaml files are keyed)
64
+ env = options['environment'] || begin
65
+ if Object.const_defined?('Rails') && ::Rails.respond_to?(:environment)
66
+ Rails.environment
67
+ else
68
+ ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
69
+ end
70
+ end
71
+
72
+ # find mauth config, given on options, or in a file at
73
+ # ENV['MAUTH_CONFIG_YML'] or config/mauth.yml in the app_root
74
+ mauth_config = options['mauth_config'] || begin
75
+ mauth_config_yml = options['mauth_config_yml']
76
+ mauth_config_yml ||= ENV['MAUTH_CONFIG_YML']
77
+ default_loc = 'config/mauth.yml'
78
+ default_yml = File.join(app_root, default_loc)
79
+ mauth_config_yml ||= default_yml if File.exist?(default_yml)
80
+ if mauth_config_yml && File.exist?(mauth_config_yml)
81
+ whole_config = ConfigFile.load(mauth_config_yml)
82
+ errmessage = "#{mauth_config_yml} config has no key #{env} - it has keys #{whole_config.keys.inspect}"
83
+ whole_config[env] || raise(MAuth::Client::ConfigurationError, errmessage)
84
+ else
85
+ raise MAuth::Client::ConfigurationError, "could not find mauth config yaml file. this file may be " \
86
+ "placed in #{default_loc}, specified with the mauth_config_yml option, or specified with the " \
87
+ "MAUTH_CONFIG_YML environment variable."
69
88
  end
89
+ end
70
90
 
71
- unless mauth_config.key?('logger')
72
- # the logger. Rails.logger if it exists, otherwise, no logger
73
- mauth_config['logger'] = options['logger'] || begin
74
- if Object.const_defined?('Rails') && ::Rails.respond_to?(:logger)
75
- Rails.logger
76
- end
91
+ unless mauth_config.key?('logger')
92
+ # the logger. Rails.logger if it exists, otherwise, no logger
93
+ mauth_config['logger'] = options['logger'] || begin
94
+ if Object.const_defined?('Rails') && ::Rails.respond_to?(:logger)
95
+ Rails.logger
77
96
  end
78
97
  end
79
-
80
- mauth_config
81
98
  end
82
- end
83
- end
84
-
85
- class ConfigFile
86
- GITHUB_URL = 'https://github.com/mdsol/mauth-client-ruby'.freeze
87
- @config = {}
88
99
 
89
- def self.load(path)
90
- unless File.exist?(path)
91
- raise "File #{path} not found. Please visit #{GITHUB_URL} for details."
92
- end
93
-
94
- @config[path] ||= YAML.load_file(path)
95
- unless @config[path]
96
- raise "File #{path} does not contain proper YAML information. Visit #{GITHUB_URL} for details."
97
- end
98
- @config[path]
100
+ mauth_config
99
101
  end
100
- end
101
- end
102
-
103
- module MAuth
104
- # mAuth client was unable to verify the authenticity of a signed object (this does NOT mean the
105
- # object is inauthentic). typically due to a failure communicating with the mAuth service, in
106
- # which case the error may include the attribute mauth_service_response - a response from
107
- # the mauth service (if it was contactable at all), which may contain more information about
108
- # the error.
109
- class UnableToAuthenticateError < StandardError
110
- # the response from the MAuth service encountered when attempting to retrieve authentication
111
- attr_accessor :mauth_service_response
112
- end
113
-
114
- # used to indicate that an object was expected to be validly signed but its signature does not
115
- # match its contents, and so is inauthentic.
116
- class InauthenticError < StandardError
117
- end
118
-
119
- # Used when the incoming request does not contain any mAuth related information
120
- class MauthNotPresent < StandardError
121
- end
122
-
123
-
124
- # required information for signing was missing
125
- class UnableToSignError < StandardError
126
- end
127
-
128
- # does operations which require a private key and corresponding app uuid. this is primarily:
129
- # - signing outgoing requests and responses
130
- # - authenticating incoming requests and responses, which may require retrieving the appropriate
131
- # public key from mAuth (which requires a request to mAuth which is signed using the private
132
- # key)
133
- #
134
- # this nominally operates on request and response objects, but really the only requirements are
135
- # that the object responds to the methods of MAuth::Signable and/or MAuth::Signed (as
136
- # appropriate)
137
- class Client
138
- class ConfigurationError < StandardError; end
139
-
140
- MWS_TOKEN = 'MWS'.freeze
141
102
 
142
103
  # new client with the given App UUID and public key. config may include the following (all
143
104
  # config keys may be strings or symbols):
@@ -190,6 +151,8 @@ module MAuth
190
151
  request_config.merge!(symbolize_keys(given_config['faraday_options'])) if given_config['faraday_options']
191
152
  @config['faraday_options'] = { request: request_config } || {}
192
153
  @config['ssl_certs_path'] = given_config['ssl_certs_path'] if given_config['ssl_certs_path']
154
+ @config['v2_only_authenticate'] = given_config['v2_only_authenticate'].to_s.downcase == 'true'
155
+ @config['v2_only_sign_requests'] = given_config['v2_only_sign_requests'].to_s.downcase == 'true'
193
156
 
194
157
  # if 'authenticator' was given, don't override that - including if it was given as nil / false
195
158
  if given_config.key?('authenticator')
@@ -234,6 +197,14 @@ module MAuth
234
197
  @config['ssl_certs_path']
235
198
  end
236
199
 
200
+ def v2_only_sign_requests?
201
+ @config['v2_only_sign_requests']
202
+ end
203
+
204
+ def v2_only_authenticate?
205
+ @config['v2_only_authenticate']
206
+ end
207
+
237
208
  def assert_private_key(err)
238
209
  raise err unless private_key
239
210
  end
@@ -255,260 +226,23 @@ module MAuth
255
226
  end
256
227
  hash
257
228
  end
229
+ end
258
230
 
259
- # methods to sign requests and responses. part of MAuth::Client
260
- module Signer
261
- # takes an outgoing request or response object, and returns an object of the same class
262
- # whose headers are updated to include mauth's signature headers
263
- def signed(object, attributes = {})
264
- object.merge_headers(signed_headers(object, attributes))
265
- end
266
-
267
- # takes a signable object (outgoing request or response). returns a hash of headers to be
268
- # applied tothe object which comprise its signature.
269
- def signed_headers(object, attributes = {})
270
- attributes = { time: Time.now.to_i.to_s, app_uuid: client_app_uuid }.merge(attributes)
271
- signature = self.signature(object, attributes)
272
- { 'X-MWS-Authentication' => "#{MWS_TOKEN} #{client_app_uuid}:#{signature}", 'X-MWS-Time' => attributes[:time] }
273
- end
274
-
275
- # takes a signable object (outgoing request or response). returns a mauth signature string
276
- # for that object.
277
- def signature(object, attributes = {})
278
- assert_private_key(UnableToSignError.new("mAuth client cannot sign without a private key!"))
279
- attributes = { time: Time.now.to_i.to_s, app_uuid: client_app_uuid }.merge(attributes)
280
- signature = Base64.encode64(private_key.private_encrypt(object.string_to_sign(attributes))).delete("\n")
281
- end
282
- end
283
- include Signer
284
-
285
- # methods common to RemoteRequestAuthenticator and LocalAuthenticator
286
- module Authenticator
287
- ALLOWED_DRIFT_SECONDS = 300
288
-
289
- # takes an incoming request or response object, and returns whether
290
- # the object is authentic according to its signature.
291
- def authentic?(object)
292
- log_authentication_request(object)
293
- begin
294
- authenticate!(object)
295
- true
296
- rescue InauthenticError, MauthNotPresent
297
- false
298
- end
299
- end
300
-
301
- # raises InauthenticError unless the given object is authentic
302
- def authenticate!(object)
303
- authentication_present!(object)
304
- time_valid!(object)
305
- token_valid!(object)
306
- signature_valid!(object)
307
- rescue MauthNotPresent => e
308
- logger.warn "mAuth signature not present on #{object.class}. Exception: #{e.message}"
309
- raise
310
- rescue InauthenticError => e
311
- logger.error "mAuth signature authentication failed for #{object.class}. Exception: #{e.message}"
312
- raise
313
- rescue UnableToAuthenticateError => e
314
- logger.error "Unable to authenticate with MAuth for #{object.class}. Exception: #{e.message}"
315
- raise
316
- end
317
-
318
- private
319
-
320
- # Note: This log is likely consumed downstream and the contents SHOULD NOT be changed without a thorough review of downstream consumers.
321
- def log_authentication_request(object)
322
- object_app_uuid = object.signature_app_uuid || '[none provided]'
323
- logger.info "Mauth-client attempting to authenticate request from app with mauth app uuid #{object_app_uuid} to app with mauth app uuid #{client_app_uuid}."
324
- end
325
-
326
- def authentication_present!(object)
327
- if object.x_mws_authentication.nil? || object.x_mws_authentication !~ /\S/
328
- raise MauthNotPresent, "Authentication Failed. No mAuth signature present; X-MWS-Authentication header is blank."
329
- end
330
- end
331
-
332
- def time_valid!(object, now = Time.now)
333
- if object.x_mws_time.nil?
334
- raise InauthenticError, "Time verification failed. No x-mws-time present."
335
- elsif !(-ALLOWED_DRIFT_SECONDS..ALLOWED_DRIFT_SECONDS).cover?(now.to_i - object.x_mws_time.to_i)
336
- raise InauthenticError, "Time verification failed. #{object.x_mws_time} not within #{ALLOWED_DRIFT_SECONDS} of #{now}"
337
- end
338
- end
339
-
340
- def token_valid!(object)
341
- unless object.signature_token == MWS_TOKEN
342
- raise InauthenticError, "Token verification failed. Expected #{MWS_TOKEN.inspect}; token was #{object.signature_token}"
343
- end
344
- end
345
- end
346
- include Authenticator
347
-
348
- # methods to verify the authenticity of signed requests and responses locally, retrieving
349
- # public keys from the mAuth service as needed
350
- module LocalAuthenticator
351
- private
352
-
353
- def signature_valid!(object)
354
- # We are in an unfortunate situation in which Euresource is percent-encoding parts of paths, but not
355
- # all of them. In particular, Euresource is percent-encoding all special characters save for '/'.
356
- # Also, unfortunately, Nginx unencodes URIs before sending them off to served applications, though
357
- # other web servers (particularly those we typically use for local testing) do not. The various forms
358
- # of the expected string to sign are meant to cover the main cases.
359
- # TODO: Revisit and simplify this unfortunate situation.
360
-
361
- original_request_uri = object.attributes_for_signing[:request_url]
362
-
363
- # craft an expected string-to-sign without doing any percent-encoding
364
- expected_no_reencoding = object.string_to_sign(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
365
-
366
- # do a simple percent reencoding variant of the path
367
- object.attributes_for_signing[:request_url] = CGI.escape(original_request_uri.to_s)
368
- expected_for_percent_reencoding = object.string_to_sign(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
369
-
370
- # do a moderately complex Euresource-style reencoding of the path
371
- object.attributes_for_signing[:request_url] = euresource_escape(original_request_uri.to_s)
372
- expected_euresource_style_reencoding = object.string_to_sign(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
373
-
374
- # reset the object original request_uri, just in case we need it again
375
- object.attributes_for_signing[:request_url] = original_request_uri
376
-
377
- pubkey = OpenSSL::PKey::RSA.new(retrieve_public_key(object.signature_app_uuid))
378
- begin
379
- actual = pubkey.public_decrypt(Base64.decode64(object.signature))
380
- rescue OpenSSL::PKey::PKeyError
381
- raise InauthenticError, "Public key decryption of signature failed!\n#{$!.class}: #{$!.message}"
382
- end
383
- # TODO: time-invariant comparison instead of #== ?
384
- unless expected_no_reencoding == actual || expected_euresource_style_reencoding == actual || expected_for_percent_reencoding == actual
385
- raise InauthenticError, "Signature verification failed for #{object.class}"
386
- end
387
- end
388
-
389
- # Note: RFC 3986 (https://www.ietf.org/rfc/rfc3986.txt) reserves the forward slash "/"
390
- # and number sign "#" as component delimiters. Since these are valid URI components,
391
- # they are decoded back into characters here to avoid signature invalidation
392
- def euresource_escape(str)
393
- CGI.escape(str).gsub(/%2F|%23/, "%2F" => "/", "%23" => "#")
394
- end
395
-
396
- def retrieve_public_key(app_uuid)
397
- retrieve_security_token(app_uuid)['security_token']['public_key_str']
398
- end
399
-
400
- def retrieve_security_token(app_uuid)
401
- security_token_cacher.get(app_uuid)
402
- end
403
-
404
- def security_token_cacher
405
- @security_token_cacher ||= SecurityTokenCacher.new(self)
406
- end
407
- class SecurityTokenCacher
408
- class ExpirableSecurityToken < Struct.new(:security_token, :create_time)
409
- CACHE_LIFE = 60
410
- def expired?
411
- create_time + CACHE_LIFE < Time.now
412
- end
413
- end
414
- def initialize(mauth_client)
415
- @mauth_client = mauth_client
416
- # TODO: should this be UnableToSignError?
417
- @mauth_client.assert_private_key(UnableToAuthenticateError.new("Cannot fetch public keys from mAuth service without a private key!"))
418
- @cache = {}
419
- require 'thread'
420
- @cache_write_lock = Mutex.new
421
- end
422
-
423
- def get(app_uuid)
424
- if !@cache[app_uuid] || @cache[app_uuid].expired?
425
- # url-encode the app_uuid to prevent trickery like escaping upward with ../../ in a malicious
426
- # app_uuid - probably not exploitable, but this is the right way to do it anyway.
427
- # use UNRESERVED instead of UNSAFE (the default) as UNSAFE doesn't include /
428
- url_encoded_app_uuid = URI.escape(app_uuid, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
429
- begin
430
- response = signed_mauth_connection.get("/mauth/#{@mauth_client.mauth_api_version}/security_tokens/#{url_encoded_app_uuid}.json")
431
- rescue ::Faraday::Error::ConnectionFailed, ::Faraday::Error::TimeoutError
432
- raise UnableToAuthenticateError, "mAuth service did not respond; received #{$!.class}: #{$!.message}"
433
- end
434
- if response.status == 200
435
- begin
436
- security_token = JSON.parse(response.body)
437
- rescue JSON::ParserError
438
- raise UnableToAuthenticateError, "mAuth service responded with unparseable json: #{response.body}\n#{$!.class}: #{$!.message}"
439
- end
440
- @cache_write_lock.synchronize do
441
- @cache[app_uuid] = ExpirableSecurityToken.new(security_token, Time.now)
442
- end
443
- elsif response.status == 404
444
- # signing with a key mAuth doesn't know about is considered inauthentic
445
- raise InauthenticError, "mAuth service responded with 404 looking up public key for #{app_uuid}"
446
- else
447
- @mauth_client.send(:mauth_service_response_error, response)
448
- end
449
- end
450
- @cache[app_uuid].security_token
451
- end
452
-
453
- private
231
+ module ConfigFile
232
+ GITHUB_URL = 'https://github.com/mdsol/mauth-client-ruby'.freeze
233
+ @config = {}
454
234
 
455
- def signed_mauth_connection
456
- require 'faraday'
457
- require 'mauth/faraday'
458
- @mauth_client.faraday_options[:ssl] = { ca_path: @mauth_client.ssl_certs_path } if @mauth_client.ssl_certs_path
459
- @signed_mauth_connection ||= ::Faraday.new(@mauth_client.mauth_baseurl, @mauth_client.faraday_options) do |builder|
460
- builder.use MAuth::Faraday::MAuthClientUserAgent
461
- builder.use MAuth::Faraday::RequestSigner, 'mauth_client' => @mauth_client
462
- builder.adapter ::Faraday.default_adapter
463
- end
464
- end
235
+ def self.load(path)
236
+ unless File.exist?(path)
237
+ raise "File #{path} not found. Please visit #{GITHUB_URL} for details."
465
238
  end
466
- end
467
239
 
468
- # methods for remotely authenticating a request by sending it to the mauth service
469
- module RemoteRequestAuthenticator
470
- private
471
-
472
- # takes an incoming request object (no support for responses currently), and errors if the
473
- # object is not authentic according to its signature
474
- def signature_valid!(object)
475
- raise ArgumentError, "Remote Authenticator can only authenticate requests; received #{object.inspect}" unless object.is_a?(MAuth::Request)
476
- authentication_ticket = {
477
- 'verb' => object.attributes_for_signing[:verb],
478
- 'app_uuid' => object.signature_app_uuid,
479
- 'client_signature' => object.signature,
480
- 'request_url' => object.attributes_for_signing[:request_url],
481
- 'request_time' => object.x_mws_time,
482
- 'b64encoded_body' => Base64.encode64(object.attributes_for_signing[:body] || '')
483
- }
484
- begin
485
- response = mauth_connection.post("/mauth/#{mauth_api_version}/authentication_tickets.json", "authentication_ticket" => authentication_ticket)
486
- rescue ::Faraday::Error::ConnectionFailed, ::Faraday::Error::TimeoutError
487
- raise UnableToAuthenticateError, "mAuth service did not respond; received #{$!.class}: #{$!.message}"
488
- end
489
- if (200..299).cover?(response.status)
490
- nil
491
- elsif response.status == 412 || response.status == 404
492
- # the mAuth service responds with 412 when the given request is not authentically signed.
493
- # older versions of the mAuth service respond with 404 when the given app_uuid
494
- # does not exist, which is also considered to not be authentically signed. newer
495
- # versions of the service respond 412 in all cases, so the 404 check may be removed
496
- # when the old version of the mAuth service is out of service.
497
- raise InauthenticError, "The mAuth service responded with #{response.status}: #{response.body}"
498
- else
499
- mauth_service_response_error(response)
500
- end
240
+ @config[path] ||= YAML.load_file(path)
241
+ unless @config[path]
242
+ raise "File #{path} does not contain proper YAML information. Visit #{GITHUB_URL} for details."
501
243
  end
502
244
 
503
- def mauth_connection
504
- require 'faraday'
505
- require 'faraday_middleware'
506
- @mauth_connection ||= ::Faraday.new(mauth_baseurl, faraday_options) do |builder|
507
- builder.use MAuth::Faraday::MAuthClientUserAgent
508
- builder.use FaradayMiddleware::EncodeJson
509
- builder.adapter ::Faraday.default_adapter
510
- end
511
- end
245
+ @config[path]
512
246
  end
513
247
  end
514
248
  end