Create a downscoped short-lived credential

This page explains how to use Credential Access Boundaries to create an OAuth 2.0 access token with downscoped Cloud Storage permissions.

The process for creating a token with downscoped permissions includes the following steps:

  1. Grant the appropriate IAM roles to a user or service account.
  2. Define a Credential Access Boundary that sets an upper bound on the permissions that are available to the user or service account.
  3. Create an OAuth 2.0 access token for the user or service account.
  4. Exchange the OAuth 2.0 access token for a new token that respects the Credential Access Boundary.

You can then use the new downscoped OAuth 2.0 access token to authenticate requests to Cloud Storage.

Before you begin

Before you use Credential Access Boundaries, make sure you meet the following requirements:

  • You need to downscope permissions only for Cloud Storage, not for other Google Cloud services.

    If you need to downscope permissions for additional Google Cloud services, you can create multiple service accounts and grant a different set of roles to each service account.

  • You can use OAuth 2.0 access tokens for authentication. Other types of short-lived credentials don't support Credential Access Boundaries.

Also, you must enable the required APIs:

  • Enable the IAM and Security Token Service APIs.

    Roles required to enable APIs

    To enable APIs, you need the serviceusage.services.enable permission. If you created the project, then you likely already have this permission through the Owner role (roles/owner). Otherwise, you can get this permission through the Service Usage Admin role (roles/serviceusage.serviceUsageAdmin). Learn how to grant roles.

    Enable the APIs

Grant IAM roles

A Credential Access Boundary sets an upper bound on the available permissions for a resource. It can subtract permissions from a principal, but it can't add permissions that the principal does not already have.

As a result, you must also grant roles to the principal that provide the permissions they need, either on a Cloud Storage bucket or on a higher-level resource, such as the project.

For example, suppose you need to create a downscoped short-lived credential that allows a service account to create objects in a bucket:

  • At a minimum, you must grant a role to the service account that includes the storage.objects.create permission, such as the Storage Object Creator role (roles/storage.objectCreator). The Credential Access Boundary must also include this permission.
  • You can also grant a role that includes more permissions, such as the Storage Object Admin role (roles/storage.objectAdmin). The service account can use only the permissions that appear in both the role grant and the Credential Access Boundary.

To learn about predefined roles for Cloud Storage, see Cloud Storage roles.

Define the Credential Access Boundary

A Credential Access Boundary is an object that contains a list of access boundary rules. The rules are made up of parameters that specify the upper bound of the permissions that are available to the user or service account. To define a Credential Access Boundary, create a JSON object that lists the access boundary rules and their parameters.

The following is an example of a Credential Access Boundary:

