mauth-client 6.4.2 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.2'
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
- spec.add_dependency 'rack'
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