class Mongo::Auth::Aws::CredentialsRetriever

Retrieves AWS credentials from a variety of sources.

This class provides for AWS credentials retrieval from:

The sources listed above are consulted in the order specified. The first source that contains any of the three credential components (access key id, secret access key or session token) is used. The credential components must form a valid set if any of the components is specified; meaning, access key id and secret access key must always be provided together, and if a session token is provided the key id and secret key must also be provided. If a source provides partial credentials, credential retrieval fails with an exception.

@api private

Constants

METADATA_TIMEOUT

Timeout for metadata operations, in seconds.

The auth spec suggests a 10 second timeout but this seems excessively long given that the endpoint is essentially local.

Attributes

user[R]

@return [ Auth::User | nil ] The user object, if one was provided.

Public Class Methods

new(user = nil, credentials_cache: CredentialsCache.instance) click to toggle source

@param [ Auth::User | nil ] user The user object, if one was provided. @param [ Auth::Aws::CredentialsCache ] credentials_cache The credentials cache.

# File lib/mongo/auth/aws/credentials_retriever.rb, line 61
def initialize(user = nil, credentials_cache: CredentialsCache.instance)
  @user = user
  @credentials_cache = credentials_cache
end

Public Instance Methods

credentials() click to toggle source

Retrieves a valid set of credentials, if possible, or raises Auth::InvalidConfiguration.

@return [ Auth::Aws::Credentials ] A valid set of credentials.

@raise Auth::InvalidConfiguration if a source contains an invalid set

of credentials.

@raise Auth::Aws::CredentialsNotFound if credentials could not be

retrieved from any source.
# File lib/mongo/auth/aws/credentials_retriever.rb, line 78
def credentials
  credentials = credentials_from_user(user)
  return credentials unless credentials.nil?

  credentials = credentials_from_environment
  return credentials unless credentials.nil?

  credentials = @credentials_cache.fetch { obtain_credentials_from_endpoints }
  return credentials unless credentials.nil?

  raise Auth::Aws::CredentialsNotFound
end

Private Instance Methods

credentials_from_environment() click to toggle source

Returns credentials from environment variables.

@return [ Auth::Aws::Credentials | nil ] A set of credentials, or nil

if retrieval failed or the obtained credentials are invalid.

@raise Auth::InvalidConfiguration if a source contains an invalid set

of credentials.
# File lib/mongo/auth/aws/credentials_retriever.rb, line 119
def credentials_from_environment
  credentials = Credentials.new(
    ENV['AWS_ACCESS_KEY_ID'],
    ENV['AWS_SECRET_ACCESS_KEY'],
    ENV['AWS_SESSION_TOKEN']
  )
  credentials if credentials && credentials_valid?(credentials, 'environment variables')
end
credentials_from_user(user) click to toggle source

Returns credentials from the user object.

@param [ Auth::User | nil ] user The user object, if one was provided.

@return [ Auth::Aws::Credentials | nil ] A set of credentials, or nil

@raise Auth::InvalidConfiguration if a source contains an invalid set

of credentials.
# File lib/mongo/auth/aws/credentials_retriever.rb, line 101
def credentials_from_user(user)
  return nil unless user

  credentials = Credentials.new(
    user.name,
    user.password,
    user.auth_mech_properties['aws_session_token']
  )
  return credentials if credentials_valid?(credentials, 'Mongo::Client URI or Ruby options')
end
credentials_from_web_identity_response(response) click to toggle source

Extracts credentials from AssumeRoleWithWebIdentity response.

@param [ Net::HTTPResponse ] response AssumeRoleWithWebIdentity

call response.

@return [ Auth::Aws::Credentials | nil ] A set of credentials, or nil

if response parsing failed.
# File lib/mongo/auth/aws/credentials_retriever.rb, line 302
def credentials_from_web_identity_response(response)
  payload = JSON.parse(response.body).dig(
    'AssumeRoleWithWebIdentityResponse',
    'AssumeRoleWithWebIdentityResult',
    'Credentials'
  ) || {}
  Credentials.new(
    payload['AccessKeyId'],
    payload['SecretAccessKey'],
    payload['SessionToken'],
    Time.at(payload['Expiration'])
  )
