mauth-client 4.2.0 → 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/CHANGELOG.md +5 -2
- 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 -366
- 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 +3 -4
- metadata +29 -55
@@ -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
|