Authenticating service-to-service

If your architecture is using multiple services, these services likely need to communicate with each other, using either asynchronous or synchronous means. Many of these services may be private and require credentials for access.

For asynchronous communication, you can use the following Google Cloud services:

  • Cloud Tasks for one to one asynchronous communication
  • Pub/Sub for one to many asynchronous communication
  • Cloud Scheduler for regularly scheduled asynchronous communication.

In all of these cases, the service used manages the interaction with the receiving service, based on the configuration you set up.

But for synchronous communication, your service calls another service directly, over HTTP, using its endpoint URL. For this use case, you should make sure that each service is only able to make requests to specific services. For example, if you have a login service, it should be able to access the user-profiles service, but not the search service.

In this situation, Google recommends that you use IAM and a service identity based on a per-service user-managed service account that has been granted the minimum set of permissions required to do its work.

In addition, the request must present proof of the calling service's identity. To do this, configure your calling service to add a Google-signed OpenID Connect ID token as part of the request.

Set up the service account

To set up a service account, you configure the receiving service to accept requests from the calling service by making the calling service's service account a member on the receiving service. Then you grant that service account the Cloud Run Invoker (roles/run.invoker) role. To do both of these tasks, follow the instuctions in the appropriate tab:

Console UI

  1. Go to the Google Cloud Console:

    Go to Google Cloud Console

  2. Select the receiving service.

  3. Click Show Info Panel in the top right corner to show the Permissions tab.

  4. In the Add members field, enter the identity of the calling service. This is usually an email address, by default PROJECT_NUMBER-compute@developer.gserviceaccount.com.

  5. Select the Cloud Run Invoker role from the Select a role drop-down menu.

  6. Click Add.

gcloud

Use the gcloud run services add-iam-policy-binding command:

gcloud run services add-iam-policy-binding RECEIVING_SERVICE \
  --member='serviceAccount:CALLING_SERVICE_IDENTITY' \
  --role='roles/run.invoker'

where RECEIVING_SERVICE is the name of the receiving service, and CALLING_SERVICE_IDENTITY is the email address of the service account, by default PROJECT_NUMBER-compute@developer.gserviceaccount.com.

Acquire and configure the ID token

Once the calling service account has been granted the proper role, you need to:

  1. Fetch a Google-signed ID token with the audience claim (aud) set to the URL of the receiving service.

  2. Include the ID token in an Authorization: Bearer ID_TOKEN header in the request to the receiving service.

The easiest and most reliable way to manage this process is to use the authentication libraries, as shown below, to generate and use this token. This code works in any environment - even outside of Google Cloud - where the libraries can obtain authentication credentials, including environments that support local Application Default Credentials.

Node.js

/**
 * TODO(developer): Uncomment these variables before running the sample.
 */
// const url = 'https://TARGET_URL';
// let targetAudience = null;
const {GoogleAuth} = require('google-auth-library');
const auth = new GoogleAuth();

async function request() {
  if (!targetAudience) {
    // Use the request URL hostname as the target audience for requests.
    const {URL} = require('url');
    targetAudience = new URL(url);
  }
  console.info(`request ${url} with target audience ${targetAudience}`);
  const client = await auth.getIdTokenClient(targetAudience);
  const res = await client.request({url});
  console.info(res.data);
}

request().catch(err => {
  console.error(err.message);
  process.exitCode = 1;
});

Python

import urllib

import google.auth.transport.requests
import google.oauth2.id_token


def make_authorized_get_request(service_url):
    """
    make_authorized_get_request makes a GET request to the specified HTTP endpoint
    in service_url (must be a complete URL) by authenticating with the
    ID token obtained from the google-auth client library.
    """

    req = urllib.request.Request(service_url)

    auth_req = google.auth.transport.requests.Request()
    id_token = google.oauth2.id_token.fetch_id_token(auth_req, service_url)

    req.add_header("Authorization", f"Bearer {id_token}")
    response = urllib.request.urlopen(req)

    return response.read()

Go


import (
	"context"
	"fmt"
	"io"

	"google.golang.org/api/idtoken"
)

// makeGetRequest makes a request to the provided targetURL with an authenticated client.
func makeGetRequest(w io.Writer, targetURL string) error {
	// functionURL := "https://TARGET_URL"
	ctx := context.Background()

	// client is a http.Client that automatically adds an "Authorization" header
	// to any requests made.
	client, err := idtoken.NewClient(ctx, targetURL)
	if err != nil {
		return fmt.Errorf("idtoken.NewClient: %v", err)
	}

	resp, err := client.Get(targetURL)
	if err != nil {
		return fmt.Errorf("client.Get: %v", err)
	}
	defer resp.Body.Close()
	if _, err := io.Copy(w, resp.Body); err != nil {
		return fmt.Errorf("io.Copy: %v", err)
	}

	return nil
}

Java

import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.IdTokenCredentials;
import com.google.auth.oauth2.IdTokenProvider;
import java.io.IOException;

public class Authentication {

  // makeGetRequest makes a GET request to the specified Cloud Run or
  // Cloud Functions endpoint, serviceUrl (must be a complete URL), by
  // authenticating with an Id token retrieved from Application Default Credentials.
  public static HttpResponse makeGetRequest(String serviceUrl) throws IOException {
    GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
    if (!(credentials instanceof IdTokenProvider)) {
      throw new IllegalArgumentException("Credentials are not an instance of IdTokenProvider.");
    }
    IdTokenCredentials tokenCredential =
        IdTokenCredentials.newBuilder()
            .setIdTokenProvider((IdTokenProvider) credentials)
            .setTargetAudience(serviceUrl)
            .build();

    GenericUrl genericUrl = new GenericUrl(serviceUrl);
    HttpCredentialsAdapter adapter = new HttpCredentialsAdapter(tokenCredential);
    HttpTransport transport = new NetHttpTransport();
    HttpRequest request = transport.createRequestFactory(adapter).buildGetRequest(genericUrl);
    return request.execute();
  }
}

Use the metadata server

If for some reason you cannot use the authentication libraries, you can fetch an ID token from the Compute metadata server while your container is running on Cloud Run. Note that this method does not work outside of Google Cloud, including from your local machine.

curl "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=[AUDIENCE]" \
     -H "Metadata-Flavor: Google"

Where AUDIENCE is the URL of the service you are invoking.

For an end-to-end walkthrough of an application using this service-to-service authentication technique, follow the securing Cloud Run services tutorial.

Calling from outside GCP

The best way to call a private service from outside Google Cloud is to use the authentication libraries as described above, having set up Application Default Credentials.

You can acquire a Google-signed ID token using a self-signed JWT, but this is quite complicated and potentially error-prone. The basic steps are:

  1. Self-sign a service account JWT with the target_audience claim set to the URL of the receiving service.

  2. Exchange the self-signed JWT for a Google-signed ID token, which should have the aud claim set to the above URL.

  3. Include the ID token in an Authorization: Bearer ID_TOKEN header in the request to the service.

You can examine this Cloud Functions example for a sample of the above steps.