mauth-client 6.4.3 → 7.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.
@@ -4,14 +4,135 @@ require 'mauth/client/security_token_cacher'
4
4
  require 'mauth/client/signer'
5
5
  require 'openssl'
6
6
 
7
- # methods to verify the authenticity of signed requests and responses locally, retrieving
8
- # public keys from the mAuth service as needed
7
+ # methods to verify the authenticity of signed requests and responses
9
8
 
10
9
  module MAuth
11
10
  class Client
12
- module LocalAuthenticator
11
+ module Authenticator
12
+ ALLOWED_DRIFT_SECONDS = 300
13
+
14
+ # takes an incoming request or response object, and returns whether
15
+ # the object is authentic according to its signature.
16
+ def authentic?(object)
17
+ log_authentication_request(object)
18
+ begin
19
+ authenticate!(object)
20
+ true
21
+ rescue InauthenticError, MAuthNotPresent, MissingV2Error
22
+ false
23
+ end
24
+ end
25
+
26
+ # raises InauthenticError unless the given object is authentic. Will only
27
+ # authenticate with v2 if the environment variable V2_ONLY_AUTHENTICATE
28
+ # is set. Otherwise will fall back to v1 when v2 authentication fails
29
+ def authenticate!(object)
30
+ case object.protocol_version
31
+ when 2
32
+ begin
33
+ authenticate_v2!(object)
34
+ rescue InauthenticError => e
35
+ raise e if v2_only_authenticate?
36
+ raise e if disable_fallback_to_v1_on_v2_failure?
37
+
38
+ object.fall_back_to_mws_signature_info
39
+ raise e unless object.signature
40
+
41
+ log_authentication_request(object)
42
+ authenticate_v1!(object)
43
+ logger.warn('Completed successful authentication attempt after fallback to v1')
44
+ end
45
+ when 1
46
+ if v2_only_authenticate?
47
+ # If v2 is required but not present and v1 is present we raise MissingV2Error
48
+ msg = 'This service requires mAuth v2 mcc-authentication header but only v1 x-mws-authentication is present'
49
+ logger.error(msg)
50
+ raise MissingV2Error, msg
51
+ end
52
+
53
+ authenticate_v1!(object)
54
+ else
55
+ sub_str = v2_only_authenticate? ? '' : 'X-MWS-Authentication header is blank, '
56
+ msg = "Authentication Failed. No mAuth signature present; #{sub_str}MCC-Authentication header is blank."
57
+ logger.warn("mAuth signature not present on #{object.class}. Exception: #{msg}")
58
+ raise MAuthNotPresent, msg
59
+ end
60
+ end
61
+
13
62
  private
14
63
 
64
+ # NOTE: This log is likely consumed downstream and the contents SHOULD NOT
65
+ # be changed without a thorough review of downstream consumers.
66
+ def log_authentication_request(object)
67
+ object_app_uuid = object.signature_app_uuid || '[none provided]'
68
+ object_token = object.signature_token || '[none provided]'
69
+ logger.info(
70
+ 'Mauth-client attempting to authenticate request from app with mauth ' \
71
+ "app uuid #{object_app_uuid} to app with mauth app uuid #{client_app_uuid} " \
72
+ "using version #{object_token}."
73
+ )
74
+ end
75
+
76
+ def log_inauthentic(object, message)
77
+ logger.error("mAuth signature authentication failed for #{object.class}. Exception: #{message}")
78
+ end
79
+
80
+ def time_within_valid_range!(object, time_signed, now = Time.now)
81
+ return if (-ALLOWED_DRIFT_SECONDS..ALLOWED_DRIFT_SECONDS).cover?(now.to_i - time_signed)
82
+
83
+ msg = "Time verification failed. #{time_signed} not within #{ALLOWED_DRIFT_SECONDS} of #{now}"
84
+ log_inauthentic(object, msg)
85
+ raise InauthenticError, msg
86
+ end
87
+
88
+ # V1 helpers
89
+ def authenticate_v1!(object)
90
+ time_valid_v1!(object)
91
+ token_valid_v1!(object)
92
+ signature_valid_v1!(object)
93
+ end
94
+
95
+ def time_valid_v1!(object)
96
+ if object.x_mws_time.nil?
97
+ msg = 'Time verification failed. No x-mws-time present.'
98
+ log_inauthentic(object, msg)
99
+ raise InauthenticError, msg
100
+ end
101
+ time_within_valid_range!(object, object.x_mws_time.to_i)
102
+ end
103
+
104
+ def token_valid_v1!(object)
105
+ return if object.signature_token == MWS_TOKEN
106
+
107
+ msg = "Token verification failed. Expected #{MWS_TOKEN}; token was #{object.signature_token}"
108
+ log_inauthentic(object, msg)
109
+ raise InauthenticError, msg
110
+ end
111
+
112
+ # V2 helpers
113
+ def authenticate_v2!(object)
114
+ time_valid_v2!(object)
115
+ token_valid_v2!(object)
116
+ signature_valid_v2!(object)
117
+ end
118
+
119
+ def time_valid_v2!(object)
120
+ if object.mcc_time.nil?
121
+ msg = 'Time verification failed. No MCC-Time present.'
122
+ log_inauthentic(object, msg)
123
+ raise InauthenticError, msg
124
+ end
125
+ time_within_valid_range!(object, object.mcc_time.to_i)
126
+ end
127
+
128
+ def token_valid_v2!(object)
129
+ return if object.signature_token == MWSV2_TOKEN
130
+
131
+ msg = "Token verification failed. Expected #{MWSV2_TOKEN}; token was #{object.signature_token}"
132
+ log_inauthentic(object, msg)
133
+ raise InauthenticError, msg
134
+ end
135
+
15
136
  def signature_valid_v1!(object)
