123456789_123456789_123456789_123456789_123456789_

Class: Mongo::Auth::Aws::Request Private

Do not use. This class is for internal use only.
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.

Since:

  • 2.0.0

Constant Summary

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Instance Attribute Details

#access_key_idString (readonly)

Returns:

  • (String)

    access_key_id The access key id.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/request.rb', line 70

attr_reader :access_key_id

#hostString (readonly)

Returns:

  • (String)

    host The value of Host HTTP header to use.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/request.rb', line 80

attr_reader :host

#secret_access_keyString (readonly)

Returns:

  • (String)

    secret_access_key The secret access key.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/request.rb', line 73

attr_reader :secret_access_key

#server_nonceString (readonly)

Returns:

  • (String)

    server_nonce The server nonce binary string.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/request.rb', line 83

attr_reader :server_nonce

#session_tokenString (readonly)

Returns:

  • (String)

    session_token The session token for temporary credentials.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/request.rb', line 77

attr_reader :session_token

#timeTime (readonly)

Returns:

  • (Time)

    time The time of the request.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/request.rb', line 86

attr_reader :time

Instance Method Details

#authorizationString

Returns the value of the Authorization header, per the AWS signature V4 specification.

Returns:

  • (String)

    Authorization header value.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/request.rb', line 220

def authorization
  "AWS4-HMAC-SHA256 Credential=#{access_key_id}/#{scope}, SignedHeaders=#{signed_headers_string}, Signature=#{signature}"
end

#canonical_requestString

Returns the canonical request used during calculation of AWS V4 signature.

Returns:

  • (String)

    The canonical request.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/request.rb', line 181

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_dateString

Returns:

  • (String)

    formatted_date YYYYMMDD formatted date of the request.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/request.rb', line 95

def formatted_date
  formatted_time[0, 8]
end

#formatted_timeString

Returns:

  • (String)

    formatted_time ISO8601-formatted time of the request, as would be used in X-Amz-Date header.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/request.rb', line 90

def formatted_time
  @formatted_time ||= @time.getutc.strftime('%Y%m%dT%H%M%SZ')
end

#headers ⇒ <Hash>

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.

Returns the hash containing the headers of the calculated canonical request.

Returns:

  • (<Hash>)

    headers The headers.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/request.rb', line 134

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).delete("\n"),
  }
  headers['x-amz-security-token'] = session_token if session_token
  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.

Returns:

  • (<Hash>)

    headers The headers.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/request.rb', line 160

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

#hmac(key, data) (private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/request.rb', line 259

def hmac(key, data)
  OpenSSL::HMAC.digest('SHA256', key, data)
end

#hmac_hex(key, data) (private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/request.rb', line 263

def hmac_hex(key, data)
  OpenSSL::HMAC.hexdigest('SHA256', key, data)
end

#regionString

Returns:

  • (String)

    region The region of the host, derived from the host.

Raises:

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/request.rb', line 100

def region
  # Common case
  return 'us-east-1' if host == 'sts.amazonaws.com'

  raise Error::InvalidServerAuthHost, "Host begins with a period: #{host}" if host.start_with?('.')
  raise Error::InvalidServerAuthHost, "Host ends with a period: #{host}" if host.end_with?('.')

  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

#scopeString

Returns the scope of the request, per the AWS signature V4 specification.

Returns:

  • (String)

    The scope.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/request.rb', line 122

def scope
  "#{formatted_date}/#{region}/sts/aws4_request"
end

#signatureString

Returns the calculated signature of the canonical request, per the AWS signature V4 specification.

Returns:

  • (String)

    The signature.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/request.rb', line 201

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_stringString

Returns semicolon-separated list of names of signed headers, per the AWS signature V4 specification.

Returns:

  • (String)

    The signed header list.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/request.rb', line 173

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.

Returns:

  • (Hash)

    GetCallerIdentity result.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/request.rb', line 228

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