rescue JSON::ParserError, TypeError
  nil
end
credentials_valid?(credentials, source) click to toggle source

Checks whether the credentials provided are valid.

Returns true if they are valid, false if they are empty, and raises Auth::InvalidConfiguration if the credentials are incomplete (i.e. some of the components are missing).

# File lib/mongo/auth/aws/credentials_retriever.rb, line 329
def credentials_valid?(credentials, source)
  unless credentials.access_key_id || credentials.secret_access_key ||
    credentials.session_token
  then
    return false
  end

  if credentials.access_key_id || credentials.secret_access_key
    if credentials.access_key_id && !credentials.secret_access_key
      raise Auth::InvalidConfiguration,
        "Access key ID is provided without secret access key (source: #{source})"
    end

    if credentials.secret_access_key && !credentials.access_key_id
      raise Auth::InvalidConfiguration,
        "Secret access key is provided without access key ID (source: #{source})"
    end

  elsif credentials.session_token
    raise Auth::InvalidConfiguration,
      "Session token is provided without access key ID or secret access key (source: #{source})"
  end

  true
end
ec2_metadata_credentials() click to toggle source

Returns credentials from the EC2 metadata endpoint. The credentials could be empty, partial or invalid.

@return [ Auth::Aws::Credentials | nil ] A set of credentials, or nil

if retrieval failed.
# File lib/mongo/auth/aws/credentials_retriever.rb, line 150
def ec2_metadata_credentials
  http = Net::HTTP.new('169.254.169.254')
  req = Net::HTTP::Put.new('/latest/api/token',
    # The TTL is required in order to obtain the metadata token.
    {'x-aws-ec2-metadata-token-ttl-seconds' => '30'})
  resp = ::Timeout.timeout(METADATA_TIMEOUT) do
    http.request(req)
  end
  if resp.code != '200'
    return nil
  end
  metadata_token = resp.body
  resp = ::Timeout.timeout(METADATA_TIMEOUT) do
    http_get(http, '/latest/meta-data/iam/security-credentials', metadata_token)
  end
  if resp.code != '200'
    return nil
  end
  role_name = resp.body
  escaped_role_name = CGI.escape(role_name).gsub('+', '%20')
  resp = ::Timeout.timeout(METADATA_TIMEOUT) do
    http_get(http, "/latest/meta-data/iam/security-credentials/#{escaped_role_name}", metadata_token)
  end
  if resp.code != '200'
    return nil
  end
  payload = JSON.parse(resp.body)
  unless payload['Code'] == 'Success'
    return nil
  end
  Credentials.new(
    payload['AccessKeyId'],
    payload['SecretAccessKey'],
    payload['Token'],
    DateTime.parse(payload['Expiration']).to_time
  )
# When trying to use the EC2 metadata endpoint on ECS:
# Errno::EINVAL: Failed to open TCP connection to 169.254.169.254:80 (Invalid argument - connect(2) for "169.254.169.254" port 80)
rescue ::Timeout::Error, IOError, SystemCallError, TypeError
  return nil
end
ecs_metadata_credentials() click to toggle source
# File lib/mongo/auth/aws/credentials_retriever.rb, line 192
def ecs_metadata_credentials
  relative_uri = ENV['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI']
  if relative_uri.nil? || relative_uri.empty?
    return nil
  end

  http = Net::HTTP.new('169.254.170.2')
  # Per https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html
  # the value in AWS_CONTAINER_CREDENTIALS_RELATIVE_URI includes
  # the leading slash.
  # The current language in MONGODB-AWS specification implies that
  # a leading slash must be added by the driver, but this is not
  # in fact needed.
  req = Net::HTTP::Get.new(relative_uri)
  resp = ::Timeout.timeout(METADATA_TIMEOUT) do
    http.request(req)
  end
  if resp.code != '200'
    return nil
  end
  payload = JSON.parse(resp.body)
  Credentials.new(
    payload['AccessKeyId'],
    payload['SecretAccessKey'],
    payload['Token'],
    DateTime.parse(payload['Expiration']).to_time
  )
