mauth-client 4.2.1 → 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +5 -9
- data/CHANGELOG.md +5 -5
- data/CONTRIBUTING.md +8 -0
- data/README.md +18 -6
- data/Rakefile +107 -0
- data/exe/mauth-client +19 -19
- data/lib/mauth/client/authenticator_base.rb +118 -0
- data/lib/mauth/client/local_authenticator.rb +137 -0
- data/lib/mauth/client/remote_authenticator.rb +75 -0
- data/lib/mauth/client/security_token_cacher.rb +71 -0
- data/lib/mauth/client/signer.rb +67 -0
- data/lib/mauth/client.rb +99 -366
- data/lib/mauth/dice_bag/mauth.yml.dice +2 -0
- data/lib/mauth/errors.rb +29 -0
- data/lib/mauth/fake/rack.rb +3 -1
- data/lib/mauth/faraday.rb +17 -3
- data/lib/mauth/rack.rb +60 -16
- data/lib/mauth/request_and_response.rb +115 -8
- data/lib/mauth/version.rb +1 -1
- data/mauth-client.gemspec +3 -3
- metadata +29 -41
data/lib/mauth/client.rb
CHANGED
@@ -7,139 +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 '
|
11
|
-
require 'mauth/
|
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'
|
12
15
|
|
13
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)
|
14
26
|
class Client
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
#
|
19
|
-
# options (may be symbols or strings) - any or all may be omitted where your usage conforms to the defaults.
|
20
|
-
# - root: the path relative to which this method looks for configuration yaml files. defaults to Rails.root
|
21
|
-
# if ::Rails is defined, otherwise ENV['RAILS_ROOT'], ENV['RACK_ROOT'], ENV['APP_ROOT'], or '.'
|
22
|
-
# - environment: the environment, pertaining to top-level keys of the configuration yaml files. by default,
|
23
|
-
# tries Rails.environment, ENV['RAILS_ENV'], and ENV['RACK_ENV'], and falls back to 'development' if none
|
24
|
-
# of these are set.
|
25
|
-
# - mauth_config - MAuth configuration. defaults to load this from a yaml file (see mauth_config_yml option)
|
26
|
-
# which is assumed to be keyed with the environment at the root. if this is specified, no yaml file is
|
27
|
-
# loaded, and the given config is passed through with any other defaults applied. at the moment, the only
|
28
|
-
# other default is to set the logger.
|
29
|
-
# - mauth_config_yml - specifies where a mauth configuration yaml file can be found. by default checks
|
30
|
-
# ENV['MAUTH_CONFIG_YML'] or a file 'config/mauth.yml' relative to the root.
|
31
|
-
# - logger - by default checks ::Rails.logger
|
32
|
-
def default_config(options = {})
|
33
|
-
options = options.stringify_symbol_keys
|
27
|
+
MWS_TOKEN = 'MWS'.freeze
|
28
|
+
MWSV2_TOKEN = 'MWSV2'.freeze
|
29
|
+
AUTH_HEADER_DELIMITER = ';'.freeze
|
34
30
|
|
35
|
-
|
36
|
-
|
37
|
-
app_root = options['root'] || begin
|
38
|
-
if Object.const_defined?('Rails') && ::Rails.respond_to?(:root) && ::Rails.root
|
39
|
-
Rails.root
|
40
|
-
else
|
41
|
-
ENV['RAILS_ROOT'] || ENV['RACK_ROOT'] || ENV['APP_ROOT'] || '.'
|
42
|
-
end
|
43
|
-
end
|
31
|
+
include AuthenticatorBase
|
32
|
+
include Signer
|
44
33
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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'] || '.'
|
52
60
|
end
|
61
|
+
end
|
53
62
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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."
|
71
88
|
end
|
89
|
+
end
|
72
90
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
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
|
79
96
|
end
|
80
97
|
end
|
81
|
-
|
82
|
-
mauth_config
|
83
98
|
end
|
84
|
-
end
|
85
|
-
end
|
86
99
|
|
87
|
-
|
88
|
-
GITHUB_URL = 'https://github.com/mdsol/mauth-client-ruby'.freeze
|
89
|
-
@config = {}
|
90
|
-
|
91
|
-
def self.load(path)
|
92
|
-
unless File.exist?(path)
|
93
|
-
raise "File #{path} not found. Please visit #{GITHUB_URL} for details."
|
94
|
-
end
|
95
|
-
|
96
|
-
@config[path] ||= YAML.load_file(path)
|
97
|
-
unless @config[path]
|
98
|
-
raise "File #{path} does not contain proper YAML information. Visit #{GITHUB_URL} for details."
|
99
|
-
end
|
100
|
-
@config[path]
|
100
|
+
mauth_config
|
101
101
|
end
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
module MAuth
|
106
|
-
# mAuth client was unable to verify the authenticity of a signed object (this does NOT mean the
|
107
|
-
# object is inauthentic). typically due to a failure communicating with the mAuth service, in
|
108
|
-
# which case the error may include the attribute mauth_service_response - a response from
|
109
|
-
# the mauth service (if it was contactable at all), which may contain more information about
|
110
|
-
# the error.
|
111
|
-
class UnableToAuthenticateError < StandardError
|
112
|
-
# the response from the MAuth service encountered when attempting to retrieve authentication
|
113
|
-
attr_accessor :mauth_service_response
|
114
|
-
end
|
115
|
-
|
116
|
-
# used to indicate that an object was expected to be validly signed but its signature does not
|
117
|
-
# match its contents, and so is inauthentic.
|
118
|
-
class InauthenticError < StandardError
|
119
|
-
end
|
120
|
-
|
121
|
-
# Used when the incoming request does not contain any mAuth related information
|
122
|
-
class MauthNotPresent < StandardError
|
123
|
-
end
|
124
|
-
|
125
|
-
|
126
|
-
# required information for signing was missing
|
127
|
-
class UnableToSignError < StandardError
|
128
|
-
end
|
129
|
-
|
130
|
-
# does operations which require a private key and corresponding app uuid. this is primarily:
|
131
|
-
# - signing outgoing requests and responses
|
132
|
-
# - authenticating incoming requests and responses, which may require retrieving the appropriate
|
133
|
-
# public key from mAuth (which requires a request to mAuth which is signed using the private
|
134
|
-
# key)
|
135
|
-
#
|
136
|
-
# this nominally operates on request and response objects, but really the only requirements are
|
137
|
-
# that the object responds to the methods of MAuth::Signable and/or MAuth::Signed (as
|
138
|
-
# appropriate)
|
139
|
-
class Client
|
140
|
-
class ConfigurationError < StandardError; end
|
141
|
-
|
142
|
-
MWS_TOKEN = 'MWS'.freeze
|
143
102
|
|
144
103
|
# new client with the given App UUID and public key. config may include the following (all
|
145
104
|
# config keys may be strings or symbols):
|
@@ -192,6 +151,8 @@ module MAuth
|
|
192
151
|
request_config.merge!(symbolize_keys(given_config['faraday_options'])) if given_config['faraday_options']
|
193
152
|
@config['faraday_options'] = { request: request_config } || {}
|
194
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'
|
195
156
|
|
196
157
|
# if 'authenticator' was given, don't override that - including if it was given as nil / false
|
197
158
|
if given_config.key?('authenticator')
|
@@ -236,6 +197,14 @@ module MAuth
|
|
236
197
|
@config['ssl_certs_path']
|
237
198
|
end
|
238
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
|
+
|
239
208
|
def assert_private_key(err)
|
240
209
|
raise err unless private_key
|
241
210
|
end
|
@@ -257,259 +226,23 @@ module MAuth
|
|
257
226
|
end
|
258
227
|
hash
|
259
228
|
end
|
229
|
+
end
|
260
230
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
# whose headers are updated to include mauth's signature headers
|
265
|
-
def signed(object, attributes = {})
|
266
|
-
object.merge_headers(signed_headers(object, attributes))
|
267
|
-
end
|
268
|
-
|
269
|
-
# takes a signable object (outgoing request or response). returns a hash of headers to be
|
270
|
-
# applied tothe object which comprise its signature.
|
271
|
-
def signed_headers(object, attributes = {})
|
272
|
-
attributes = { time: Time.now.to_i.to_s, app_uuid: client_app_uuid }.merge(attributes)
|
273
|
-
signature = self.signature(object, attributes)
|
274
|
-
{ 'X-MWS-Authentication' => "#{MWS_TOKEN} #{client_app_uuid}:#{signature}", 'X-MWS-Time' => attributes[:time] }
|
275
|
-
end
|
276
|
-
|
277
|
-
# takes a signable object (outgoing request or response). returns a mauth signature string
|
278
|
-
# for that object.
|
279
|
-
def signature(object, attributes = {})
|
280
|
-
assert_private_key(UnableToSignError.new("mAuth client cannot sign without a private key!"))
|
281
|
-
attributes = { time: Time.now.to_i.to_s, app_uuid: client_app_uuid }.merge(attributes)
|
282
|
-
signature = Base64.encode64(private_key.private_encrypt(object.string_to_sign(attributes))).delete("\n")
|
283
|
-
end
|
284
|
-
end
|
285
|
-
include Signer
|
286
|
-
|
287
|
-
# methods common to RemoteRequestAuthenticator and LocalAuthenticator
|
288
|
-
module Authenticator
|
289
|
-
ALLOWED_DRIFT_SECONDS = 300
|
290
|
-
|
291
|
-
# takes an incoming request or response object, and returns whether
|
292
|
-
# the object is authentic according to its signature.
|
293
|
-
def authentic?(object)
|
294
|
-
log_authentication_request(object)
|
295
|
-
begin
|
296
|
-
authenticate!(object)
|
297
|
-
true
|
298
|
-
rescue InauthenticError, MauthNotPresent
|
299
|
-
false
|
300
|
-
end
|
301
|
-
end
|
302
|
-
|
303
|
-
# raises InauthenticError unless the given object is authentic
|
304
|
-
def authenticate!(object)
|
305
|
-
authentication_present!(object)
|
306
|
-
time_valid!(object)
|
307
|
-
token_valid!(object)
|
308
|
-
signature_valid!(object)
|
309
|
-
rescue MauthNotPresent => e
|
310
|
-
logger.warn "mAuth signature not present on #{object.class}. Exception: #{e.message}"
|
311
|
-
raise
|
312
|
-
rescue InauthenticError => e
|
313
|
-
logger.error "mAuth signature authentication failed for #{object.class}. Exception: #{e.message}"
|
314
|
-
raise
|
315
|
-
rescue UnableToAuthenticateError => e
|
316
|
-
logger.error "Unable to authenticate with MAuth for #{object.class}. Exception: #{e.message}"
|
317
|
-
raise
|
318
|
-
end
|
319
|
-
|
320
|
-
private
|
321
|
-
|
322
|
-
# Note: This log is likely consumed downstream and the contents SHOULD NOT be changed without a thorough review of downstream consumers.
|
323
|
-
def log_authentication_request(object)
|
324
|
-
object_app_uuid = object.signature_app_uuid || '[none provided]'
|
325
|
-
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}."
|
326
|
-
end
|
327
|
-
|
328
|
-
def authentication_present!(object)
|
329
|
-
if object.x_mws_authentication.nil? || object.x_mws_authentication !~ /\S/
|
330
|
-
raise MauthNotPresent, "Authentication Failed. No mAuth signature present; X-MWS-Authentication header is blank."
|
331
|
-
end
|
332
|
-
end
|
333
|
-
|
334
|
-
def time_valid!(object, now = Time.now)
|
335
|
-
if object.x_mws_time.nil?
|
336
|
-
raise InauthenticError, "Time verification failed. No x-mws-time present."
|
337
|
-
elsif !(-ALLOWED_DRIFT_SECONDS..ALLOWED_DRIFT_SECONDS).cover?(now.to_i - object.x_mws_time.to_i)
|
338
|
-
raise InauthenticError, "Time verification failed. #{object.x_mws_time} not within #{ALLOWED_DRIFT_SECONDS} of #{now}"
|
339
|
-
end
|
340
|
-
end
|
341
|
-
|
342
|
-
def token_valid!(object)
|
343
|
-
unless object.signature_token == MWS_TOKEN
|
344
|
-
raise InauthenticError, "Token verification failed. Expected #{MWS_TOKEN.inspect}; token was #{object.signature_token}"
|
345
|
-
end
|
346
|
-
end
|
347
|
-
end
|
348
|
-
include Authenticator
|
349
|
-
|
350
|
-
# methods to verify the authenticity of signed requests and responses locally, retrieving
|
351
|
-
# public keys from the mAuth service as needed
|
352
|
-
module LocalAuthenticator
|
353
|
-
private
|
354
|
-
|
355
|
-
def signature_valid!(object)
|
356
|
-
# We are in an unfortunate situation in which Euresource is percent-encoding parts of paths, but not
|
357
|
-
# all of them. In particular, Euresource is percent-encoding all special characters save for '/'.
|
358
|
-
# Also, unfortunately, Nginx unencodes URIs before sending them off to served applications, though
|
359
|
-
# other web servers (particularly those we typically use for local testing) do not. The various forms
|
360
|
-
# of the expected string to sign are meant to cover the main cases.
|
361
|
-
# TODO: Revisit and simplify this unfortunate situation.
|
362
|
-
|
363
|
-
original_request_uri = object.attributes_for_signing[:request_url]
|
364
|
-
|
365
|
-
# craft an expected string-to-sign without doing any percent-encoding
|
366
|
-
expected_no_reencoding = object.string_to_sign(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
|
367
|
-
|
368
|
-
# do a simple percent reencoding variant of the path
|
369
|
-
object.attributes_for_signing[:request_url] = CGI.escape(original_request_uri.to_s)
|
370
|
-
expected_for_percent_reencoding = object.string_to_sign(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
|
371
|
-
|
372
|
-
# do a moderately complex Euresource-style reencoding of the path
|
373
|
-
object.attributes_for_signing[:request_url] = euresource_escape(original_request_uri.to_s)
|
374
|
-
expected_euresource_style_reencoding = object.string_to_sign(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
|
375
|
-
|
376
|
-
# reset the object original request_uri, just in case we need it again
|
377
|
-
object.attributes_for_signing[:request_url] = original_request_uri
|
378
|
-
|
379
|
-
pubkey = OpenSSL::PKey::RSA.new(retrieve_public_key(object.signature_app_uuid))
|
380
|
-
begin
|
381
|
-
actual = pubkey.public_decrypt(Base64.decode64(object.signature))
|
382
|
-
rescue OpenSSL::PKey::PKeyError
|
383
|
-
raise InauthenticError, "Public key decryption of signature failed!\n#{$!.class}: #{$!.message}"
|
384
|
-
end
|
385
|
-
# TODO: time-invariant comparison instead of #== ?
|
386
|
-
unless expected_no_reencoding == actual || expected_euresource_style_reencoding == actual || expected_for_percent_reencoding == actual
|
387
|
-
raise InauthenticError, "Signature verification failed for #{object.class}"
|
388
|
-
end
|
389
|
-
end
|
390
|
-
|
391
|
-
# Note: RFC 3986 (https://www.ietf.org/rfc/rfc3986.txt) reserves the forward slash "/"
|
392
|
-
# and number sign "#" as component delimiters. Since these are valid URI components,
|
393
|
-
# they are decoded back into characters here to avoid signature invalidation
|
394
|
-
def euresource_escape(str)
|
395
|
-
CGI.escape(str).gsub(/%2F|%23/, "%2F" => "/", "%23" => "#")
|
396
|
-
end
|
397
|
-
|
398
|
-
def retrieve_public_key(app_uuid)
|
399
|
-
retrieve_security_token(app_uuid)['security_token']['public_key_str']
|
400
|
-
end
|
401
|
-
|
402
|
-
def retrieve_security_token(app_uuid)
|
403
|
-
security_token_cacher.get(app_uuid)
|
404
|
-
end
|
405
|
-
|
406
|
-
def security_token_cacher
|
407
|
-
@security_token_cacher ||= SecurityTokenCacher.new(self)
|
408
|
-
end
|
409
|
-
class SecurityTokenCacher
|
410
|
-
|
411
|
-
def initialize(mauth_client)
|
412
|
-
@mauth_client = mauth_client
|
413
|
-
# TODO: should this be UnableToSignError?
|
414
|
-
@mauth_client.assert_private_key(
|
415
|
-
UnableToAuthenticateError.new("Cannot fetch public keys from mAuth service without a private key!")
|
416
|
-
)
|
417
|
-
end
|
418
|
-
|
419
|
-
def get(app_uuid)
|
420
|
-
# url-encode the app_uuid to prevent trickery like escaping upward with ../../ in a malicious
|
421
|
-
# app_uuid - probably not exploitable, but this is the right way to do it anyway.
|
422
|
-
# use UNRESERVED instead of UNSAFE (the default) as UNSAFE doesn't include /
|
423
|
-
url_encoded_app_uuid = URI.escape(app_uuid, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
424
|
-
path = "/mauth/#{@mauth_client.mauth_api_version}/security_tokens/#{url_encoded_app_uuid}.json"
|
425
|
-
response = signed_mauth_connection.get(path)
|
426
|
-
|
427
|
-
case response.status
|
428
|
-
when 200
|
429
|
-
security_token_from(response.body)
|
430
|
-
when 404
|
431
|
-
# signing with a key mAuth doesn't know about is considered inauthentic
|
432
|
-
raise InauthenticError, "mAuth service responded with 404 looking up public key for #{app_uuid}"
|
433
|
-
else
|
434
|
-
@mauth_client.send(:mauth_service_response_error, response)
|
435
|
-
end
|
436
|
-
rescue ::Faraday::ConnectionFailed, ::Faraday::TimeoutError => e
|
437
|
-
msg = "mAuth service did not respond; received #{e.class}: #{e.message}"
|
438
|
-
@mauth_client.logger.error("Unable to authenticate with MAuth. Exception #{msg}")
|
439
|
-
raise UnableToAuthenticateError, msg
|
440
|
-
end
|
441
|
-
|
442
|
-
private
|
443
|
-
|
444
|
-
def security_token_from(response_body)
|
445
|
-
JSON.parse response_body
|
446
|
-
rescue JSON::ParserError => e
|
447
|
-
msg = "mAuth service responded with unparseable json: #{response_body}\n#{e.class}: #{e.message}"
|
448
|
-
@mauth_client.logger.error("Unable to authenticate with MAuth. Exception #{msg}")
|
449
|
-
raise UnableToAuthenticateError, msg
|
450
|
-
end
|
451
|
-
|
452
|
-
def signed_mauth_connection
|
453
|
-
@signed_mauth_connection ||= begin
|
454
|
-
if @mauth_client.ssl_certs_path
|
455
|
-
@mauth_client.faraday_options[:ssl] = { ca_path: @mauth_client.ssl_certs_path }
|
456
|
-
end
|
231
|
+
module ConfigFile
|
232
|
+
GITHUB_URL = 'https://github.com/mdsol/mauth-client-ruby'.freeze
|
233
|
+
@config = {}
|
457
234
|
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
builder.use :http_cache, logger: MAuth::Client.new.logger, shared_cache: false
|
462
|
-
builder.adapter ::Faraday.default_adapter
|
463
|
-
end
|
464
|
-
end
|
465
|
-
end
|
235
|
+
def self.load(path)
|
236
|
+
unless File.exist?(path)
|
237
|
+
raise "File #{path} not found. Please visit #{GITHUB_URL} for details."
|
466
238
|
end
|
467
|
-
end
|
468
239
|
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
# takes an incoming request object (no support for responses currently), and errors if the
|
474
|
-
# object is not authentic according to its signature
|
475
|
-
def signature_valid!(object)
|
476
|
-
raise ArgumentError, "Remote Authenticator can only authenticate requests; received #{object.inspect}" unless object.is_a?(MAuth::Request)
|
477
|
-
authentication_ticket = {
|
478
|
-
'verb' => object.attributes_for_signing[:verb],
|
479
|
-
'app_uuid' => object.signature_app_uuid,
|
480
|
-
'client_signature' => object.signature,
|
481
|
-
'request_url' => object.attributes_for_signing[:request_url],
|
482
|
-
'request_time' => object.x_mws_time,
|
483
|
-
'b64encoded_body' => Base64.encode64(object.attributes_for_signing[:body] || '')
|
484
|
-
}
|
485
|
-
begin
|
486
|
-
response = mauth_connection.post("/mauth/#{mauth_api_version}/authentication_tickets.json", "authentication_ticket" => authentication_ticket)
|
487
|
-
rescue ::Faraday::Error::ConnectionFailed, ::Faraday::Error::TimeoutError
|
488
|
-
raise UnableToAuthenticateError, "mAuth service did not respond; received #{$!.class}: #{$!.message}"
|
489
|
-
end
|
490
|
-
if (200..299).cover?(response.status)
|
491
|
-
nil
|
492
|
-
elsif response.status == 412 || response.status == 404
|
493
|
-
# the mAuth service responds with 412 when the given request is not authentically signed.
|
494
|
-
# older versions of the mAuth service respond with 404 when the given app_uuid
|
495
|
-
# does not exist, which is also considered to not be authentically signed. newer
|
496
|
-
# versions of the service respond 412 in all cases, so the 404 check may be removed
|
497
|
-
# when the old version of the mAuth service is out of service.
|
498
|
-
raise InauthenticError, "The mAuth service responded with #{response.status}: #{response.body}"
|
499
|
-
else
|
500
|
-
mauth_service_response_error(response)
|
501
|
-
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."
|
502
243
|
end
|
503
244
|
|
504
|
-
|
505
|
-
require 'faraday'
|
506
|
-
require 'faraday_middleware'
|
507
|
-
@mauth_connection ||= ::Faraday.new(mauth_baseurl, faraday_options) do |builder|
|
508
|
-
builder.use MAuth::Faraday::MAuthClientUserAgent
|
509
|
-
builder.use FaradayMiddleware::EncodeJson
|
510
|
-
builder.adapter ::Faraday.default_adapter
|
511
|
-
end
|
512
|
-
end
|
245
|
+
@config[path]
|
513
246
|
end
|
514
247
|
end
|
515
248
|
end
|
@@ -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
|
data/lib/mauth/errors.rb
ADDED
@@ -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
|
data/lib/mauth/fake/rack.rb
CHANGED
@@ -26,8 +26,10 @@ module MAuth
|
|
26
26
|
def call(env)
|
27
27
|
retval = if should_authenticate?(env)
|
28
28
|
mauth_request = MAuth::Rack::Request.new(env)
|
29
|
+
env['mauth.protocol_version'] = mauth_request.protocol_version
|
30
|
+
|
29
31
|
if self.class.is_authentic?
|
30
|
-
@app.call(env.merge('mauth.app_uuid' => mauth_request.signature_app_uuid, 'mauth.authentic' => true))
|
32
|
+
@app.call(env.merge!('mauth.app_uuid' => mauth_request.signature_app_uuid, 'mauth.authentic' => true))
|
31
33
|
else
|
32
34
|
response_for_inauthentic_request(env)
|
33
35
|
end
|
data/lib/mauth/faraday.rb
CHANGED
@@ -36,9 +36,15 @@ module MAuth
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def attributes_for_signing
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|