Как аутентифицировать учетную запись службы с помощью функции облака Google?

Я пытаюсь настроить функцию облака Google (2-го поколения с триггером HTTP), но, похоже, не могу пройти неавторизованную ошибку 401. Я заработал, используя токен удостоверения, который я сгенерировал через интерфейс gcloud, но мне нужно использовать учетную запись службы, чтобы избежать истечения срока действия токена удостоверения в 1 час.

Я установил разрешения для вызова облачной функции и запуска облака для учетной записи службы, а также напрямую подключил учетную запись службы к облачной функции. Несмотря на то, что я делаю, я не могу добиться авторизации с использованием учетной записи службы.

Я пытаюсь пройти аутентификацию в приложении Rails, используя драгоценный камень Googleauth. Ниже мой сервис.

# app/services/cloud_function_service.rb

class CloudFunctionService
  # Use the credentials and URL from Rails credentials or environment
  def self.api_request(function_type, body_data)
    new.api_request(function_type, body_data)
  end

  def api_request(function_type, body_data)
    access_token = fetch_access_token
    uri = Rails.application.credentials.CLOUD_FUNCTION_URL

    body = {
      'function_type': function_type,
      'body': body_data
    }.to_json

    headers = {
      'Authorization' => "Bearer #{access_token}",
      'Content-Type' => 'application/json'
    }

    # Make the POST request with headers and body
    result = HTTParty.post(uri, body: body, headers: headers)
    JSON.parse(result.body)
  rescue => e
    Rails.logger.error "Failed to call cloud function: #{e}"
    nil
  end

  private

  def fetch_access_token
    authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
      json_key_io: File.open('./google-service-account-keys.json'),
      scope: 'https://www.googleapis.com/auth/cloud-platform'
    )
    response = authorizer.fetch_access_token!
    response['access_token']
  end
end

🤔 А знаете ли вы, что...
Ruby on Rails активно использует принцип DRY (Don't Repeat Yourself), что способствует уменьшению дублирования кода.


445
1

Ответ:

Решено

Отвечаю здесь на свой вопрос. На самом деле существовало две проблемы, которые могут оказаться полезными для всех, кто вызывает облачную функцию из приложения Ruby on Rails.

Во-первых, я неправильно понял использование токена доступа и токена идентификации, но на самом деле вам нужны оба. Токен доступа необходим для доступа к конечным точкам API Google (конечные точки облачных функций не считаются частью). Токен идентификации необходим для доступа к конечным точкам облачной функции. Но... вы используете токен доступа, чтобы получить токен идентификации.

Во-вторых, в отличие от других языковых библиотек для Google Auth, библиотека Ruby не предоставляет метода получения токена идентификации. Он предоставляет только метод получения токена доступа. Вам необходимо создать собственный обработчик API для конечной точки токена идентификации.

В принципе поток выглядит так:

  1. Используйте GoogleAuth, чтобы получить токен доступа для учетной записи службы.
  2. Укажите этот токен доступа в запросе на получение токена идентификации (поскольку вы нажимаете здесь API Google, вы используете токен доступа для получения токена идентификатора)
  3. Укажите URL-адрес своей облачной функции, указав токен идентификации в качестве токена носителя в заголовке аутентификации.

Вот мой окончательный код, который работает!

class CloudFunctionService
  def self.api_request(function_type, body_data)
    new.api_request(function_type, body_data)
  end

  def api_request(function_type, body_data)
    access_token = fetch_access_token
    service_account_email = _your_gcp_service_account_email_
    target_audience = _your_cloud_function_uri_
    identity_token = fetch_identity_token(service_account_email, target_audience, access_token)
    uri = _your_cloud_function_uri_

    body = {
      # Your body data
    }.to_json

    headers = {
      'Authorization' => "Bearer #{identity_token}",
      'Content-Type' => 'application/json'
    }

    result = HTTParty.post(uri, body: body, headers: headers)
    JSON.parse(result.body)
  rescue => e
    Rails.logger.error "Failed to call cloud function: #{e}"
    nil
  end

  private

  def fetch_access_token
    decoded_json_key = Base64.decode64(_your_base64_encoded_json_key_file)
    json_key_io = StringIO.new(decoded_json_key)
    authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
      json_key_io: json_key_io,
      scope: 'https://www.googleapis.com/auth/cloud-platform'
    )

    authorizer.fetch_access_token!['access_token']
  end

  def fetch_identity_token(service_account_email, target_audience, access_token)
    url = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/#{service_account_email}:generateIdToken"
    body = {
      audience: target_audience,
      includeEmail: true
    }.to_json
    response = HTTParty.post(
      url,
      body: body,
      headers: {
        'Content-Type' => 'application/json',
        'Authorization' => "Bearer #{access_token}"
      }
    )
    if response.code == 200
      JSON.parse(response.body)['token']
    else
      raise "Failed to fetch identity token: #{response.body}"
    end
  end
end