google-cloud-platformjwtgoogle-cloud-endpointsgrpc

Google Cloud Endpoints Metadata Server Authentication


CONTEXT:

For a project I am working on I have setup service to service authentication using a service account for a gRPC API sat behind the Extensible Service Proxy (ESP) as per the instructions here. For reference my authentication configuration looks like the following

authentication:
  providers:
    - id: google_service_account
      issuer: <service-account-email>
      jwks_uri: https://www.googleapis.com/robot/v1/metadata/x509/<service-account-email>
  rules:
    - selector: "*"
      requirements:
        - provider_id: google_service_account

I then have a ruby client that reads in a service account key (attained through the GCP console) from disk and generates a JWT using the googleauth gem to be used to authenticate with the API.

module Authenticated
  def credentials
    @credentials ||= Google::Auth::ServiceAccountJwtHeaderCredentials
      .make_creds(json_key_io: service_account_json_io)
      .apply(jwt_aud_uri: ENV.fetch('SERVICE_NAME'))
  end

  def self.service_account
    @service_account ||= StringIO.new(
      File.read('/etc/secrets/service-account.json'),
    )
  end

  private

  def service_account_json_io
    Authenticated.service_account.tap(&:rewind)
  end
end

Currently this is in a working state and the client is able to authentication with ESP.

THE PROBLEM:

Since implementing the above I have another client application needing to reuse the same API. This means having to generate a new service account key and mounting that into the new application for authentication. Eventually if I create more clients then having to securely store many service account keys is error prone and a potential security risk. Instead I'd like to use the GCE metadata server to generate the JWT from lets say the default compute engine service account (although I may use a different account later) and pass this through to the ESP.

What I have tried so far is to change the ESP authentication configuration as follows

authentication:
  providers:
    - id: google_service_account
      issuer: https://accounts.google.com
      jwks_uri: https://www.googleapis.com/robot/v1/metadata/x509/<gce-default-service-account-email>
  rules:
    - selector: "*"
      requirements:
        - provider_id: google_service_account

and updated the ruby client to request a JWT from the metadata server as follows

module Authenticated
  METADATA_SERVER_IDENTITY_URI = 'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?format=full&audience='.freeze

  def credentials
    { authorization: "Bearer #{identity_jwt}" }
  end

  private

  def identity_jwt
    http = Net::HTTP.new(identity_uri.hostname)
    http.request(identity_request).body
  end

  def identity_request
    Net::HTTP::Get.new(identity_uri).tap do |req|
      req.add_field('Metadata-Flavor', 'Google')
    end
  end

  def identity_uri
    URI.parse("#{METADATA_SERVER_IDENTITY_URI}https://#{ENV['SERVICE_NAME']}")
  end
end

This again generates a JWT however this time with the issuer set to https://accounts.google.com (as reflected in the ESP authentication configuration). However this time the client is unable to authenticate with the ESP reporting Error: KEY_RETRIEVAL_ERROR

THE QUESTION:

Is it possible to authenticate against the ESP using a JWT generated through the GCE metadata server? And what are the configuration steps?


Solution

  • By removing the jwks key from the Google Cloud Endpoints configuration yaml it forces ESP to use OpenID discovery to fetch the jwks_uri that will be used to obtain the correct JWK. This can be seen in the api manager configuration.

    Given the same issuer defined in the question (accounts.google.com) the code will generate a URL to https://accounts.google.com/.well-known/openid-configuration containing the jwks_uri. ESP uses the jwks specified here to verify a JWT.