Using Custom Standard HTTP headers for Google Cloud Client Libraries

2021-12-15

GCP Cloud APIs supports a variety of system-headers described here

This section will briefly cover the following headers, when you would use them and how to add them to your API calls.


Authorization

This is the standard header used to authenticate in both REST and gRPC. For REST, it can be set either as a query parameter (&access_token) or as an HTTP header in the form Authorization: Bearer [access_token]. For gRPC, this header is sent in within a gRPC metadata key. Thee tokens are automatically included and attached in when using the GCP Cloud Libraries after local authentication credentials are acquired though Application Default Credentials

These authentication tokens come in several flavors but principally these tokens are Oauth2 tokens issued by Google through one of the flows described in Using OAuth 2.0 to Access Google APIs.

GCP also supports several other token types and mechanisms to exchange third party identities for GCP credentials but ultimately, these tokens are emitted to GCP using this header value.

  • access_token: These are oauth2 access tokens that generally grant access to the GCP resource associated with that user subject to the scopes defined within that token.

  • id_token: These are OpenID Connect tokens issued by Google that is used by callers against user-deployable resource. In this context, a user resource is an application you deploy on a service like Cloud Run, Cloud Functions, Cloud Endpoints. These tokens are also emitted by certain GCP services to authenticate itself to remote services. For more information, see Authenticating using Google OpenID Connect Tokens

  • jwt_access_token: These tokens are a Google-specific oauth to optimization casually described in an Addendum: Service account authorization without OAuth. Basically, this flow uses a service account key to generate a specific JWT which indicates the destination service and is signed by a service account entitled to access that service. The JWT is sent directly to the service and does not involve an intermediate exchange for an access_token. For more information, see Using JWT AccessTokens.

You can print out the details of these tokens using curl and the oauth2 tokeninfo endpoint shown below. id_tokens are just JWTs which you can decode anywhere (eg jwt.io)

$ curl -s https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=`gcloud auth print-access-token` | jq '.'
{
  "azp": "32555940559.apps.googleusercontent.com",
  "aud": "32555940559.apps.googleusercontent.com",
  "sub": "1081579130932748411228",
  "scope": "openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/appengine.admin https://www.googleapis.com/auth/compute https://www.googleapis.com/auth/accounts.reauth",
  "exp": "1640026115",
  "expires_in": "3598",
  "email": "user@domain.com",
  "email_verified": "true"
}
$ curl -s https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=`gcloud auth application-default print-access-token` | jq '.'
{
  "azp": "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com",
  "aud": "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com",
  "sub": "1081579130932748411228",
  "scope": "openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/accounts.reauth",
  "exp": "1640026184",
  "expires_in": "3598",
  "email": "user@domain.com",
  "email_verified": "true"
}
$ curl -s https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=`gcloud auth print-identity-token` | jq '.'
{
  "iss": "https://accounts.google.com",
  "azp": "32555940559.apps.googleusercontent.com",
  "aud": "32555940559.apps.googleusercontent.com",
  "sub": "10449703227021975232",
  "hd": "domain.com",
  "email": "user1@domain.com",
  "email_verified": "true",
  "at_hash": "tBcn_NJEadsztkeLPtna6A",
  "iat": "1640024536",
  "exp": "1640028136",
  "alg": "RS256",
  "kid": "d98f49bc6ca4581eae8dfadd494fce10ea23aab0",
  "typ": "JWT"
}

You might be wondering what the audiences 32555940559.apps.googleusercontent.com and 764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com are. These are the client_id associated with the gcloud and Application Default credentials, respectively. For more info on that, see gcloud alias for Application Default Credentials

You can use these headers directly in curl based commands

The following acquires an access_token and calls a GCP resource

export TOKEN=`gcloud auth print-access-token`

curl -s -H "Authorization: Bearer $TOKEN" https://storage.googleapis.com/storage/v1/b/$BUCKET/o/foo.txt

The following issues an id_token with a STATIC aud: value of 32555940559.apps.googleusercontent.com. Normally, cloud run requires the audience value to match the service name. However, a special allow list was granted for Cloud Run here that allows this through. The following will NOT work for resourced protected by IAP.

export ID_TOKEN=`gcloud auth print-identity-token`

curl -H "Authorization: Bearer $ID_TOKEN" https://cloud_run_url

The following uses service accounts own id_token and then specifies the audience the token should have. The value here matches what Cloud Run expects so the request is allowed through

export ID_TOKEN=`gcloud auth print-identity-token --impersonate-service-account=target-svc-account@developer.gserviceaccount.com  --audiences=https://cloud_run_url`

