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

API:

  • private

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

API:

  • private

"Action=GetCallerIdentity&Version=2011-06-15".freeze
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

API:

  • private

10

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(access_key_id:, secret_access_key:, session_token: nil, host:, server_nonce:, 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.

Since:

  • 2.0.0

Parameters:

  • The access key id.

  • The secret access key.

  • (defaults to: nil)

    The session token for temporary credentials.

  • The value of Host HTTP header to use.

  • The server nonce binary string.

  • (defaults to: Time.now)

    The time of the request.

API:

  • private



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# 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

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.

Since:

  • 2.0.0

Returns:

  • access_key_id The access key id.

API:

  • private



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

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.

Since:

  • 2.0.0

Returns:

  • host The value of Host HTTP header to use.

API:

  • private



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

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.

Since:

  • 2.0.0

Returns:

  • secret_access_key The secret access key.

API:

  • private



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

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.

Since:

  • 2.0.0

Returns:

  • server_nonce The server nonce binary string.

API:

  • private



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

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.

Since:

  • 2.0.0

Returns:

  • session_token The session token for temporary credentials.

API:

  • private



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

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.

Since:

  • 2.0.0

Returns:

  • time The time of the request.

API:

  • private



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

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.

Since:

  • 2.0.0

Returns:

  • Authorization header value.

API:

  • private



235
236
237
# File 'lib/mongo/auth/aws/request.rb', line 235

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.

Since:

  • 2.0.0

Returns:

  • The canonical request.

API:

  • private



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# 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_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.

Since:

  • 2.0.0

Returns:

  • formatted_date YYYYMMDD formatted date of the request.

API:

  • private



102
103
104
# File 'lib/mongo/auth/aws/request.rb', line 102

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.

Since:

  • 2.0.0

Returns:

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

API:

  • private



97
98
99
# 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>

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.

Since:

  • 2.0.0

Returns:

  • headers The headers.

API:

  • private



147
148
149
150
151
152
153
154
155
156
157
158
159
160
# 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>

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.

Since:

  • 2.0.0

Returns:

  • headers The headers.

API:

  • private



175
176
177
178
179
180
181
182
# 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

#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.

Since:

  • 2.0.0

Returns:

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

API:

  • private



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# 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

#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.

Since:

  • 2.0.0

Returns:

  • The scope.

API:

  • private



135
136
137
# File 'lib/mongo/auth/aws/request.rb', line 135

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.

Since:

  • 2.0.0

Returns:

  • The signature.

API:

  • private



216
217
218
219
220
221
222
223
224
225
226
227
228
229
# 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_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.

Since:

  • 2.0.0

Returns:

  • The signed header list.

API:

  • private



188
189
190
# File 'lib/mongo/auth/aws/request.rb', line 188

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.

Since:

  • 2.0.0

Returns:

  • GetCallerIdentity result.

API:

  • private



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# 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