class Mongo::Auth::Aws::Request
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.
@api private
Constants
- STS_REQUEST_BODY
The body of the STS GetCallerIdentity request.
This is currently the only request that this class supports making.
- VALIDATE_TIMEOUT
The timeout, in seconds, to use for validating credentials via STS.
Attributes
@return [ String ] access_key_id
The access key id.
@return [ String ] host The value of Host HTTP header to use.
@return [ String ] secret_access_key
The secret access key.
@return [ String ] server_nonce
The server nonce binary string.
@return [ String ] session_token
The session token for temporary
credentials.
@return [ Time ] time The time of the request.
Public Class Methods
Constructs the request.
@note By overriding the time, it is possible to create reproducible
requests (in other words, replay a request).
@param [ String ] access_key_id
The access key id. @param [ String ] secret_access_key
The secret access key. @param [ String ] session_token
The session token for temporary
credentials.
@param [ String ] host The value of Host HTTP header to use. @param [ String ] server_nonce
The server nonce binary string. @param [ Time ] time The time of the request.
# File lib/mongo/auth/aws/request.rb, line 54 def initialize(access_key_id:, secret_access_key:, session_token: nil, host:, server_nonce:, time: Time.now ) @access_key_id = access_key_id @secret_access_key = secret_access_key @session_token = session_token @host = host @server_nonce = server_nonce @time = time %i(access_key_id secret_access_key host server_nonce).each do |arg| value = instance_variable_get("@#{arg}") if value.nil? || value.empty? raise Error::InvalidServerAuthResponse, "Value for '#{arg}' is required" end end if host && host.length > 255 raise Error::InvalidServerAuthHost, "Value for 'host' is too long: #{@host}" end end
Public Instance Methods
Returns the canonical request used during calculation of AWS V4 signature.
@return [ String ] The canonical request.
# 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
@return [ String ] formatted_date
YYYYMMDD formatted date of the request.
# File lib/mongo/auth/aws/request.rb, line 102 def formatted_date formatted_time[0, 8] end
@return [ String ] formatted_time
ISO8601-formatted time of the
request, as would be used in X-Amz-Date header.
# File lib/mongo/auth/aws/request.rb, line 97 def formatted_time @formatted_time ||= @time.getutc.strftime('%Y%m%dT%H%M%SZ') end
Returns the hash containing the headers of the calculated canonical request.
@note 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.
@return [ <Hash> ] headers The headers.
# 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
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.
@return [ <Hash> ] headers The headers.
# File lib/mongo/auth/aws/request.rb, line 175 def headers_to_sign headers_to_sign = {} headers.keys.sort_by { |k| k.downcase }.each do |key| write_key = key.downcase headers_to_sign[write_key] = headers[key] end headers_to_sign end
@return [ String ] region The region of the host, derived from the host.
# 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
Returns the scope of the request, per the AWS signature V4 specification.
@return [ String ] The scope.
# File lib/mongo/auth/aws/request.rb, line 135 def scope "#{formatted_date}/#{region}/sts/aws4_request" end
Returns the calculated signature of the canonical request, per the AWS signature V4 specification.
@return [ String ] The signature.
# 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
Returns semicolon-separated list of names of signed headers, per the AWS signature V4 specification.
@return [ String ] The signed header list.
# File lib/mongo/auth/aws/request.rb, line 188 def signed_headers_string headers_to_sign.keys.join(';') end
Validates the credentials and the constructed request components by sending a real STS GetCallerIdentity request.
@return [ Hash ] GetCallerIdentity result.
# 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'] = 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') aws_message = payload.fetch('Error').fetch('Message') msg = "Credential check for user #{access_key_id} failed with HTTP status code #{resp.code}: #{aws_code}: #{aws_message}" 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
Private Instance Methods
# File lib/mongo/auth/aws/request.rb, line 273 def hmac(key, data) OpenSSL::HMAC.digest("SHA256", key, data) end
# File lib/mongo/auth/aws/request.rb, line 277 def hmac_hex(key, data) OpenSSL::HMAC.hexdigest("SHA256", key, data) end