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 +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +6 -0
- data/CONTRIBUTING.md +8 -0
- data/README.md +18 -6
- data/Rakefile +107 -0
- data/exe/mauth-client +19 -19
- data/lib/mauth/client.rb +99 -365
- 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/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 +1 -0
- metadata +22 -2
@@ -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
|
data/lib/mauth/rack.rb
CHANGED
@@ -15,19 +15,26 @@ module MAuth
|
|
15
15
|
# from this is false, the request is passed to the app with no authentication performed.
|
16
16
|
class RequestAuthenticator < MAuth::Middleware
|
17
17
|
def call(env)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
18
|
+
mauth_request = MAuth::Rack::Request.new(env)
|
19
|
+
env['mauth.protocol_version'] = mauth_request.protocol_version
|
20
|
+
|
21
|
+
return @app.call(env) unless should_authenticate?(env)
|
22
|
+
|
23
|
+
if mauth_client.v2_only_authenticate? && mauth_request.protocol_version == 1
|
24
|
+
return response_for_missing_v2(env)
|
25
|
+
end
|
26
|
+
|
27
|
+
begin
|
28
|
+
if mauth_client.authentic?(mauth_request)
|
29
|
+
@app.call(env.merge!(
|
30
|
+
'mauth.app_uuid' => mauth_request.signature_app_uuid,
|
31
|
+
'mauth.authentic' => true
|
32
|
+
))
|
33
|
+
else
|
34
|
+
response_for_inauthentic_request(env)
|
28
35
|
end
|
29
|
-
|
30
|
-
|
36
|
+
rescue MAuth::UnableToAuthenticateError
|
37
|
+
response_for_unable_to_authenticate(env)
|
31
38
|
end
|
32
39
|
end
|
33
40
|
|
@@ -61,6 +68,18 @@ module MAuth
|
|
61
68
|
[500, { 'Content-Type' => 'application/json' }, [JSON.pretty_generate(body)]]
|
62
69
|
end
|
63
70
|
end
|
71
|
+
|
72
|
+
# response when the requests includes V1 headers but does not include V2
|
73
|
+
# headers and the V2_ONLY_AUTHENTICATE flag is set.
|
74
|
+
def response_for_missing_v2(env)
|
75
|
+
handle_head(env) do
|
76
|
+
body = {
|
77
|
+
'type' => 'errors:mauth:missing_v2',
|
78
|
+
'title' => 'This service requires mAuth v2 mcc-authentication header. Upgrade your mAuth library and configure it properly.'
|
79
|
+
}
|
80
|
+
[401, { 'Content-Type' => 'application/json' }, [JSON.pretty_generate(body)]]
|
81
|
+
end
|
82
|
+
end
|
64
83
|
end
|
65
84
|
|
66
85
|
# same as MAuth::Rack::RequestAuthenticator, but does not authenticate /app_status
|
@@ -70,12 +89,24 @@ module MAuth
|
|
70
89
|
end
|
71
90
|
end
|
72
91
|
|
73
|
-
# signs outgoing responses
|
92
|
+
# signs outgoing responses with only the protocol used to sign the request.
|
74
93
|
class ResponseSigner < MAuth::Middleware
|
75
94
|
def call(env)
|
76
95
|
unsigned_response = @app.call(env)
|
77
|
-
|
78
|
-
|
96
|
+
|
97
|
+
method =
|
98
|
+
if env['mauth.protocol_version'] == 2
|
99
|
+
:signed_v2
|
100
|
+
elsif env['mauth.protocol_version'] == 1
|
101
|
+
:signed_v1
|
102
|
+
else
|
103
|
+
# if no protocol was supplied then use `signed` which either signs
|
104
|
+
# with both protocol versions (by default) or only v2 when the
|
105
|
+
# v2_only_sign_requests flag is set to true.
|
106
|
+
:signed
|
107
|
+
end
|
108
|
+
response = mauth_client.send(method, MAuth::Rack::Response.new(*unsigned_response))
|
109
|
+
response.status_headers_body
|
79
110
|
end
|
80
111
|
end
|
81
112
|
|
@@ -93,7 +124,12 @@ module MAuth
|
|
93
124
|
env['rack.input'].rewind
|
94
125
|
body = env['rack.input'].read
|
95
126
|
env['rack.input'].rewind
|
96
|
-
{
|
127
|
+
{
|
128
|
+
verb: env['REQUEST_METHOD'],
|
129
|
+
request_url: env['PATH_INFO'],
|
130
|
+
body: body,
|
131
|
+
query_string: env['QUERY_STRING']
|
132
|
+
}
|
97
133
|
end
|
98
134
|
end
|
99
135
|
|
@@ -104,6 +140,14 @@ module MAuth
|
|
104
140
|
def x_mws_authentication
|
105
141
|
@env['HTTP_X_MWS_AUTHENTICATION']
|
106
142
|
end
|
143
|
+
|
144
|
+
def mcc_time
|
145
|
+
@env['HTTP_MCC_TIME']
|
146
|
+
end
|
147
|
+
|
148
|
+
def mcc_authentication
|
149
|
+
@env['HTTP_MCC_AUTHENTICATION']
|
150
|
+
end
|
107
151
|
end
|
108
152
|
|
109
153
|
# representation of a response composed from a rack response (status, headers, body) which
|
@@ -4,7 +4,7 @@ module MAuth
|
|
4
4
|
# module which composes a string to sign.
|
5
5
|
#
|
6
6
|
# includer must provide
|
7
|
-
# - SIGNATURE_COMPONENTS constant - array of keys to get from
|
7
|
+
# - SIGNATURE_COMPONENTS OR SIGNATURE_COMPONENTS_V2 constant - array of keys to get from
|
8
8
|
# - #attributes_for_signing
|
9
9
|
# - #merge_headers (takes a Hash of headers; returns an instance of includer's own class whose
|
10
10
|
# headers have been updated with the argument headers)
|
@@ -12,15 +12,89 @@ module MAuth
|
|
12
12
|
# composes a string suitable for private-key signing from the SIGNATURE_COMPONENTS keys of
|
13
13
|
# attributes for signing, which are themselves taken from #attributes_for_signing and
|
14
14
|
# the given argument more_attributes
|
15
|
-
|
15
|
+
|
16
|
+
# the string to sign for V1 protocol will be (where LF is line feed character)
|
17
|
+
# for requests:
|
18
|
+
# string_to_sign =
|
19
|
+
# http_verb + <LF> +
|
20
|
+
# resource_url_path (no host, port or query string; first "/" is included) + <LF> +
|
21
|
+
# request_body + <LF> +
|
22
|
+
# app_uuid + <LF> +
|
23
|
+
# current_seconds_since_epoch
|
24
|
+
#
|
25
|
+
# for responses:
|
26
|
+
# string_to_sign =
|
27
|
+
# status_code_string + <LF> +
|
28
|
+
# response_body_digest + <LF> +
|
29
|
+
# app_uuid + <LF> +
|
30
|
+
# current_seconds_since_epoch
|
31
|
+
def string_to_sign_v1(more_attributes)
|
16
32
|
attributes_for_signing = self.attributes_for_signing.merge(more_attributes)
|
17
33
|
missing_attributes = self.class::SIGNATURE_COMPONENTS.select { |key| !attributes_for_signing.key?(key) || attributes_for_signing[key].nil? }
|
18
34
|
missing_attributes.delete(:body) # body may be omitted
|
19
35
|
if missing_attributes.any?
|
20
36
|
raise(UnableToSignError, "Missing required attributes to sign: #{missing_attributes.inspect}\non object to sign: #{inspect}")
|
21
37
|
end
|
22
|
-
|
23
|
-
|
38
|
+
self.class::SIGNATURE_COMPONENTS.map { |k| attributes_for_signing[k].to_s }.join("\n")
|
39
|
+
end
|
40
|
+
|
41
|
+
# the string to sign for V2 protocol will be (where LF is line feed character)
|
42
|
+
# for requests:
|
43
|
+
# string_to_sign =
|
44
|
+
# http_verb + <LF> +
|
45
|
+
# resource_url_path (no host, port or query string; first "/" is included) + <LF> +
|
46
|
+
# request_body_digest + <LF> +
|
47
|
+
# app_uuid + <LF> +
|
48
|
+
# current_seconds_since_epoch + <LF> +
|
49
|
+
# encoded_query_params
|
50
|
+
#
|
51
|
+
# for responses:
|
52
|
+
# string_to_sign =
|
53
|
+
# status_code_string + <LF> +
|
54
|
+
# response_body_digest + <LF> +
|
55
|
+
# app_uuid + <LF> +
|
56
|
+
# current_seconds_since_epoch
|
57
|
+
def string_to_sign_v2(override_attrs)
|
58
|
+
attrs_with_overrides = self.attributes_for_signing.merge(override_attrs)
|
59
|
+
|
60
|
+
# memoization of body_digest to avoid hashing three times when we call
|
61
|
+
# string_to_sign_v2 three times in client#signature_valid_v2!
|
62
|
+
# note that if :body is nil we hash an empty string ("")
|
63
|
+
attrs_with_overrides[:body_digest] ||= Digest::SHA512.hexdigest(attrs_with_overrides[:body].to_s)
|
64
|
+
attrs_with_overrides[:encoded_query_params] = encode_query_string(attrs_with_overrides[:query_string].to_s)
|
65
|
+
|
66
|
+
missing_attributes = self.class::SIGNATURE_COMPONENTS_V2.reject do |key|
|
67
|
+
attrs_with_overrides.dig(key)
|
68
|
+
end
|
69
|
+
|
70
|
+
missing_attributes.delete(:body_digest) # body may be omitted
|
71
|
+
missing_attributes.delete(:encoded_query_params) # query_string may be omitted
|
72
|
+
if missing_attributes.any?
|
73
|
+
raise(UnableToSignError, "Missing required attributes to sign: #{missing_attributes.inspect}\non object to sign: #{inspect}")
|
74
|
+
end
|
75
|
+
|
76
|
+
self.class::SIGNATURE_COMPONENTS_V2.map do |k|
|
77
|
+
attrs_with_overrides[k].to_s.force_encoding('UTF-8')
|
78
|
+
end.join("\n")
|
79
|
+
end
|
80
|
+
|
81
|
+
# sorts query string parameters by codepoint, uri encodes keys and values,
|
82
|
+
# and rejoins parameters into a query string
|
83
|
+
def encode_query_string(q_string)
|
84
|
+
q_string.split('&').sort.map do |part|
|
85
|
+
k, e, v = part.partition('=')
|
86
|
+
uri_escape(k) + e + uri_escape(v)
|
87
|
+
end.join('&')
|
88
|
+
end
|
89
|
+
|
90
|
+
# percent encodes special characters, preserving character encoding.
|
91
|
+
# encodes space as '%20'
|
92
|
+
# does not encode A-Z, a-z, 0-9, hyphen ( - ), underscore ( _ ), period ( . ),
|
93
|
+
# or tilde ( ~ )
|
94
|
+
# NOTE the CGI.escape spec changed in 2.5 to not escape tildes. we gsub
|
95
|
+
# tilde encoding back to tildes to account for older Rubies
|
96
|
+
def uri_escape(string)
|
97
|
+
CGI.escape(string).gsub(/\+|%7E/, '+' => '%20', '%7E' => '~')
|
24
98
|
end
|
25
99
|
|
26
100
|
def initialize(attributes_for_signing)
|
@@ -35,13 +109,27 @@ module MAuth
|
|
35
109
|
# methods for an incoming object which is expected to have a signature.
|
36
110
|
#
|
37
111
|
# includer must provide
|
112
|
+
# - #mcc_authentication which returns that header's value
|
113
|
+
# - #mcc_time
|
114
|
+
# OR
|
38
115
|
# - #x_mws_authentication which returns that header's value
|
39
116
|
# - #x_mws_time
|
40
117
|
module Signed
|
41
|
-
#
|
118
|
+
# mauth_client will authenticate with the highest protocol version present and ignore other
|
119
|
+
# protocol versions.
|
120
|
+
# returns a hash with keys :token, :app_uuid, and :signature parsed from the MCC-Authentication header
|
121
|
+
# if it is present and if not then the X-MWS-Authentication header if it is present.
|
122
|
+
# Note MWSV2 protocol no longer allows more than one space between the token and app uuid.
|
42
123
|
def signature_info
|
43
124
|
@signature_info ||= begin
|
44
|
-
match =
|
125
|
+
match = if mcc_authentication
|
126
|
+
mcc_authentication.match(
|
127
|
+
/\A(#{MAuth::Client::MWSV2_TOKEN}) ([^:]+):([^:]+)#{MAuth::Client::AUTH_HEADER_DELIMITER}\z/
|
128
|
+
)
|
129
|
+
elsif x_mws_authentication
|
130
|
+
x_mws_authentication.match(/\A([^ ]+) *([^:]+):([^:]+)\z/)
|
131
|
+
end
|
132
|
+
|
45
133
|
match ? { token: match[1], app_uuid: match[2], signature: match[3] } : {}
|
46
134
|
end
|
47
135
|
end
|
@@ -57,17 +145,36 @@ module MAuth
|
|
57
145
|
def signature
|
58
146
|
signature_info[:signature]
|
59
147
|
end
|
148
|
+
|
149
|
+
def protocol_version
|
150
|
+
if !mcc_authentication.to_s.strip.empty?
|
151
|
+
2
|
152
|
+
elsif !x_mws_authentication.to_s.strip.empty?
|
153
|
+
1
|
154
|
+
end
|
155
|
+
end
|
60
156
|
end
|
61
157
|
|
62
158
|
# virtual base class for signable requests
|
63
159
|
class Request
|
64
|
-
SIGNATURE_COMPONENTS = [
|
160
|
+
SIGNATURE_COMPONENTS = %i[verb request_url body app_uuid time].freeze
|
161
|
+
SIGNATURE_COMPONENTS_V2 =
|
162
|
+
%i[
|
163
|
+
verb
|
164
|
+
request_url
|
165
|
+
body_digest
|
166
|
+
app_uuid
|
167
|
+
time
|
168
|
+
encoded_query_params
|
169
|
+
].freeze
|
170
|
+
|
65
171
|
include Signable
|
66
172
|
end
|
67
173
|
|
68
174
|
# virtual base class for signable responses
|
69
175
|
class Response
|
70
|
-
SIGNATURE_COMPONENTS = [
|
176
|
+
SIGNATURE_COMPONENTS = %i[status_code body app_uuid time].freeze
|
177
|
+
SIGNATURE_COMPONENTS_V2 = %i[status_code body_digest app_uuid time].freeze
|
71
178
|
include Signable
|
72
179
|
end
|
73
180
|
end
|
data/lib/mauth/version.rb
CHANGED
data/mauth-client.gemspec
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mauth-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 5.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthew Szenher
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: exe
|
13
13
|
cert_chain: []
|
14
|
-
date: 2019-
|
14
|
+
date: 2019-07-23 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: faraday
|
@@ -201,6 +201,20 @@ dependencies:
|
|
201
201
|
- - "~>"
|
202
202
|
- !ruby/object:Gem::Version
|
203
203
|
version: '0.9'
|
204
|
+
- !ruby/object:Gem::Dependency
|
205
|
+
name: benchmark-ips
|
206
|
+
requirement: !ruby/object:Gem::Requirement
|
207
|
+
requirements:
|
208
|
+
- - "~>"
|
209
|
+
- !ruby/object:Gem::Version
|
210
|
+
version: '2.7'
|
211
|
+
type: :development
|
212
|
+
prerelease: false
|
213
|
+
version_requirements: !ruby/object:Gem::Requirement
|
214
|
+
requirements:
|
215
|
+
- - "~>"
|
216
|
+
- !ruby/object:Gem::Version
|
217
|
+
version: '2.7'
|
204
218
|
description: Client for signing and authentication of requests and responses with
|
205
219
|
mAuth authentication. Includes middleware for Rack and Faraday for incoming and
|
206
220
|
outgoing requests and responses.
|
@@ -237,11 +251,17 @@ files:
|
|
237
251
|
- lib/mauth-client.rb
|
238
252
|
- lib/mauth/autoload.rb
|
239
253
|
- lib/mauth/client.rb
|
254
|
+
- lib/mauth/client/authenticator_base.rb
|
255
|
+
- lib/mauth/client/local_authenticator.rb
|
256
|
+
- lib/mauth/client/remote_authenticator.rb
|
257
|
+
- lib/mauth/client/security_token_cacher.rb
|
258
|
+
- lib/mauth/client/signer.rb
|
240
259
|
- lib/mauth/core_ext.rb
|
241
260
|
- lib/mauth/dice_bag/mauth.rb.dice
|
242
261
|
- lib/mauth/dice_bag/mauth.yml.dice
|
243
262
|
- lib/mauth/dice_bag/mauth_key.dice
|
244
263
|
- lib/mauth/dice_bag/mauth_templates.rb
|
264
|
+
- lib/mauth/errors.rb
|
245
265
|
- lib/mauth/fake/rack.rb
|
246
266
|
- lib/mauth/faraday.rb
|
247
267
|
- lib/mauth/middleware.rb
|