16
137
  # We are in an unfortunate situation in which Euresource is percent-encoding parts of paths, but not
17
138
  # all of them. In particular, Euresource is percent-encoding all special characters save for '/'.
@@ -5,7 +5,7 @@ require 'mauth/faraday'
5
5
 
6
6
  module MAuth
7
7
  class Client
8
- module LocalAuthenticator
8
+ module Authenticator
9
9
  class SecurityTokenCacher
10
10
  def initialize(mauth_client)
11
11
  @mauth_client = mauth_client
data/lib/mauth/client.rb CHANGED
@@ -7,12 +7,10 @@ require 'json'
7
7
  require 'yaml'
8
8
  require 'mauth/core_ext'
9
9
  require 'mauth/autoload'
10
- require 'mauth/dice_bag/mauth_templates'
11
10
  require 'mauth/version'
12
- require 'mauth/client/authenticator_base'
13
- require 'mauth/client/local_authenticator'
14
- require 'mauth/client/remote_authenticator'
11
+ require 'mauth/client/authenticator'
15
12
  require 'mauth/client/signer'
13
+ require 'mauth/config_env'
16
14
  require 'mauth/errors'
17
15
 
18
16
  module MAuth
@@ -31,66 +29,22 @@ module MAuth
31
29
  AUTH_HEADER_DELIMITER = ';'
32
30
  RACK_ENV_APP_UUID_KEY = 'mauth.app_uuid'
33
31
 
34
- include AuthenticatorBase
32
+ include Authenticator
35
33
  include Signer
36
34
 
37
35
  # returns a configuration (to be passed to MAuth::Client.new) which is configured from information stored in
38
36
  # standard places. all of which is overridable by options in case some defaults do not apply.
39
37
  #
40
38
  # options (may be symbols or strings) - any or all may be omitted where your usage conforms to the defaults.
41
- # - root: the path relative to which this method looks for configuration yaml files. defaults to Rails.root
42
- # if ::Rails is defined, otherwise ENV['RAILS_ROOT'], ENV['RACK_ROOT'], ENV['APP_ROOT'], or '.'
43
- # - environment: the environment, pertaining to top-level keys of the configuration yaml files. by default,
44
- # tries Rails.environment, ENV['RAILS_ENV'], and ENV['RACK_ENV'], and falls back to 'development' if none
45
- # of these are set.
46
- # - mauth_config - MAuth configuration. defaults to load this from a yaml file (see mauth_config_yml option)
47
- # which is assumed to be keyed with the environment at the root. if this is specified, no yaml file is
48
- # loaded, and the given config is passed through with any other defaults applied. at the moment, the only
49
- # other default is to set the logger.
50
- # - mauth_config_yml - specifies where a mauth configuration yaml file can be found. by default checks
51
- # ENV['MAUTH_CONFIG_YML'] or a file 'config/mauth.yml' relative to the root.
39
+ # - mauth_config - MAuth configuration. defaults to load this from environment variables. if this is specified,
40
+ # no environment variable is loaded, and the given config is passed through with any other defaults applied.
41
+ # at the moment, the only other default is to set the logger.
52
42
  # - logger - by default checks ::Rails.logger
