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

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.

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.

Since:

  • 2.0.0



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.

Returns:

  • (String)

    access_key_id The access key id.

Since:

  • 2.0.0



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.

Returns:

  • (String)

    host The value of Host HTTP header to use.

Since:

  • 2.0.0



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.

Returns:

  • (String)

    secret_access_key The secret access key.

Since:

  • 2.0.0



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.

Returns:

  • (String)

    server_nonce The server nonce binary string.

Since:

  • 2.0.0



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.

Returns:

  • (String)

    session_token The session token for temporary credentials.

Since:

  • 2.0.0



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.

Returns:

  • (Time)

    time The time of the request.

Since:

  • 2.0.0



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.

Returns:

  • (String)

    Authorization header value.

Since:

  • 2.0.0



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.

Returns:

  • (String)

    The canonical request.

Since:

  • 2.0.0



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.

Returns:

  • (String)

    formatted_date YYYYMMDD formatted date of the request.

Since:

  • 2.0.0



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.

Returns:

  • (String)

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

Since:

  • 2.0.0



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.

Returns:

  • (<Hash>)

    headers The headers.

Since:

  • 2.0.0



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.

Returns:

  • (<Hash>)

    headers The headers.

Since:

  • 2.0.0



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.

Returns:

  • (String)

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

Since:

  • 2.0.0



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.

Returns:

  • (String)

    The scope.

Since:

  • 2.0.0



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.

Returns:

  • (String)

    The signature.

Since:

  • 2.0.0



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.

Returns:

  • (String)

    The signed header list.

Since:

  • 2.0.0



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.

Returns:

  • (Hash)

    GetCallerIdentity result.

Since:

  • 2.0.0



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