curl -H "Authorization: Bearer $ID_TOKEN" https://cloud_run_url

The default access_token is emitted by default through Application Default Credentials flow. Using id_token and jwt_access_tokens may require special constructors

For usage with jwt_access_tokens. Note this is currently a rare API auth flow and must be done by the caller.

Use google.auth.jwt.Credentials and specify the audience= value as shown

TODO: sample

Use google.JWTAccessTokenSourceFromJSON

    aud := "https://pubsub.googleapis.com/google.pubsub.v1.Publisher"
	tokenSource, err := google.JWTAccessTokenSourceFromJSON(keyBytes, aud)

	client, err := pubsub.NewClient(ctx, *projectID, option.WithTokenSource(tokenSource))

Use google.auth.JWTAccess

TODO: sample

Not sure how to use this in node

X-Goog-FieldMask

FieldMasks are used to request Partial Responses from a GCP API.

For more information on using it, see examples in Using FieldMask

X-Goog-Api-Key

Google API keys are used by some services metering and billing. Almost no GCP service uses API key alone and elects to use the oauth2 bearer tokens described above.

However, some services like the Natural Language APIs allows for both types of tokens. If an API key is used, its a static token that must be handled and secured. When the API key is used, the project that is associated with the key incurs quota and usage costs.

For example, using API key from ProjectA, the consumer project needs the service enabled (thats still projectA)

$ curl --request POST   "https://language.googleapis.com/v1/documents:analyzeEntities?key=$API_KEY" \
    --header 'Accept: application/json'   --header 'Content-Type: application/json'  \
    --data '{"document":{"content":"Hello World","type":"PLAIN_TEXT"}}'
{
  "error": {
    "code": 403,
    "message": "Cloud Natural Language API has not been used in project 248066739555 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/language.googleapis.com/overview?project=248066739555 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.",
    "status": "PERMISSION_DENIED",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.Help",
        "links": [
          {
            "description": "Google developers console API activation",
            "url": "https://console.developers.google.com/apis/api/language.googleapis.com/overview?project=248066739555"
          }
        ]
      },
      {
        "@type": "type.googleapis.com/google.rpc.ErrorInfo",
        "reason": "SERVICE_DISABLED",
        "domain": "googleapis.com",
        "metadata": {
          "service": "language.googleapis.com",
          "consumer": "projects/248066739555"
        }
      }
    ]
  }
}

After api enablement, calls succeed and the API usage is logged

$ curl --request POST   "https://language.googleapis.com/v1/documents:analyzeEntities?key=$API_KEY"   --header 'Accept: application/json'   --header 'Content-Type: application/json'   --data '{"document":{"content":"Hello World","type":"PLAIN_TEXT"}}'
{
  "entities": [
    {
      "name": "Hello World",
      "type": "WORK_OF_ART",
      "metadata": {
        "mid": "/m/03lm3",
        "wikipedia_url": "https://en.wikipedia.org/wiki/\"Hello,_World!\"_program"
      },
      "salience": 1,
      "mentions": [
        {
          "text": {
            "content": "Hello World",
            "beginOffset": -1
          },
          "type": "PROPER"
        }
      ]
    }
  ],
  "language": "en"
}

images/api_usage.png

For more information, see

Cloud Endpoints also uses it to rate limit quota (Configuring quotas)

TODO

TODO

const language = require('@google-cloud/language');
const {GoogleAuth, grpc} = require('google-gax');
const apiKey = 'AIza-API_KEY';

function getApiKeyCredentials() {
  const sslCreds = grpc.credentials.createSsl();
  const googleAuth = new GoogleAuth();
  const authClient = googleAuth.fromAPIKey(apiKey);
  const credentials = grpc.credentials.combineChannelCredentials(
    sslCreds,
    grpc.credentials.createFromGoogleCredential(authClient)
  );
  return credentials;
}

async function main() {
  const sslCreds = getApiKeyCredentials();
  const client = new language.LanguageServiceClient({sslCreds});
  const text = 'Hello, world!';
  const document = {
    content: text,
    type: 'PLAIN_TEXT',
  };

  const [result] = await client.analyzeSentiment({document: document});
  const sentiment = result.documentSentiment;
  console.log(`Text: ${text}`);
  console.log(`Sentiment score: ${sentiment.score}`);
  console.log(`Sentiment magnitude: ${sentiment.magnitude}`);
}
main().catch(console.error);

from StackOverflow

X-Goog-Quota-User

This parameter is normally used with the API key to identity and control rate limits per caller.

It is not used in Google Cloud APIs.

X-Goog-Api-Client

The x-goog-api-client is intended for use by Google-written clients for metrics.

