class Aws::Google

An auto-refreshing credential provider that works by assuming a role via {Aws::STS::Client#assume_role_with_web_identity}, using an ID token derived from a Google refresh token.

role_credentials = Aws::Google.new(
  role_arn: aws_role,
  google_client_id: client_id,
  google_client_secret: client_secret
)

ec2 = Aws::EC2::Client.new(credentials: role_credentials)

If you omit `:client` option, a new {Aws::STS::Client} object will be constructed.

Constants

VERSION

Attributes

config[RW]

Use `Aws::Google.config` to set default options for any instance of this provider.

Public Class Methods

new(options = {}) click to toggle source

@option options [required, String] :role_arn @option options [String] :policy @option options [Integer] :duration_seconds @option options [String] :external_id @option options [STS::Client] :client STS::Client to use (default: create new client) @option options [String] :domain G Suite domain for account-selection hint @option options [String] :online if `true` only a temporary access token will be provided,

a long-lived refresh token will not be created and stored on the filesystem.

@option options [String] :port port for local server to listen on to capture oauth browser redirect.

Defaults to 1234. Set to nil or 0 to use an out-of-band authentication process.

@option options [String] :client_id Google client ID @option options [String] :client_secret Google client secret

Calls superclass method Aws::Google::CachedCredentials::new
# File lib/aws/google.rb, line 47
def initialize(options = {})
  options = options.merge(self.class.config)
  @oauth_attempted = false
  @assume_role_params = options.slice(
    *Aws::STS::Client.api.operation(:assume_role_with_web_identity).
      input.shape.member_names
  )

  @google_id = ::Google::Auth::ClientId.new(
    options[:client_id],
    options[:client_secret]
  )
  @client = options[:client] || Aws::STS::Client.new(credentials: nil)
  @domain = options[:domain]
  @online = options[:online]
  @port = options[:port] || 1234
  super
end

Private Instance Methods

get_oauth_code(client, options) click to toggle source
# File lib/aws/google.rb, line 110
def get_oauth_code(client, options)
  raise 'fallback' unless @port && !@port.zero?

  require 'launchy'
  require 'webrick'
  code = nil
  server = WEBrick::HTTPServer.new(
    Port: @port,
    Logger: WEBrick::Log.new(STDOUT, 0),
    AccessLog: []
  )
  server.mount_proc '/' do |req, res|
    code = req.query['code']
    res.status = 202
    res.body = 'Login successful, you may close this browser window.'
    server.stop
  end
  trap('INT') { server.shutdown }
  client.redirect_uri = "http://localhost:#{@port}"
  silence_output do
    launchy = Launchy.open(client.authorization_uri(options).to_s)
    server_thread = Thread.new do
      begin
        server.start
      ensure server.shutdown
      end
    end
    while server_thread.alive?
      raise 'fallback' if !launchy.alive? && !launchy.value.success?

      sleep 0.1
    end
  end
  code || raise('fallback')
rescue StandardError
  trap('INT', 'DEFAULT')
  # Fallback to out-of-band authentication if browser launch failed.
  client.redirect_uri = 'oob'
  return ENV['OAUTH_CODE'] if ENV['OAUTH_CODE']

  raise RuntimeError, 'Open the following URL in a browser to get a code,' \
         "export to $OAUTH_CODE and rerun:\n#{client.authorization_uri(options)}", []
end
google_client() click to toggle source

Use cached Application Default Credentials if available, otherwise fallback to creating new Google credentials through browser login.

# File lib/aws/google.rb, line 70
def google_client
  @google_client ||= (::Google::Auth.get_application_default rescue nil) || google_oauth
end
google_oauth() click to toggle source

Create an OAuth2 Client using Google's default browser-based OAuth InstalledAppFlow. Store cached credentials to the standard Google Application Default Credentials location. Ref: goo.gl/IUuyuX @return [Signet::OAuth2::Client]

# File lib/aws/google.rb, line 78
def google_oauth
  return nil if @oauth_attempted
  @oauth_attempted = true

  path = "#{ENV['HOME']}/.config/#{::Google::Auth::CredentialsLoader::WELL_KNOWN_PATH}"
  FileUtils.mkdir_p(File.dirname(path))
  storage = GoogleStorage.new(::Google::APIClient::FileStore.new(path))

  options = {
    client_id: @google_id.id,
    client_secret: @google_id.secret,
    scope: %w[openid email]
  }
  uri_options = {include_granted_scopes: true}
  uri_options[:hd] = @domain if @domain
  uri_options[:access_type] = 'online' if @online

  credentials = ::Google::Auth::UserRefreshCredentials.new(options)
  credentials.code = get_oauth_code(credentials, uri_options)
  credentials.fetch_access_token!
  credentials.tap(&storage.method(:write_credentials))
end
refresh() click to toggle source
# File lib/aws/google.rb, line 154
    def refresh
      assume_role = begin
        client = google_client
        return unless client

        begin
          tries ||= 2
          id_token = client.id_token
          # Decode the JWT id_token to use the Google email as the AWS role session name.
          token_params = JWT.decode(id_token, nil, false).first
        rescue JWT::DecodeError, JWT::ExpiredSignature
          # Refresh and retry once if token is expired or invalid.
          client.refresh!
          raise if (tries -= 1).zero?
          retry
        end

        @client.assume_role_with_web_identity(
          @assume_role_params.merge(
            web_identity_token: id_token,
            role_session_name: token_params['email']
          )
        )
      rescue Signet::AuthorizationError, Aws::STS::Errors::ExpiredTokenException
        retry if (@google_client = google_oauth)
        raise
      rescue Aws::STS::Errors::AccessDenied => e
        retry if (@google_client = google_oauth)
        raise e, "\nYour Google ID does not have access to the requested AWS Role. Ask your administrator to provide access.
Role: #{@assume_role_params[:role_arn]}
Email: #{token_params['email']}
Google ID: #{token_params['sub']}", []
      end

      c = assume_role.credentials
      @credentials = Aws::Credentials.new(
        c.access_key_id,
        c.secret_access_key,
        c.session_token
      )
      @expiration = c.expiration.to_i
    end
silence_output() { || ... } click to toggle source
# File lib/aws/google.rb, line 101
def silence_output
  outs = [$stdout, $stderr]
  clones = outs.map(&:clone)
  outs.each { |io| io.reopen '/dev/null'}
  yield
ensure
  outs.each_with_index { |io, i| io.reopen(clones[i]) }
end