{
  "accessBoundary": {
    "accessBoundaryRules": [
      {
        "availablePermissions": [
          "inRole:ROLE_ID"
        ],
        "availableResource": "//storage.googleapis.com/projects/_/buckets/BUCKET_NAME"
         "availabilityCondition": {
            "expression" : "CONDITION"
      }
    ]
  }
}

Replace the following:

  • ROLE_ID: The ID of a predefined or custom role that defines the upper bound on the available permissions for the resource. For example, roles/storage.objectViewer. To specify multiple roles, add a new line with an inRole: prefix followed by the role ID. Only the permissions in the specified roles will be available.
  • BUCKET_NAME: The name of the Cloud Storage bucket that the rule applies to.
  • CONDITION: Optional. A condition expression that specifies the Cloud Storage objects where permissions are available. For example, the following condition makes permissions available for objects whose name starts with customer-a:

    resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a')
    

To learn more about how to create and customize credential access boundaries, see Components of a Credential Access Boundary.

For examples of potential use cases for Credential Access Boundaries, see Examples of Credential Access Boundaries.

Create an OAuth 2.0 access token

Before you create a downscoped short-lived credential, you must create a normal OAuth 2.0 access token. You can then exchange the normal credential for a downscoped credential. When you create the access token, use the OAuth 2.0 scope https://www.googleapis.com/auth/cloud-platform.

To create an access token for a service account, you can complete the server-to-server OAuth 2.0 flow, or you can use the Service Account Credentials API to generate an OAuth 2.0 access token.

To create an access token for a user, see Obtaining OAuth 2.0 access tokens. You can also use the OAuth 2.0 Playground to create an access token for your own Google Account.

Exchange the OAuth 2.0 access token

After you create an OAuth 2.0 access token, you can exchange the access token for a downscoped token that respects the Credential Access Boundary. This process typically involves a token broker and a token consumer:

  • The token broker is responsible for defining the Credential Access Boundary and exchanging an access token for a downscoped token.

    The token broker can use a supported authentication library to exchange access tokens automatically, or it can call the Security Token Service to exchange tokens manually.

  • The token consumer requests a downscoped access token from the token broker, then uses the downscoped access token to perform another action.

    The token consumer can use a supported authentication library to automatically refresh access tokens before they expire. Alternatively, it can refresh tokens manually, or it can allow tokens to expire without refreshing them.

There are two ways to exchange the access token for a downscoped token:

  • Client-side token exchange (recommended): Clients obtain cryptographic materials from the Security Token Service API server. The cryptographic materials allow clients to generate downscoped tokens with varying Credential Access Boundary rules independently on the client side for a set duration (for example, 1 hour). This approach reduces latency and improves efficiency, especially for clients requiring frequent Credential Access Boundary rule updates. It's also more efficient for applications that need to generate many unique downscoped tokens. This is the recommended approach because it provides better performance, scalability, and future feature compatibility.

  • Server-side token exchange: Clients request a new downscoped token from the Security Token Service API server whenever a Credential Access Boundary rule changes. This approach is straightforward but requires a round trip to the Security Token Service API server for each downscoped token request. This approach is recommended only for customers who require a client library that doesn't support client-side token exchange, due to the round trip to the Security Token Service API for each downscoped token request.

Client-side token exchange

If you create the token broker and token consumer with the following language, you can use Google's authentication library to exchange and refresh tokens automatically through the client-side approach.

Java

For Java, you can exchange and refresh tokens automatically with version 1.32.1 or later of the com.google.auth:google-auth-library-cab-token-generator artifact.

To check your artifact version, run the following Maven command in your application directory:

mvn dependency:list -DincludeArtifactIds=google-auth-library-cab-token-generator

The following example shows how a token broker can generate downscoped tokens:

import com.google.auth.credentialaccessboundary.ClientSideCredentialAccessBoundaryFactory;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.CredentialAccessBoundary;
import com.google.auth.oauth2.GoogleCredentials;
import dev.cel.common.CelValidationException;
import java.io.IOException;
import java.security.GeneralSecurityException;
  public static AccessToken getTokenFromBroker(String bucketName, String objectPrefix)
      throws IOException {
    // Retrieve the source credentials from ADC.
    GoogleCredentials sourceCredentials =
        GoogleCredentials.getApplicationDefault()
            .createScoped("https://www.googleapis.com/auth/cloud-platform");

    // Initialize the Credential Access Boundary rules.
    String availableResource = "//storage.googleapis.com/projects/_/buckets/" + bucketName;

    // Downscoped credentials will have readonly access to the resource.
    String availablePermission = "inRole:roles/storage.objectViewer";

    // Only objects starting with the specified prefix string in the object name will be allowed
    // read access.
    String expression =
        "resource.name.startsWith('projects/_/buckets/"
            + bucketName
            + "/objects/"
            + objectPrefix
            + "')";

    // Build the AvailabilityCondition.
    CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition availabilityCondition =
        CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition.newBuilder()
            .setExpression(expression)
            .build();

    // Define the single access boundary rule using the above properties.
    CredentialAccessBoundary.AccessBoundaryRule rule =
        CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
            .setAvailableResource(availableResource)
            .addAvailablePermission(availablePermission)
            .setAvailabilityCondition(availabilityCondition)
            .build();

    // Define the Credential Access Boundary with all the relevant rules.
    CredentialAccessBoundary credentialAccessBoundary =
        CredentialAccessBoundary.newBuilder().addRule(rule).build();

    // Create an instance of ClientSideCredentialAccessBoundaryFactory.
    ClientSideCredentialAccessBoundaryFactory factory =
        ClientSideCredentialAccessBoundaryFactory.newBuilder()
            .setSourceCredential(sourceCredentials)
            .build();

    // Generate the token and pass it to the Token Consumer.
    try {
      return factory.generateToken(credentialAccessBoundary);
    } catch (GeneralSecurityException | CelValidationException e) {
      throw new IOException("Error generating downscoped token", e);
    }
  }

The following example shows how a token consumer can use a refresh handler to automatically obtain and refresh downscoped tokens:

import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.OAuth2CredentialsWithRefresh;
import com.google.cloud.storage.Blob;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
import java.io.IOException;
  public static String retrieveBlobWithDownscopedToken(
      final String bucketName, final String objectName) throws IOException {
    // You can pass an `OAuth2RefreshHandler` to `OAuth2CredentialsWithRefresh` which will allow the
    // library to seamlessly handle downscoped token refreshes on expiration.
    OAuth2CredentialsWithRefresh.OAuth2RefreshHandler handler =
        new OAuth2CredentialsWithRefresh.OAuth2RefreshHandler() {
          @Override
          public AccessToken refreshAccessToken() throws IOException {
            // The common pattern of usage is to have a token broker pass the downscoped short-lived
            // access tokens to a token consumer via some secure authenticated channel.
            // For illustration purposes, we are generating the downscoped token locally.
            // We want to test the ability to limit access to objects with a certain prefix string
            // in the resource bucket. objectName.substring(0, 3) is the prefix here. This field is
            // not required if access to all bucket resources are allowed. If access to limited
            // resources in the bucket is needed, this mechanism can be used.
            return DownscopedAccessTokenGenerator
                .getTokenFromBroker(bucketName, objectName);
          }
        };

    AccessToken downscopedToken = handler.refreshAccessToken();

    OAuth2CredentialsWithRefresh credentials =
        OAuth2CredentialsWithRefresh.newBuilder()
            .setAccessToken(downscopedToken)
            .setRefreshHandler(handler)
            .build();

    StorageOptions options = StorageOptions.newBuilder().setCredentials(credentials).build();
    Storage storage = options.getService();

    Blob blob = storage.get(bucketName, objectName);
    if (blob == null) {
      return null;
    }
    return new String(blob.getContent());
  }

Server-side token exchange

This section describes the following methods that you can use to exchange tokens through the service side approach:

Exchange and refresh the access token automatically using server-side approach

If you create the token broker and token consumer with one of the following languages, you can use Google's authentication library to exchange and refresh tokens automatically using the server-side token generation approach:

Go

For Go, you can exchange and refresh tokens automatically with version v0.0.0-20210819190943-2bc19b11175f or later of the golang.org/x/oauth2 package.

To check your package version, run the following command in your application directory:

go list -m golang.org/x/oauth2

The following example shows how a token broker can generate downscoped tokens:


import (
	"context"
	"fmt"

	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"golang.org/x/oauth2/google/downscope"
)

// createDownscopedToken would be run on the token broker in order to generate
// a downscoped access token that only grants access to objects whose name begins with prefix.
// The token broker would then pass the newly created token to the requesting token consumer for use.
func createDownscopedToken(bucketName string, prefix string) error {
	// bucketName := "foo"
	// prefix := "profile-picture-"

	ctx := context.Background()
	// A condition can optionally be provided to further restrict access permissions.
	condition := downscope.AvailabilityCondition{
		Expression:  "resource.name.startsWith('projects/_/buckets/" + bucketName + "/objects/" + prefix + "')",
		Title:       prefix + " Only",
		Description: "Restricts a token to only be able to access objects that start with `" + prefix + "`",
	}
	// Initializes an accessBoundary with one Rule which restricts the downscoped
	// token to only be able to access the bucket "bucketName" and only grants it the
	// permission "storage.objectViewer".
	accessBoundary := []downscope.AccessBoundaryRule{
		{
			AvailableResource:    "//storage.googleapis.com/projects/_/buckets/" + bucketName,
			AvailablePermissions: []string{"inRole:roles/storage.objectViewer"},
			Condition:            &condition, // Optional
		},
	}

	// This Source can be initialized in multiple ways; the following example uses
	// Application Default Credentials.
	var rootSource oauth2.TokenSource

	// You must provide the "https://www.googleapis.com/auth/cloud-platform" scope.
	rootSource, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform")
	if err !=