mauth-client 4.2.1 → 5.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.
- 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
@@ -0,0 +1,75 @@
|
|
1
|
+
# methods for remotely authenticating a request by sending it to the mauth service
|
2
|
+
|
3
|
+
module MAuth
|
4
|
+
class Client
|
5
|
+
module RemoteRequestAuthenticator
|
6
|
+
private
|
7
|
+
|
8
|
+
# takes an incoming request object (no support for responses currently), and errors if the
|
9
|
+
# object is not authentic according to its signature
|
10
|
+
def signature_valid_v1!(object)
|
11
|
+
raise ArgumentError, "Remote Authenticator can only authenticate requests; received #{object.inspect}" unless object.is_a?(MAuth::Request)
|
12
|
+
authentication_ticket = {
|
13
|
+
'verb' => object.attributes_for_signing[:verb],
|
14
|
+
'app_uuid' => object.signature_app_uuid,
|
15
|
+
'client_signature' => object.signature,
|
16
|
+
'request_url' => object.attributes_for_signing[:request_url],
|
17
|
+
'request_time' => object.x_mws_time,
|
18
|
+
'b64encoded_body' => Base64.encode64(object.attributes_for_signing[:body] || '')
|
19
|
+
}
|
20
|
+
make_mauth_request(authentication_ticket)
|
21
|
+
end
|
22
|
+
|
23
|
+
def signature_valid_v2!(object)
|
24
|
+
unless object.is_a?(MAuth::Request)
|
25
|
+
msg = "Remote Authenticator can only authenticate requests; received #{object.inspect}"
|
26
|
+
raise ArgumentError, msg
|
27
|
+
end
|
28
|
+
|
29
|
+
authentication_ticket = {
|
30
|
+
verb: object.attributes_for_signing[:verb],
|
31
|
+
app_uuid: object.signature_app_uuid,
|
32
|
+
client_signature: object.signature,
|
33
|
+
request_url: object.attributes_for_signing[:request_url],
|
34
|
+
request_time: object.mcc_time,
|
35
|
+
b64encoded_body: Base64.encode64(object.attributes_for_signing[:body] || ''),
|
36
|
+
query_string: object.attributes_for_signing[:query_string],
|
37
|
+
token: object.signature_token
|
38
|
+
}
|
39
|
+
make_mauth_request(authentication_ticket)
|
40
|
+
end
|
41
|
+
|
42
|
+
def make_mauth_request(authentication_ticket)
|
43
|
+
begin
|
44
|
+
response = mauth_connection.post("/mauth/#{mauth_api_version}/authentication_tickets.json", 'authentication_ticket' => authentication_ticket)
|
45
|
+
rescue ::Faraday::Error::ConnectionFailed, ::Faraday::Error::TimeoutError => e
|
46
|
+
msg = "mAuth service did not respond; received #{e.class}: #{e.message}"
|
47
|
+
logger.error("Unable to authenticate with MAuth. Exception #{msg}")
|
48
|
+
raise UnableToAuthenticateError, msg
|
49
|
+
end
|
50
|
+
if (200..299).cover?(response.status)
|
51
|
+
nil
|
52
|
+
elsif response.status == 412 || response.status == 404
|
53
|
+
# the mAuth service responds with 412 when the given request is not authentically signed.
|
54
|
+
# older versions of the mAuth service respond with 404 when the given app_uuid
|
55
|
+
# does not exist, which is also considered to not be authentically signed. newer
|
56
|
+
# versions of the service respond 412 in all cases, so the 404 check may be removed
|
57
|
+
# when the old version of the mAuth service is out of service.
|
58
|
+
raise InauthenticError, "The mAuth service responded with #{response.status}: #{response.body}"
|
59
|
+
else
|
60
|
+
mauth_service_response_error(response)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def mauth_connection
|
65
|
+
require 'faraday'
|
66
|
+
require 'faraday_middleware'
|
67
|
+
@mauth_connection ||= ::Faraday.new(mauth_baseurl, faraday_options) do |builder|
|
68
|
+
builder.use MAuth::Faraday::MAuthClientUserAgent
|
69
|
+
builder.use FaradayMiddleware::EncodeJson
|
70
|
+
builder.adapter ::Faraday.default_adapter
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module MAuth
|
2
|
+
class Client
|
3
|
+
module LocalAuthenticator
|
4
|
+
class SecurityTokenCacher
|
5
|
+
|
6
|
+
class ExpirableSecurityToken < Struct.new(:security_token, :create_time)
|
7
|
+
CACHE_LIFE = 60
|
8
|
+
def expired?
|
9
|
+
create_time + CACHE_LIFE < Time.now
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(mauth_client)
|
14
|
+
@mauth_client = mauth_client
|
15
|
+
# TODO: should this be UnableToSignError?
|
16
|
+
@mauth_client.assert_private_key(UnableToAuthenticateError.new("Cannot fetch public keys from mAuth service without a private key!"))
|
17
|
+
@cache = {}
|
18
|
+
require 'thread'
|
19
|
+
@cache_write_lock = Mutex.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def get(app_uuid)
|
23
|
+
if !@cache[app_uuid] || @cache[app_uuid].expired?
|
24
|
+
# url-encode the app_uuid to prevent trickery like escaping upward with ../../ in a malicious
|
25
|
+
# app_uuid - probably not exploitable, but this is the right way to do it anyway.
|
26
|
+
# use UNRESERVED instead of UNSAFE (the default) as UNSAFE doesn't include /
|
27
|
+
url_encoded_app_uuid = URI.escape(app_uuid, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
28
|
+
begin
|
29
|
+
response = signed_mauth_connection.get("/mauth/#{@mauth_client.mauth_api_version}/security_tokens/#{url_encoded_app_uuid}.json")
|
30
|
+
rescue ::Faraday::Error::ConnectionFailed, ::Faraday::Error::TimeoutError => e
|
31
|
+
msg = "mAuth service did not respond; received #{e.class}: #{e.message}"
|
32
|
+
@mauth_client.logger.error("Unable to authenticate with MAuth. Exception #{msg}")
|
33
|
+
raise UnableToAuthenticateError, msg
|
34
|
+
end
|
35
|
+
if response.status == 200
|
36
|
+
begin
|
37
|
+
security_token = JSON.parse(response.body)
|
38
|
+
rescue JSON::ParserError => e
|
39
|
+
msg = "mAuth service responded with unparseable json: #{response.body}\n#{e.class}: #{e.message}"
|
40
|
+
@mauth_client.logger.error("Unable to authenticate with MAuth. Exception #{msg}")
|
41
|
+
raise UnableToAuthenticateError, msg
|
42
|
+
end
|
43
|
+
@cache_write_lock.synchronize do
|
44
|
+
@cache[app_uuid] = ExpirableSecurityToken.new(security_token, Time.now)
|
45
|
+
end
|
46
|
+
elsif response.status == 404
|
47
|
+
# signing with a key mAuth doesn't know about is considered inauthentic
|
48
|
+
raise InauthenticError, "mAuth service responded with 404 looking up public key for #{app_uuid}"
|
49
|
+
else
|
50
|
+
@mauth_client.send(:mauth_service_response_error, response)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
@cache[app_uuid].security_token
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def signed_mauth_connection
|
59
|
+
require 'faraday'
|
60
|
+
require 'mauth/faraday'
|
61
|
+
@mauth_client.faraday_options[:ssl] = { ca_path: @mauth_client.ssl_certs_path } if @mauth_client.ssl_certs_path
|
62
|
+
@signed_mauth_connection ||= ::Faraday.new(@mauth_client.mauth_baseurl, @mauth_client.faraday_options) do |builder|
|
63
|
+
builder.use MAuth::Faraday::MAuthClientUserAgent
|
64
|
+
builder.use MAuth::Faraday::RequestSigner, 'mauth_client' => @mauth_client
|
65
|
+
builder.adapter ::Faraday.default_adapter
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'mauth/errors'
|
3
|
+
|
4
|
+
# methods to sign requests and responses. part of MAuth::Client
|
5
|
+
|
6
|
+
module MAuth
|
7
|
+
class Client
|
8
|
+
SIGNING_DIGEST = OpenSSL::Digest::SHA512.new
|
9
|
+
|
10
|
+
module Signer
|
11
|
+
UNABLE_TO_SIGN_ERR = UnableToSignError.new('mAuth client cannot sign without a private key!')
|
12
|
+
|
13
|
+
# takes an outgoing request or response object, and returns an object of the same class
|
14
|
+
# whose headers are updated to include mauth's signature headers
|
15
|
+
def signed(object, attributes = {})
|
16
|
+
object.merge_headers(signed_headers(object, attributes))
|
17
|
+
end
|
18
|
+
|
19
|
+
# signs with v1 only. used when signing responses to v1 requests.
|
20
|
+
def signed_v1(object, attributes = {})
|
21
|
+
object.merge_headers(signed_headers_v1(object, attributes))
|
22
|
+
end
|
23
|
+
|
24
|
+
def signed_v2(object, attributes = {})
|
25
|
+
object.merge_headers(signed_headers_v2(object, attributes))
|
26
|
+
end
|
27
|
+
|
28
|
+
# takes a signable object (outgoing request or response). returns a hash of headers to be
|
29
|
+
# applied to the object which comprises its signature.
|
30
|
+
def signed_headers(object, attributes = {})
|
31
|
+
if v2_only_sign_requests?
|
32
|
+
signed_headers_v2(object, attributes)
|
33
|
+
else # by default sign with both the v1 and v2 protocol
|
34
|
+
signed_headers_v1(object, attributes).merge(signed_headers_v2(object, attributes))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def signed_headers_v1(object, attributes = {})
|
39
|
+
attributes = { time: Time.now.to_i.to_s, app_uuid: client_app_uuid }.merge(attributes)
|
40
|
+
string_to_sign = object.string_to_sign_v1(attributes)
|
41
|
+
signature = self.signature_v1(string_to_sign)
|
42
|
+
{ 'X-MWS-Authentication' => "#{MWS_TOKEN} #{client_app_uuid}:#{signature}", 'X-MWS-Time' => attributes[:time] }
|
43
|
+
end
|
44
|
+
|
45
|
+
def signed_headers_v2(object, attributes = {})
|
46
|
+
attributes = { time: Time.now.to_i.to_s, app_uuid: client_app_uuid }.merge(attributes)
|
47
|
+
string_to_sign = object.string_to_sign_v2(attributes)
|
48
|
+
signature = self.signature_v2(string_to_sign)
|
49
|
+
{
|
50
|
+
'MCC-Authentication' => "#{MWSV2_TOKEN} #{client_app_uuid}:#{signature}#{AUTH_HEADER_DELIMITER}",
|
51
|
+
'MCC-Time' => attributes[:time]
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def signature_v1(string_to_sign)
|
56
|
+
assert_private_key(UNABLE_TO_SIGN_ERR)
|
57
|
+
hashed_string_to_sign = Digest::SHA512.hexdigest(string_to_sign)
|
58
|
+
Base64.encode64(private_key.private_encrypt(hashed_string_to_sign)).delete("\n")
|
59
|
+
end
|
60
|
+
|
61
|
+
def signature_v2(string_to_sign)
|
62
|
+
assert_private_key(UNABLE_TO_SIGN_ERR)
|
63
|
+
Base64.encode64(private_key.sign(SIGNING_DIGEST, string_to_sign)).delete("\n")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|