53
43
  def self.default_config(options = {})
54
44
  options = options.stringify_symbol_keys
55
45
 
56
- # find the app_root (relative to which we look for yaml files). note that this
57
- # is different than MAuth::Client.root, the root of the mauth-client library.
58
- app_root = options['root'] || begin
59
- if Object.const_defined?(:Rails) && ::Rails.respond_to?(:root) && ::Rails.root
60
- Rails.root
61
- else
62
- ENV['RAILS_ROOT'] || ENV['RACK_ROOT'] || ENV['APP_ROOT'] || '.'
63
- end
64
- end
65
-
66
- # find the environment (with which yaml files are keyed)
67
- env = options['environment'] || begin
68
- if Object.const_defined?(:Rails) && ::Rails.respond_to?(:environment)
69
- Rails.environment
70
- else
71
- ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
72
- end
73
- end
74
-
75
- # find mauth config, given on options, or in a file at
76
- # ENV['MAUTH_CONFIG_YML'] or config/mauth.yml in the app_root
77
- mauth_config = options['mauth_config'] || begin
78
- mauth_config_yml = options['mauth_config_yml']
79
- mauth_config_yml ||= ENV['MAUTH_CONFIG_YML']
80
- default_loc = 'config/mauth.yml'
81
- default_yml = File.join(app_root, default_loc)
82
- mauth_config_yml ||= default_yml if File.exist?(default_yml)
83
- if mauth_config_yml && File.exist?(mauth_config_yml)
84
- whole_config = ConfigFile.load(mauth_config_yml)
85
- errmessage = "#{mauth_config_yml} config has no key #{env} - it has keys #{whole_config.keys.inspect}"
86
- whole_config[env] || raise(MAuth::Client::ConfigurationError, errmessage)
87
- else
88
- raise MAuth::Client::ConfigurationError,
89
- 'could not find mauth config yaml file. this file may be ' \
90
- "placed in #{default_loc}, specified with the mauth_config_yml option, or specified with the " \
91
- 'MAUTH_CONFIG_YML environment variable.'
92
- end
93
- end
46
+ # find mauth config
47
+ mauth_config = options['mauth_config'] || ConfigEnv.load
94
48
 
95
49
  unless mauth_config.key?('logger')
96
50
  # the logger. Rails.logger if it exists, otherwise, no logger
@@ -106,18 +60,13 @@ module MAuth
106
60
 
107
61
  # new client with the given App UUID and public key. config may include the following (all
108
62
  # config keys may be strings or symbols):
109
- # - private_key - required for signing and for authenticating responses. may be omitted if
110
- # only remote authentication of requests is being performed (with
111
- # MAuth::Rack::RequestAuthenticator). may be given as a string or a OpenSSL::PKey::RSA
112
- # instance.
63
+ # - private_key - required for signing and for authenticating responses.
64
+ # may be given as a string or a OpenSSL::PKey::RSA instance.
113
65
  # - app_uuid - required in the same circumstances where a private_key is required
114
- # - mauth_baseurl - required. needed for local authentication to retrieve public keys; needed
115
- # for remote authentication for hopefully obvious reasons.
66
+ # - mauth_baseurl - required. needed to retrieve public keys.
116
67
  # - mauth_api_version - required. only 'v1' exists / is supported as of this writing.
117
68
  # - logger - a Logger to which any useful information will be written. if this is omitted and
118
69
  # Rails.logger exists, that will be used.
119
- # - authenticator - this pretty much never needs to be specified. LocalAuthenticator or
120
- # RemoteRequestAuthenticator will be used as appropriate.
121
70
  def initialize(config = {})
122
71
  # stringify symbol keys
123
72
  given_config = config.stringify_symbol_keys
@@ -159,25 +108,13 @@ module MAuth
159
108
  @config['ssl_certs_path'] = given_config['ssl_certs_path'] if given_config['ssl_certs_path']
