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

Inherits:
Object
  • Object
show all
Defined in:
lib/mongo/auth/aws/request.rb

Overview

This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.

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 collapse

STS_REQUEST_BODY =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

The body of the STS GetCallerIdentity request.

This is currently the only request that this class supports making.

Since:

  • 2.0.0

'Action=GetCallerIdentity&Version=2011-06-15'
VALIDATE_TIMEOUT =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

The timeout, in seconds, to use for validating credentials via STS.

Since:

  • 2.0.0

10

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(access_key_id:, secret_access_key:, host:, server_nonce:, session_token: nil, time: Time.now) ⇒ Request

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Note:

By overriding the time, it is possible to create reproducible requests (in other words, replay a request).

Constructs the request.

Parameters:

  • access_key_id (String)

    The access key id.

  • secret_access_key (String)

    The secret access key.

  • session_token (String) (defaults to: nil)

    The session token for temporary credentials.

  • host (String)

    The value of Host HTTP header to use.

  • server_nonce (String)

    The server nonce binary string.

  • time (Time) (defaults to: Time.now)

    The time of the request.

Raises:

Since:

  • 2.0.0



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/mongo/auth/aws/request.rb', line 51

def initialize(access_key_id:, secret_access_key:, host:, server_nonce:, session_token: nil, 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}")
    raise Error::InvalidServerAuthResponse, "Value for '#{arg}' is required" if value.nil? || value.empty?
  end

  return unless host && host.length > 255

  raise Error::InvalidServerAuthHost, "Value for 'host' is too long: #{@host}"
end

Instance Attribute Details

#access_key_idString (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns access_key_id The access key id.

Returns:

  • (String)

    access_key_id The access key id.

Since:

  • 2.0.0



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

def access_key_id
  @access_key_id
end

#hostString (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns host The value of Host HTTP header to use.

Returns:

  • (String)

    host The value of Host HTTP header to use.

Since:

  • 2.0.0



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

def host
  @host
end

#secret_access_keyString (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns secret_access_key The secret access key.

Returns:

  • (String)

    secret_access_key The secret access key.

Since:

  • 2.0.0



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

def secret_access_key
  @secret_access_key
end

#server_nonceString (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns server_nonce The server nonce binary string.

Returns:

  • (String)

    server_nonce The server nonce binary string.

Since:

  • 2.0.0



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

def server_nonce
  @server_nonce
end

#session_tokenString (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns session_token The session token for temporary credentials.

Returns:

  • (String)

    session_token The session token for temporary credentials.

Since:

  • 2.0.0



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

def session_token
  @session_token
end

#timeTime (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns time The time of the request.

Returns:

  • (Time)

    time The time of the request.

Since:

  • 2.0.0



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

def time
  @time
end

Instance Method Details

#authorizationString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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

Returns:

  • (String)

    Authorization header value.

Since:

  • 2.0.0



220
221
222
# 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

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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

Returns:

  • (String)

    The canonical request.

Since:

  • 2.0.0



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# 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

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns formatted_date YYYYMMDD formatted date of the request.

Returns:

  • (String)

    formatted_date YYYYMMDD formatted date of the request.

Since:

  • 2.0.0



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

def formatted_date
  formatted_time[0, 8]
end

#formatted_timeString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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

Returns:

  • (String)

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

Since:

  • 2.0.0



90
91
92
# 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>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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



134
135
136
137
138
139
140
141
142
143
144
145
# 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>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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



160
161
162
163
164
165
166
167
# 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

#regionString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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

Returns:

  • (String)

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

Raises:

Since:

  • 2.0.0



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# 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

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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

Returns:

  • (String)

    The scope.

Since:

  • 2.0.0



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

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

#signatureString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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

Returns:

  • (String)

    The signature.

Since:

  • 2.0.0



201
202
203
204
205
206
207
208
209
210
211
212
213
214
# 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

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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



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

def signed_headers_string
  headers_to_sign.keys.join(';')
end

#validate!Hash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Validates the credentials and the constructed request components by sending a real STS GetCallerIdentity request.

Returns:

  • (Hash)

    GetCallerIdentity result.

Since:

  • 2.0.0



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# 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