Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,21 @@ Once you have an API key replace it in the main function in ApiKeyAuthExample an

The same configuration above applies.

To run the samples for [Downscoping with Credential Access Boundaries](https://cloud.google.com/iam/docs/downscoping-short-lived-credentials)
you must provide both a bucket name and object name under the TODO(developer): in the main method of `DownscopingExample`.
This section provides examples for [Downscoping with Credential Access Boundaries](https://cloud.google.com/iam/docs/downscoping-short-lived-credentials).
There are two examples demonstrating different ways to implement downscoping.

**`DownscopedAccessTokenGenerator` and `DownscopedAccessTokenConsumer` Examples:**

These examples demonstrate a common pattern for downscoping, using a token broker and consumer.
The `DownscopedAccessTokenGenerator` generates the downscoped access token using a client-side approach, and the `DownscopedAccessTokenConsumer` uses it to access Cloud Storage resources.
To run the `DownscopedAccessTokenConsumer`, you must provide a bucket name and object name under the `TODO(developer):` in the `main` method.
You can then run `DownscopedAccessTokenConsumer` via:

mvn exec:java -Dexec.mainClass=com.google.cloud.auth.samples.DownscopedAccessTokenConsumer

**`DownscopingExample` Example:**

This example demonstrates downscoping using a server-side approach. To run this example you must provide both a bucket name and object name under the TODO(developer): in the main method of `DownscopingExample`.

You can then run `DownscopingExample` via:

Expand Down
4 changes: 4 additions & 0 deletions auth/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ limitations under the License.
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
</dependency>
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-cab-token-generator</artifactId>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-apikeys</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.auth.samples;

// [START auth_client_cab_consumer]
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;
// [END auth_client_cab_consumer]


/**
* Demonstrates retrieving a Cloud Storage blob using a downscoped. This example showcases the
* consumer side of the downscoping process. It retrieves a blob's content using credentials that
* have limited access based on a pre-defined Credential Access Boundary.
*/
public class DownscopedAccessTokenConsumer {

public static void main(String[] args) throws IOException {
// TODO(developer): Replace these variables before running the sample.
// The Cloud Storage bucket name.
String bucketName = "your-gcs-bucket-name";
// The Cloud Storage object name that resides in the specified bucket.
String objectName = "your-gcs-object-name";

retrieveBlobWithDownscopedToken(bucketName, objectName);
}

/**
* Simulates token consumer readonly access to the specified object.
*
* @param bucketName The name of the Cloud Storage bucket containing the blob.
* @param objectName The name of the Cloud Storage object (blob).
* @return The content of the blob as a String, or {@code null} if the blob does not exist.
* @throws IOException If an error occurs during communication with Cloud Storage or token
* retrieval. This can include issues with authentication, authorization, or network
* connectivity.
*/
// [START auth_client_cab_consumer]
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.
Comment on lines +65 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: for further notice, we discourage placing lengthy explanations in the code. you should collaborate with tech writer to have these recommendations placed in the documentation that demonstrates the code snippet instead.

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());
}
// [END auth_client_cab_consumer]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.auth.samples;

// [START auth_client_cab_token_broker]
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;
// [END auth_client_cab_token_broker]

/**
* Demonstrates how to use ClientSideCredentialAccessBoundaryFactory to generate downscoped tokens.
*/
public class DownscopedAccessTokenGenerator {

/**
* Simulates a token broker generating downscoped tokens for specific objects in a bucket.
*
* @param bucketName The name of the Cloud Storage bucket.
* @param objectPrefix Prefix of the object name for downscoped token access.
* @return An AccessToken representing the downscoped token.
* @throws IOException If an error occurs during token generation.
*/
// [START auth_client_cab_token_broker]
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);
}
}
// [END auth_client_cab_token_broker]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.auth.samples;

import static com.google.cloud.auth.samples.DownscopedAccessTokenConsumer.retrieveBlobWithDownscopedToken;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import com.google.cloud.storage.Blob;
import com.google.cloud.storage.BlobId;
import com.google.cloud.storage.BlobInfo;
import com.google.cloud.storage.Bucket;
import com.google.cloud.storage.BucketInfo;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
// CHECKSTYLE OFF: AbbreviationAsWordInName
public class DownscopedAccessTokenIT {
// CHECKSTYLE ON: AbbreviationAsWordInName
private static final String CONTENT = "CONTENT";
private Bucket bucket;
private Blob blob;

@Before
public void setUp() {
String credentials = System.getenv("GOOGLE_APPLICATION_CREDENTIALS");
assertNotNull(credentials);

// Create a bucket and object that are deleted once the test completes.
Storage storage = StorageOptions.newBuilder().build().getService();

String suffix = UUID.randomUUID().toString().substring(0, 18);
String bucketName = String.format("bucket-client-side-cab-test-%s", suffix);
bucket = storage.create(BucketInfo.newBuilder(bucketName).build());

String objectName = String.format("blob-client-side-cab-test-%s", suffix);
BlobId blobId = BlobId.of(bucketName, objectName);
BlobInfo blobInfo = Blob.newBuilder(blobId).build();
blob = storage.create(blobInfo, CONTENT.getBytes(StandardCharsets.UTF_8));
}

@After
public void cleanup() {
if (blob != null) {
blob.delete();
}
if (bucket != null) {
bucket.delete();
}
}

@Test
public void testDownscopedAccessToken() throws IOException {
String content = retrieveBlobWithDownscopedToken(bucket.getName(), blob.getName());
assertEquals(CONTENT, content);
}
}