Users can submit UserAgent metadata in api calls. option.WithUserAgent

X-Goog-Request-Reason

Also See Request Annotation with Cloud Audit Logging and Monitoring on GCP

X-Goog-User-Project

Used to allow the caller to redirect quota and billing. Requires the caller to have the serviceusage.services.use permission on the target project

See GCP Quota and Cost Distribution between Projects

Use client_options.quota_project_id()

quota_project_id (Optional[str]) – A project name that a client’s quota belongs to.

TODO


Custom Headers

To set arbitrary Headers for REST (GCS) and GRPC services (pubsub), you’ll need special handling:

#!/usr/bin/python

# virtualenv env
# source env/bin/activate
# pip install google-cloud-pubsub google-cloud storage

import os
import time

import google.auth.credentials
import google.auth.jwt
import google.auth.transport.grpc
import grpc

import google.auth.transport.requests
from google.auth.transport.requests import AuthorizedSession


# from google.pubsub_v1.services.publisher import PublisherAsyncClient
# from google.pubsub_v1.services.publisher import PublisherClient
from google.cloud import pubsub_v1

from google.pubsub_v1.services.publisher import transports

# https://github.com/grpc/grpc/tree/master/examples/python/interceptors/headers
import header_manipulator_client_interceptor
import six


project_id = "projectID"
allow     = "foo"
header = 'x-goog-custom-header'

credentials, _ = google.auth.default()
request = google.auth.transport.requests.Request()

## Pubsub

header_adder_interceptor = header_manipulator_client_interceptor.header_adder_interceptor(header,deny)

channel = google.auth.transport.grpc.secure_authorized_channel(
    credentials=credentials,  request=request, target=pubsub_v1.PublisherClient.SERVICE_ADDRESS, 
    ssl_credentials=grpc.ssl_channel_credentials())

intercept_channel = grpc.intercept_channel(channel, header_adder_interceptor)
transport = transports.PublisherGrpcTransport(channel=intercept_channel)
publisher =pubsub_v1.PublisherClient(transport=transport)

topic_name = 'projects/{project_id}/topics/{topic}'.format(
    project_id=project_id,
    topic='topic1', 
)    

topic = publisher.topic_path(project_id, 'topic1')
data = b'The rain in Wales falls mainly on the snails.'

for n in range(1, 2):
    future = publisher.publish(topic, data)
    print(future.result())
print("Published messages.")

### GCS

authed_session = AuthorizedSession(credentials)
authed_session.headers[header] = allow
from google.cloud import storage
client = storage.Client(_http=authed_session)
buckets = client.list_buckets()
for bkt in buckets:
  print(bkt)
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"

	"google.golang.org/api/option"

	pubsub "cloud.google.com/go/pubsub"
	"cloud.google.com/go/storage"
	"golang.org/x/net/context"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/iterator"
)


const (
	projectID  = "projectid"
	bucketName = "golang-test-bucket"
	fileName   = "foo.txt"
	allow       = "foo"
)

type headerTransportLayer struct {
	http.Header
	baseTransit http.RoundTripper
}

func newTransportWithHeaders(baseTransit http.RoundTripper, key, value string) headerTransportLayer {
	if baseTransit == nil {
		baseTransit = http.DefaultTransport
	}
	headers := make(http.Header)
	headers.Set(key, value)
	return headerTransportLayer{Header: headers, baseTransit: baseTransit}
}