160
109
  @config['v2_only_authenticate'] = given_config['v2_only_authenticate'].to_s.casecmp('true').zero?
161
110
  @config['v2_only_sign_requests'] = given_config['v2_only_sign_requests'].to_s.casecmp('true').zero?
162
- @config['disable_fallback_to_v1_on_v2_failure'] =
163
- given_config['disable_fallback_to_v1_on_v2_failure'].to_s.casecmp('true').zero?
164
111
  @config['v1_only_sign_requests'] = given_config['v1_only_sign_requests'].to_s.casecmp('true').zero?
165
-
166
112
  if @config['v2_only_sign_requests'] && @config['v1_only_sign_requests']
167
113
  raise MAuth::Client::ConfigurationError, 'v2_only_sign_requests and v1_only_sign_requests may not both be true'
168
114
  end
169
115
 
170
- # if 'authenticator' was given, don't override that - including if it was given as nil / false
171
- if given_config.key?('authenticator')
172
- @config['authenticator'] = given_config['authenticator']
173
- elsif client_app_uuid && private_key
174
- @config['authenticator'] = LocalAuthenticator
175
- # MAuth::Client can authenticate locally if it's provided a client_app_uuid and private_key
176
- else
177
- # otherwise, it will authenticate remotely (requests only)
178
- @config['authenticator'] = RemoteRequestAuthenticator
179
- end
180
- extend @config['authenticator'] if @config['authenticator']
116
+ @config['disable_fallback_to_v1_on_v2_failure'] =
117
+ given_config['disable_fallback_to_v1_on_v2_failure'].to_s.casecmp('true').zero?
181
118
  end
182
119
 
183
120
  def logger
@@ -246,27 +183,4 @@ module MAuth
246
183
  hash
247
184
  end
248
185
  end
249
-
250
- module ConfigFile
251
- GITHUB_URL = 'https://github.com/mdsol/mauth-client-ruby'
252
- @config = {}
253
-
254
- def self.load(path)
255
- unless File.exist?(path)
256
- raise "File #{path} not found. Please visit #{GITHUB_URL} for details."
257
- end
258
-
259
- @config[path] ||= yaml_safe_load_file(path)
260
- unless @config[path]
261
- raise "File #{path} does not contain proper YAML information. Visit #{GITHUB_URL} for details."
262
- end
263
-
264
- @config[path]
265
- end
266
-
267
- def self.yaml_safe_load_file(path)
268
- yml_data = File.read(path)
269
- YAML.safe_load(yml_data, aliases: true)
270
- end
271
- end
272
186
  end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MAuth
