Class: Mongo::Auth::Aws::Request Private
Relationships & Source Files | |
Inherits: | Object |
Defined in: | lib/mongo/auth/aws/request.rb |
Overview
Helper class for working with AWS requests.
The primary purpose of this class is to produce the canonical AWS STS request and calculate the signed headers and signature for it.
Constant Summary
-
STS_REQUEST_BODY =
The body of the STS GetCallerIdentity request.
This is currently the only request that this class supports making.
"Action=GetCallerIdentity&Version=2011-06-15".freeze
-
VALIDATE_TIMEOUT =
The timeout, in seconds, to use for validating credentials via STS.
10
Class Method Summary
-
.new(access_key_id:, secret_access_key:, session_token: nil, host:, server_nonce:, time: Time.now) ⇒ Request
constructor
Internal use only
Constructs the request.
Instance Attribute Summary
- #access_key_id ⇒ String readonly Internal use only
- #host ⇒ String readonly Internal use only
- #secret_access_key ⇒ String readonly Internal use only
- #server_nonce ⇒ String readonly Internal use only
- #session_token ⇒ String readonly Internal use only
- #time ⇒ Time readonly Internal use only
Instance Method Summary
-
#authorization ⇒ String
Internal use only
Returns the value of the Authorization header, per the AWS signature V4 specification.
-
#canonical_request ⇒ String
Internal use only
Returns the canonical request used during calculation of AWS V4 signature.
- #formatted_date ⇒ String Internal use only
- #formatted_time ⇒ String Internal use only
-
#headers ⇒ <Hash>
Internal use only
Returns the hash containing the headers of the calculated canonical request.
-
#headers_to_sign ⇒ <Hash>
Internal use only
Returns the hash containing the headers of the calculated canonical request that should be signed, in a ready to sign form.
- #region ⇒ String Internal use only
-
#scope ⇒ String
Internal use only
Returns the scope of the request, per the AWS signature V4 specification.
-
#signature ⇒ String
Internal use only
Returns the calculated signature of the canonical request, per the AWS signature V4 specification.
-
#signed_headers_string ⇒ String
Internal use only
Returns semicolon-separated list of names of signed headers, per the AWS signature V4 specification.
-
#validate! ⇒ Hash
Internal use only
Validates the credentials and the constructed request components by sending a real STS GetCallerIdentity request.
- #hmac(key, data) private Internal use only
- #hmac_hex(key, data) private Internal use only
Instance Attribute Details
#access_key_id ⇒ String
(readonly)
# File 'lib/mongo/auth/aws/request.rb', line 77
attr_reader :access_key_id
#host ⇒ String
(readonly)
# File 'lib/mongo/auth/aws/request.rb', line 87
attr_reader :host
#secret_access_key ⇒ String
(readonly)
# File 'lib/mongo/auth/aws/request.rb', line 80
attr_reader :secret_access_key
#server_nonce ⇒ String
(readonly)
# File 'lib/mongo/auth/aws/request.rb', line 90
attr_reader :server_nonce
#session_token ⇒ String
(readonly)
# File 'lib/mongo/auth/aws/request.rb', line 84
attr_reader :session_token
#time ⇒ Time
(readonly)
# File 'lib/mongo/auth/aws/request.rb', line 93
attr_reader :time
Instance Method Details
#authorization ⇒ String
Returns the value of the Authorization header, per the AWS signature V4 specification.
# File 'lib/mongo/auth/aws/request.rb', line 235
def "AWS4-HMAC-SHA256 Credential=#{access_key_id}/#{scope}, SignedHeaders=#{signed_headers_string}, Signature=#{signature}" end
#canonical_request ⇒ String
Returns the canonical request used during calculation of AWS V4 signature.
# File 'lib/mongo/auth/aws/request.rb', line 196
def canonical_request headers = headers_to_sign serialized_headers = headers.map do |k, v| "#{k}:#{v}" end.join("\n") hashed_payload = Digest::SHA256.new.update(STS_REQUEST_BODY).hexdigest "POST\n/\n\n" + # There are two newlines after serialized headers because the # signature V4 specification treats each header as containing the # terminating newline, and there is an additional newline # separating headers from the signed header names. "#{serialized_headers}\n\n" + "#{signed_headers_string}\n" + hashed_payload end
#formatted_date ⇒ String
# File 'lib/mongo/auth/aws/request.rb', line 102
def formatted_date formatted_time[0, 8] end
#formatted_time ⇒ String
# File 'lib/mongo/auth/aws/request.rb', line 97
def formatted_time @formatted_time ||= @time.getutc.strftime('%Y%m%dT%H%M%SZ') end
#headers ⇒ <Hash
>
Not all of these headers are part of the signed headers list, the keys of the hash are not necessarily ordered lexicographically, and the keys may be in any case.
Returns the hash containing the headers of the calculated canonical request.
# File 'lib/mongo/auth/aws/request.rb', line 147
def headers headers = { 'content-length' => STS_REQUEST_BODY.length.to_s, 'content-type' => 'application/x-www-form-urlencoded', 'host' => host, 'x-amz-date' => formatted_time, 'x-mongodb-gs2-cb-flag' => 'n', 'x-mongodb-server-nonce' => Base64.encode64(server_nonce).gsub("\n", ''), } if session_token headers['x-amz-security-token'] = session_token end headers end
#headers_to_sign ⇒ <Hash
>
Returns the hash containing the headers of the calculated canonical request that should be signed, in a ready to sign form.
The differences between #headers and this method is this method:
-
Removes any headers that are not to be signed. Per AWS specifications it should be possible to sign all headers, but MongoDB server expects only some headers to be signed and will not form the correct request if other headers are signed.
-
Lowercases all header names.
-
Orders the headers lexicographically in the hash.
#hmac(key, data) (private)
# File 'lib/mongo/auth/aws/request.rb', line 273
def hmac(key, data) OpenSSL::HMAC.digest("SHA256", key, data) end
#hmac_hex(key, data) (private)
# File 'lib/mongo/auth/aws/request.rb', line 277
def hmac_hex(key, data) OpenSSL::HMAC.hexdigest("SHA256", key, data) end
#region ⇒ String
# File 'lib/mongo/auth/aws/request.rb', line 107
def region # Common case if host == 'sts.amazonaws.com' return 'us-east-1' end if host.start_with?('.') raise Error::InvalidServerAuthHost, "Host begins with a period: #{host}" end if host.end_with?('.') raise Error::InvalidServerAuthHost, "Host ends with a period: #{host}" end parts = host.split('.') if parts.any? { |part| part.empty? } raise Error::InvalidServerAuthHost, "Host has an empty component: #{host}" end if parts.length == 1 'us-east-1' else parts[1] end end
#scope ⇒ String
Returns the scope of the request, per the AWS signature V4 specification.
# File 'lib/mongo/auth/aws/request.rb', line 135
def scope "#{formatted_date}/#{region}/sts/aws4_request" end
#signature ⇒ String
Returns the calculated signature of the canonical request, per the AWS signature V4 specification.
# File 'lib/mongo/auth/aws/request.rb', line 216
def signature hashed_canonical_request = Digest::SHA256.hexdigest(canonical_request) string_to_sign = "AWS4-HMAC-SHA256\n" + "#{formatted_time}\n" + "#{scope}\n" + hashed_canonical_request # All of the intermediate HMAC operations are not hex-encoded. mac = hmac("AWS4#{secret_access_key}", formatted_date) mac = hmac(mac, region) mac = hmac(mac, 'sts') signing_key = hmac(mac, 'aws4_request') # Only the final HMAC operation is hex-encoded. hmac_hex(signing_key, string_to_sign) end
#signed_headers_string ⇒ String
Returns semicolon-separated list of names of signed headers, per the AWS signature V4 specification.
# File 'lib/mongo/auth/aws/request.rb', line 188
def signed_headers_string headers_to_sign.keys.join(';') end
#validate! ⇒ Hash
Validates the credentials and the constructed request components by sending a real STS GetCallerIdentity request.
# File 'lib/mongo/auth/aws/request.rb', line 243
def validate! sts_request = Net::HTTP::Post.new("https://#{host}").tap do |req| headers.each do |k, v| req[k] = v end req['authorization'] = req['accept'] = 'application/json' req.body = STS_REQUEST_BODY end http = Net::HTTP.new(host, 443) http.use_ssl = true http.start do resp = Timeout.timeout(VALIDATE_TIMEOUT, Error::CredentialCheckError, 'GetCallerIdentity request timed out') do http.request(sts_request) end payload = JSON.parse(resp.body) if resp.code != '200' aws_code = payload.fetch('Error').fetch('Code') = payload.fetch('Error').fetch('Message') msg = "Credential check for user #{access_key_id} failed with HTTP status code #{resp.code}: #{aws_code}: #{}" msg += '.' unless msg.end_with?('.') msg += " Please check that the credentials are valid, and if they are temporary (i.e. use the session token) that the session token is provided and not expired" raise Error::CredentialCheckError, msg end payload.fetch('GetCallerIdentityResponse').fetch('GetCallerIdentityResult') end end