func (h headerTransportLayer) RoundTrip(req *http.Request) (*http.Response, error) {
	for key, value := range h.Header {
		// only set headers that are not previously defined
		if _, ok := req.Header[key]; !ok {
			req.Header[key] = value
		}
	}
	return h.baseTransit.RoundTrip(req)
}
func main() {

	ctx := context.Background()

	// for pubsub grpc
	client, err := pubsub.NewClient(ctx, projectID)
	if err != nil {
		fmt.Printf("Could not create pubsub Client: %v", err)
		return
	}
	defer client.Close()

	ctx = metadata.AppendToOutgoingContext(ctx, "x-goog-custom-header", deny)

	t := client.Topic("testing")
	ok, err := t.Exists(ctx)
	if err != nil {
		fmt.Printf("Could not check pubsub tpic: %v\b", err)
		return
	}
	fmt.Printf("Topic Exists %v\n", ok)
	r := t.Publish(ctx, &pubsub.Message{
		Data: []byte("foo"),
	})

	mid, err := r.Get(ctx)
	if err != nil {
		fmt.Printf("Could not create pubsub Client: %v", err)
		return
	}
	fmt.Printf("Published %s\n", mid)

	// for storage http
	hc, err := google.DefaultClient(ctx)
	if err != nil {
		fmt.Printf("%v", err)
		return
	}
	hc.Transport = newTransportWithHeaders(hc.Transport, "x-goog-custom-header", allow)

	storageClient, err := storage.NewClient(ctx, option.WithHTTPClient(hc))
	if err != nil {
		fmt.Printf("Unable to acquire storage Client: %v\n", err)
		return
	}
package com.test;

import java.util.Map;

import com.google.api.gax.core.FixedCredentialsProvider;
import com.google.api.gax.rpc.FixedHeaderProvider;
import com.google.api.gax.rpc.HeaderProvider;
import com.google.api.gax.rpc.TransportChannelProvider;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.pubsub.v1.TopicAdminClient;
import com.google.cloud.pubsub.v1.TopicAdminClient.ListTopicsPagedResponse;
import com.google.cloud.pubsub.v1.TopicAdminSettings;
import com.google.cloud.storage.Blob;
import com.google.cloud.storage.Bucket;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
import com.google.common.collect.ImmutableMap;
import com.google.pubsub.v1.ListTopicsRequest;
import com.google.pubsub.v1.ProjectName;
import com.google.pubsub.v1.Topic;
public class TestApp {
	public static void main(String[] args) {
		TestApp tc = new TestApp();
	}

	public TestApp() {
		try {

			String project_id = "projectid";
			String bucketName = "golang-test-bucket";
			String objectName = "foo.txt";
			String allow = "foo";
			String header = "x-goog-custom-header";

			GoogleCredentials sourceCredentials = GoogleCredentials.getApplicationDefault();

			Map<String, String> mergedHeaders = ImmutableMap.<String, String>builder()
					.put(header, deny)
					.build();
			HeaderProvider headerProvider = FixedHeaderProvider.create(mergedHeaders);

			Storage storage_service = StorageOptions.newBuilder().setCredentials(sourceCredentials)
					.setHeaderProvider(headerProvider).build().getService();

			Blob blob =	storage_service.get(bucketName, objectName, Storage.BlobGetOption.fields(Storage.BlobField.values()));
			
			System.out.println("Blob bucket " + blob.getBucket());

			for (Bucket b : storage_service.list().iterateAll()) {
				System.out.println(b);
			}

			FixedCredentialsProvider credentialsProvider = FixedCredentialsProvider.create(sourceCredentials);

			TransportChannelProvider channelProvider = TopicAdminSettings.defaultTransportChannelProvider();

			TopicAdminClient topicClient = TopicAdminClient.create(TopicAdminSettings.newBuilder()
					.setTransportChannelProvider(channelProvider).setHeaderProvider(headerProvider)
					.setCredentialsProvider(credentialsProvider).build());

			ListTopicsRequest listTopicsRequest = ListTopicsRequest.newBuilder()
					.setProject(ProjectName.format(project_id)).build();
			ListTopicsPagedResponse response = topicClient.listTopics(listTopicsRequest);
			Iterable<Topic> topics = response.iterateAll();
			for (Topic topic : topics)
				System.out.println(topic);

		} catch (Exception ex) {
			System.out.println("Error:  " + ex);
		}
	}

}

const PubSub = require('@google-cloud/pubsub');
const Storage = require('@google-cloud/storage');
var GoogleAuth = require('google-auth-library');

var CallOptions = require('google-gax');

var project_id = "projectid";
var bucketName = "grpc-test-bucket";
var objectName = "foo.txt";
var allow = "foo";
var header = "x-goog-custom-header";


async function main() {

  const pubSubClient = new PubSub(
    {
      projectId: project_id,
    }
  );

  async function listAllTopics() {

    options = {
      autoPaginate: false,
      gaxOpts: {
        otherArgs: {
          headers: {
            "x-goog-custom-header": deny
          }
        }
     }
    }

    const [topics] = await pubSubClient.getTopics(options);
    console.log('Topics:');
    topics.forEach(topic => console.log(topic.name));
  }

  listAllTopics().catch(console.error);

  const storage = new Storage();

  storage.interceptors.push({
    request: reqOpts => {
      reqOpts.headers =  {
        "x-goog-custom-header" : deny
      };
      return reqOpts
    }
  })

  storage.getBuckets(function (err, buckets) {
    if (err) {
      console.log(err);
    }
    if (!err) {
      buckets.forEach(function (value) {
        console.log(value.id);
      });
    }
  });

  const contents = await storage.bucket(bucketName).file(objectName).download();

  console.log(contents);
}

main().catch(console.error);

This site supports webmentions. Send me a mention via this form.