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.
@@ -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