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
@@ -0,0 +1,118 @@
|
|
1
|
+
# methods common to RemoteRequestAuthenticator and LocalAuthenticator
|
2
|
+
|
3
|
+
module MAuth
|
4
|
+
class Client
|
5
|
+
module AuthenticatorBase
|
6
|
+
ALLOWED_DRIFT_SECONDS = 300
|
7
|
+
|
8
|
+
# takes an incoming request or response object, and returns whether
|
9
|
+
# the object is authentic according to its signature.
|
10
|
+
def authentic?(object)
|
11
|
+
log_authentication_request(object)
|
12
|
+
begin
|
13
|
+
authenticate!(object)
|
14
|
+
true
|
15
|
+
rescue InauthenticError, MAuthNotPresent, MissingV2Error
|
16
|
+
false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# raises InauthenticError unless the given object is authentic. Will only
|
21
|
+
# authenticate with v2 if the environment variable V2_ONLY_AUTHENTICATE
|
22
|
+
# is set. Otherwise will authenticate with only the highest protocol version present
|
23
|
+
def authenticate!(object)
|
24
|
+
if object.protocol_version == 2
|
25
|
+
authenticate_v2!(object)
|
26
|
+
elsif object.protocol_version == 1
|
27
|
+
if v2_only_authenticate?
|
28
|
+
# If v2 is required but not present and v1 is present we raise MissingV2Error
|
29
|
+
msg = 'This service requires mAuth v2 mcc-authentication header but only v1 x-mws-authentication is present'
|
30
|
+
logger.error(msg)
|
31
|
+
raise MissingV2Error, msg
|
32
|
+
end
|
33
|
+
|
34
|
+
authenticate_v1!(object)
|
35
|
+
else
|
36
|
+
sub_str = v2_only_authenticate? ? '' : 'X-MWS-Authentication header is blank, '
|
37
|
+
msg = "Authentication Failed. No mAuth signature present; #{sub_str}MCC-Authentication header is blank."
|
38
|
+
logger.warn("mAuth signature not present on #{object.class}. Exception: #{msg}")
|
39
|
+
raise MAuthNotPresent, msg
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Note: This log is likely consumed downstream and the contents SHOULD NOT
|
46
|
+
# be changed without a thorough review of downstream consumers.
|
47
|
+
def log_authentication_request(object)
|
48
|
+
object_app_uuid = object.signature_app_uuid || '[none provided]'
|
49
|
+
object_token = object.signature_token || '[none provided]'
|
50
|
+
logger.info(
|
51
|
+
"Mauth-client attempting to authenticate request from app with mauth" \
|
52
|
+
" app uuid #{object_app_uuid} to app with mauth app uuid #{client_app_uuid}" \
|
53
|
+
" using version #{object_token}."
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
def log_inauthentic(object, message)
|
58
|
+
logger.error("mAuth signature authentication failed for #{object.class}. Exception: #{message}")
|
59
|
+
end
|
60
|
+
|
61
|
+
def time_within_valid_range!(object, time_signed, now = Time.now)
|
62
|
+
return if (-ALLOWED_DRIFT_SECONDS..ALLOWED_DRIFT_SECONDS).cover?(now.to_i - time_signed)
|
63
|
+
|
64
|
+
msg = "Time verification failed. #{time_signed} not within #{ALLOWED_DRIFT_SECONDS} of #{now}"
|
65
|
+
log_inauthentic(object, msg)
|
66
|
+
raise InauthenticError, msg
|
67
|
+
end
|
68
|
+
|
69
|
+
# V1 helpers
|
70
|
+
def authenticate_v1!(object)
|
71
|
+
time_valid_v1!(object)
|
72
|
+
token_valid_v1!(object)
|
73
|
+
signature_valid_v1!(object)
|
74
|
+
end
|
75
|
+
|
76
|
+
def time_valid_v1!(object)
|
77
|
+
if object.x_mws_time.nil?
|
78
|
+
msg = 'Time verification failed. No x-mws-time present.'
|
79
|
+
log_inauthentic(object, msg)
|
80
|
+
raise InauthenticError, msg
|
81
|
+
end
|
82
|
+
time_within_valid_range!(object, object.x_mws_time.to_i)
|
83
|
+
end
|
84
|
+
|
85
|
+
def token_valid_v1!(object)
|
86
|
+
return if object.signature_token == MWS_TOKEN
|
87
|
+
|
88
|
+
msg = "Token verification failed. Expected #{MWS_TOKEN}; token was #{object.signature_token}"
|
89
|
+
log_inauthentic(object, msg)
|
90
|
+
raise InauthenticError, msg
|
91
|
+
end
|
92
|
+
|
93
|
+
# V2 helpers
|
94
|
+
def authenticate_v2!(object)
|
95
|
+
time_valid_v2!(object)
|
96
|
+
token_valid_v2!(object)
|
97
|
+
signature_valid_v2!(object)
|
98
|
+
end
|
99
|
+
|
100
|
+
def time_valid_v2!(object)
|
101
|
+
if object.mcc_time.nil?
|
102
|
+
msg = 'Time verification failed. No MCC-Time present.'
|
103
|
+
log_inauthentic(object, msg)
|
104
|
+
raise InauthenticError, msg
|
105
|
+
end
|
106
|
+
time_within_valid_range!(object, object.mcc_time.to_i)
|
107
|
+
end
|
108
|
+
|
109
|
+
def token_valid_v2!(object)
|
110
|
+
return if object.signature_token == MWSV2_TOKEN
|
111
|
+
|
112
|
+
msg = "Token verification failed. Expected #{MWSV2_TOKEN}; token was #{object.signature_token}"
|
113
|
+
log_inauthentic(object, msg)
|
114
|
+
raise InauthenticError, msg
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'mauth/client/security_token_cacher'
|
2
|
+
require 'mauth/client/signer'
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
# methods to verify the authenticity of signed requests and responses locally, retrieving
|
6
|
+
# public keys from the mAuth service as needed
|
7
|
+
|
8
|
+
module MAuth
|
9
|
+
class Client
|
10
|
+
module LocalAuthenticator
|
11
|
+
private
|
12
|
+
|
13
|
+
def signature_valid_v1!(object)
|
14
|
+
# We are in an unfortunate situation in which Euresource is percent-encoding parts of paths, but not
|
15
|
+
# all of them. In particular, Euresource is percent-encoding all special characters save for '/'.
|
16
|
+
# Also, unfortunately, Nginx unencodes URIs before sending them off to served applications, though
|
17
|
+
# other web servers (particularly those we typically use for local testing) do not. The various forms
|
18
|
+
# of the expected string to sign are meant to cover the main cases.
|
19
|
+
# TODO: Revisit and simplify this unfortunate situation.
|
20
|
+
|
21
|
+
original_request_uri = object.attributes_for_signing[:request_url]
|
22
|
+
|
23
|
+
# craft an expected string-to-sign without doing any percent-encoding
|
24
|
+
expected_no_reencoding = object.string_to_sign_v1(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
|
25
|
+
|
26
|
+
# do a simple percent reencoding variant of the path
|
27
|
+
object.attributes_for_signing[:request_url] = CGI.escape(original_request_uri.to_s)
|
28
|
+
expected_for_percent_reencoding = object.string_to_sign_v1(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
|
29
|
+
|
30
|
+
# do a moderately complex Euresource-style reencoding of the path
|
31
|
+
object.attributes_for_signing[:request_url] = euresource_escape(original_request_uri.to_s)
|
32
|
+
expected_euresource_style_reencoding = object.string_to_sign_v1(time: object.x_mws_time, app_uuid: object.signature_app_uuid)
|
33
|
+
|
34
|
+
# reset the object original request_uri, just in case we need it again
|
35
|
+
object.attributes_for_signing[:request_url] = original_request_uri
|
36
|
+
|
37
|
+
begin
|
38
|
+
pubkey = OpenSSL::PKey::RSA.new(retrieve_public_key(object.signature_app_uuid))
|
39
|
+
actual = pubkey.public_decrypt(Base64.decode64(object.signature))
|
40
|
+
rescue OpenSSL::PKey::PKeyError => e
|
41
|
+
msg = "Public key decryption of signature failed! #{e.class}: #{e.message}"
|
42
|
+
log_inauthentic(object, msg)
|
43
|
+
raise InauthenticError, msg
|
44
|
+
end
|
45
|
+
|
46
|
+
unless verify_signature_v1!(actual, expected_no_reencoding) ||
|
47
|
+
verify_signature_v1!(actual, expected_euresource_style_reencoding) ||
|
48
|
+
verify_signature_v1!(actual, expected_for_percent_reencoding)
|
49
|
+
msg = "Signature verification failed for #{object.class}"
|
50
|
+
log_inauthentic(object, msg)
|
51
|
+
raise InauthenticError, msg
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def verify_signature_v1!(actual, expected_str_to_sign)
|
56
|
+
actual == Digest::SHA512.hexdigest(expected_str_to_sign)
|
57
|
+
end
|
58
|
+
|
59
|
+
def signature_valid_v2!(object)
|
60
|
+
# We are in an unfortunate situation in which Euresource is percent-encoding parts of paths, but not
|
61
|
+
# all of them. In particular, Euresource is percent-encoding all special characters save for '/'.
|
62
|
+
# Also, unfortunately, Nginx unencodes URIs before sending them off to served applications, though
|
63
|
+
# other web servers (particularly those we typically use for local testing) do not. The various forms
|
64
|
+
# of the expected string to sign are meant to cover the main cases.
|
65
|
+
# TODO: Revisit and simplify this unfortunate situation.
|
66
|
+
|
67
|
+
original_request_uri = object.attributes_for_signing[:request_url]
|
68
|
+
original_query_string = object.attributes_for_signing[:query_string]
|
69
|
+
|
70
|
+
# craft an expected string-to-sign without doing any percent-encoding
|
71
|
+
expected_no_reencoding = object.string_to_sign_v2(
|
72
|
+
time: object.mcc_time,
|
73
|
+
app_uuid: object.signature_app_uuid
|
74
|
+
)
|
75
|
+
|
76
|
+
# do a simple percent reencoding variant of the path
|
77
|
+
expected_for_percent_reencoding = object.string_to_sign_v2(
|
78
|
+
time: object.mcc_time,
|
79
|
+
app_uuid: object.signature_app_uuid,
|
80
|
+
request_url: CGI.escape(original_request_uri.to_s),
|
81
|
+
query_string: CGI.escape(original_query_string.to_s)
|
82
|
+
)
|
83
|
+
|
84
|
+
# do a moderately complex Euresource-style reencoding of the path
|
85
|
+
expected_euresource_style_reencoding = object.string_to_sign_v2(
|
86
|
+
time: object.mcc_time,
|
87
|
+
app_uuid: object.signature_app_uuid,
|
88
|
+
request_url: euresource_escape(original_request_uri.to_s),
|
89
|
+
query_string: euresource_escape(original_query_string.to_s)
|
90
|
+
)
|
91
|
+
|
92
|
+
pubkey = OpenSSL::PKey::RSA.new(retrieve_public_key(object.signature_app_uuid))
|
93
|
+
actual = Base64.decode64(object.signature)
|
94
|
+
|
95
|
+
unless verify_signature_v2!(object, actual, pubkey, expected_no_reencoding) ||
|
96
|
+
verify_signature_v2!(object, actual, pubkey, expected_euresource_style_reencoding) ||
|
97
|
+
verify_signature_v2!(object, actual, pubkey, expected_for_percent_reencoding)
|
98
|
+
msg = "Signature inauthentic for #{object.class}"
|
99
|
+
log_inauthentic(object, msg)
|
100
|
+
raise InauthenticError, msg
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def verify_signature_v2!(object, actual, pubkey, expected_str_to_sign)
|
105
|
+
pubkey.verify(
|
106
|
+
MAuth::Client::SIGNING_DIGEST,
|
107
|
+
actual,
|
108
|
+
expected_str_to_sign
|
109
|
+
)
|
110
|
+
rescue OpenSSL::PKey::PKeyError => e
|
111
|
+
msg = "RSA verification of signature failed! #{e.class}: #{e.message}"
|
112
|
+
log_inauthentic(object, msg)
|
113
|
+
raise InauthenticError, msg
|
114
|
+
end
|
115
|
+
|
116
|
+
# Note: RFC 3986 (https://www.ietf.org/rfc/rfc3986.txt) reserves the forward slash "/"
|
117
|
+
# and number sign "#" as component delimiters. Since these are valid URI components,
|
118
|
+
# they are decoded back into characters here to avoid signature invalidation
|
119
|
+
def euresource_escape(str)
|
120
|
+
CGI.escape(str).gsub(/%2F|%23/, '%2F' => '/', '%23' => '#')
|
121
|
+
end
|
122
|
+
|
123
|
+
def retrieve_public_key(app_uuid)
|
124
|
+
retrieve_security_token(app_uuid)['security_token']['public_key_str']
|
125
|
+
end
|
126
|
+
|
127
|
+
def retrieve_security_token(app_uuid)
|
128
|
+
security_token_cacher.get(app_uuid)
|
129
|
+
end
|
130
|
+
|
131
|
+
def security_token_cacher
|
132
|
+
@security_token_cacher ||= SecurityTokenCacher.new(self)
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -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
|