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



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

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



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

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



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

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



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

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



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

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



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

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



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

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



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

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



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/mongo/auth/aws/request.rb', line 194

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



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

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



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

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



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/mongo/auth/aws/request.rb', line 144

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", ''),
  }
  # Hash#compact is available as of Ruby 2.4
  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



173
174
175
176
177
178
179
180
# File 'lib/mongo/auth/aws/request.rb', line 173

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



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/mongo/auth/aws/request.rb', line 104

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



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

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



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/mongo/auth/aws/request.rb', line 214

def signature
  hashed_canonical_request = Digest::SHA256.hexdigest(canonical_request)
  hashed_body = Digest::SHA256.new.update(STS_REQUEST_BODY).hexdigest
  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



186
187
188
# File 'lib/mongo/auth/aws/request.rb', line 186

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



242
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
# File 'lib/mongo/auth/aws/request.rb', line 242

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