rescue ::Timeout::Error, IOError, SystemCallError, TypeError
  return nil
end
http_get(http, uri, metadata_token) click to toggle source
# File lib/mongo/auth/aws/credentials_retriever.rb, line 318
def http_get(http, uri, metadata_token)
  req = Net::HTTP::Get.new(uri,
    {'x-aws-ec2-metadata-token' => metadata_token})
  http.request(req)
end
obtain_credentials_from_endpoints() click to toggle source

Returns credentials from the AWS metadata endpoints.

@return [ Auth::Aws::Credentials | nil ] A set of credentials, or nil

if retrieval failed or the obtained credentials are invalid.

@raise Auth::InvalidConfiguration if a source contains an invalid set

of credentials.
# File lib/mongo/auth/aws/credentials_retriever.rb, line 135
def obtain_credentials_from_endpoints
  if (credentials = web_identity_credentials) && credentials_valid?(credentials, 'Web identity token')
    credentials
  elsif (credentials = ecs_metadata_credentials) && credentials_valid?(credentials, 'ECS task metadata')
    credentials
  elsif (credentials = ec2_metadata_credentials) && credentials_valid?(credentials, 'EC2 instance metadata')
    credentials
  end
end
prepare_web_identity_inputs() click to toggle source

Returns inputs for the AssumeRoleWithWebIdentity AWS API call.

@return [ Array<String | nil, String | nil, String | nil> ] Web

identity token, role arn, and role session name.
# File lib/mongo/auth/aws/credentials_retriever.rb, line 244
def prepare_web_identity_inputs
  token_file = ENV['AWS_WEB_IDENTITY_TOKEN_FILE']
  role_arn = ENV['AWS_ROLE_ARN']
  if token_file.nil? || role_arn.nil?
    return nil
  end
  web_identity_token = File.open(token_file).read
  role_session_name = ENV['AWS_ROLE_SESSION_NAME']
  if role_session_name.nil?
    role_session_name = "ruby-app-#{SecureRandom.alphanumeric(50)}"
  end
  [web_identity_token, role_arn, role_session_name]
rescue Errno::ENOENT, IOError, SystemCallError
  nil
end
request_web_identity_credentials(token, role_arn, role_session_name) click to toggle source

Calls AssumeRoleWithWebIdentity to obtain credentials for the given web identity token.

@param [ String ] token The OAuth 2.0 access token or

OpenID Connect ID token that is provided by the identity provider.

@param [ String ] role_arn The Amazon Resource Name (ARN) of the role

that the caller is assuming.

@param [ String ] role_session_name An identifier for the assumed

role session.

@return [ Net::HTTPResponse | nil ] AWS API response if successful,

otherwise nil.
# File lib/mongo/auth/aws/credentials_retriever.rb, line 272
def request_web_identity_credentials(token, role_arn, role_session_name)
  uri = URI('https://sts.amazonaws.com/')
  params = {
    'Action' => 'AssumeRoleWithWebIdentity',
    'Version' => '2011-06-15',
    'RoleArn' => role_arn,
    'WebIdentityToken' => token,
    'RoleSessionName' => role_session_name
  }
  uri.query = ::URI.encode_www_form(params)
  req = Net::HTTP::Post.new(uri)
  req['Accept'] = 'application/json'
  resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |https|
    https.request(req)
  end
  if resp.code != '200'
    return nil
  end
  resp
rescue Errno::ENOENT, IOError, SystemCallError
  nil
end
web_identity_credentials() click to toggle source

Returns credentials associated with web identity token that is stored in a file. This authentication mechanism is used to authenticate inside EKS. See docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html for further details.

@return [ Auth::Aws::Credentials | nil ] A set of credentials, or nil

if retrieval failed.
# File lib/mongo/auth/aws/credentials_retriever.rb, line 230
def web_identity_credentials
  web_identity_token, role_arn, role_session_name = prepare_web_identity_inputs
  return nil if web_identity_token.nil?
  response = request_web_identity_credentials(
    web_identity_token, role_arn, role_session_name
  )
  return if response.nil?
  credentials_from_web_identity_response(response)
end