4
+ class ConfigEnv
5
+ GITHUB_URL = 'https://github.com/mdsol/mauth-client-ruby'
6
+
7
+ ENV_STUFF = {
8
+ 'MAUTH_URL' => nil,
9
+ 'MAUTH_API_VERSION' => 'v1',
10
+ 'MAUTH_APP_UUID' => nil,
11
+ 'MAUTH_PRIVATE_KEY' => nil,
12
+ 'MAUTH_PRIVATE_KEY_FILE' => 'config/mauth_key',
13
+ 'MAUTH_V2_ONLY_AUTHENTICATE' => false,
14
+ 'MAUTH_V2_ONLY_SIGN_REQUESTS' => false,
15
+ 'MAUTH_DISABLE_FALLBACK_TO_V1_ON_V2_FAILURE' => false,
16
+ 'MAUTH_V1_ONLY_SIGN_REQUESTS' => true
17
+ }.freeze
18
+
19
+ class << self
20
+ def load
21
+ validate! if production?
22
+
23
+ {
24
+ 'mauth_baseurl' => env[:mauth_url] || 'http://localhost:7000',
25
+ 'mauth_api_version' => env[:mauth_api_version],
26
+ 'app_uuid' => env[:mauth_app_uuid] || 'fb17460e-9868-11e1-8399-0090f5ccb4d3',
27
+ 'private_key' => private_key || generate_private_key,
28
+ 'v2_only_authenticate' => env[:mauth_v2_only_authenticate],
29
+ 'v2_only_sign_requests' => env[:mauth_v2_only_sign_requests],
30
+ 'disable_fallback_to_v1_on_v2_failure' => env[:mauth_disable_fallback_to_v1_on_v2_failure],
31
+ 'v1_only_sign_requests' => env[:mauth_v1_only_sign_requests]
32
+ }
33
+ end
34
+
35
+ private
36
+
37
+ def validate!
38
+ errors = []
39
+ errors << 'The MAUTH_URL environment variable must be set' if env[:mauth_url].nil?
40
+ errors << 'The MAUTH_APP_UUID environment variable must be set' if env[:mauth_app_uuid].nil?
41
+ errors << 'The MAUTH_PRIVATE_KEY environment variable must be set' if env[:mauth_private_key].nil?
42
+ return if errors.empty?
43
+
44
+ errors.map! { |err| "#{err} => See #{GITHUB_URL}" }
45
+ errors.unshift('Invalid MAuth Client configuration:')
46
+ raise errors.join("\n")
47
+ end
48
+
49
+ def env
50
+ @env ||= ENV_STUFF.each_with_object({}) do |(key, default), hsh|
51
+ env_key = key.downcase.to_sym
52
+ hsh[env_key] = ENV.fetch(key, default)
53
+
54
+ case default
55
+ when TrueClass, FalseClass
56
+ hsh[env_key] = hsh[env_key].to_s.casecmp('true').zero?
57
+ end
58
+ end
59
+ end
60
+
61
+ def production?
62
+ environment.to_s.casecmp('production').zero?
63
+ end
64
+
65
+ def environment
66
+ return Rails.environment if Object.const_defined?(:Rails) && ::Rails.respond_to?(:environment)
67
+
68
+ ENV.fetch('RAILS_ENV') { ENV.fetch('RACK_ENV', 'development') }
69
+ end
70
+
71
+ def private_key
72
+ return env[:mauth_private_key] if env[:mauth_private_key]
73
+ return nil unless env[:mauth_private_key_file] && File.readable?(env[:mauth_private_key_file])
74
+
75
+ File.read(env[:mauth_private_key_file])
76
+ end
77
+
78
+ def generate_private_key
79
+ require 'openssl'
80
+ OpenSSL::PKey::RSA.generate(2048).to_s
81
+ end
82
+ end
83
+ end
84
+ end
data/lib/mauth/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MAuth
4
- VERSION = '6.4.3'
4
+ VERSION = '7.0.0'
5
5
  end
data/mauth-client.gemspec CHANGED
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
14
14
  'Includes middleware for Rack and Faraday for incoming and outgoing requests and responses.'
15
15
  spec.homepage = 'https://github.com/mdsol/mauth-client-ruby'
16
16
  spec.license = 'MIT'
17
- spec.required_ruby_version = '>= 2.6.0'
17
+ spec.required_ruby_version = '>= 2.7.0'
18
18
 
19
19
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
20
  spec.bindir = 'exe'
@@ -23,23 +23,8 @@ Gem::Specification.new do |spec|
23
23
 
24
24
  spec.add_dependency 'addressable', '~> 2.0'
25
25
  spec.add_dependency 'coderay', '~> 1.0'
26
- spec.add_dependency 'dice_bag', '>= 0.9', '< 2.0'
27
26
  spec.add_dependency 'faraday', '>= 0.9', '< 3.0'
28
27
  spec.add_dependency 'faraday-http-cache', '>= 2.0', '< 3.0'
29
28
  spec.add_dependency 'rack', '> 2.2.3'
30
29
  spec.add_dependency 'term-ansicolor', '~> 1.0'
31
-
32
- spec.add_development_dependency 'appraisal'
33
- spec.add_development_dependency 'benchmark-ips', '~> 2.7'
34
- spec.add_development_dependency 'bundler', '>= 1.17'
35
- spec.add_development_dependency 'byebug'
36
- spec.add_development_dependency 'rack-test', '~> 1.1.0'
37
- spec.add_development_dependency 'rake', '~> 12.0'
38
- spec.add_development_dependency 'rspec', '~> 3.8'
39
- spec.add_development_dependency 'rubocop', '= 1.25.1'
40
- spec.add_development_dependency 'rubocop-mdsol', '~> 0.1'
41
- spec.add_development_dependency 'rubocop-performance', '= 1.13.2'
42
- spec.add_development_dependency 'simplecov', '~> 0.16'
43
- spec.add_development_dependency 'timecop', '~> 0.9'
44
- spec.add_development_dependency 'webmock', '~> 3.0'
45
30
  end