Skip to content

Commit f481123

Browse files
nbayatihuangjiahuaaeitzman
authored
feat: Introduce Client-Side Credential Access Boundary (CAB) functionality (#1629)
* feat: Implement ClientSideCredentialAccessBoundaryFactory (#1562) * feat: Implement ClientSideCredentialAccessBoundaryFactory.refreshCredentials() Set up the ClientSideCredentialAccessBoundaryFactory class and module. Implement the function to fetch and refresh intermediary tokens from STS. * feat: Add the generated ClientSideAccessBoundaryProto class for Client-Side CAB feature. (#1571) Change-Id: Ic7ef3cbd80b2ad778d61b9ccabf780561d3cc709 * feat: Implement refreshCredentialsIfRequired for intermediate token r… (#1583) * feat: Implement refreshCredentialsIfRequired for intermediate token refresh Implement `refreshCredentialsIfRequired`, called by `generateToken()`, to handle token refresh. It uses `refreshMargin` and `minimumTokenLifetime` to decide on synchronous or asynchronous refresh * Add unit tests for the builder and refreshCredentials() * Improve concurrency handling during credential refresh. Introduced a refresh task to manage concurrent refresh requests, preventing redundant attempts and potential race conditions. This aligns the refresh mechanism with the pattern used in OAuth2Credentials and ensures more robust credential management. * Update existing unit tests for compatibility and readability. * Add unit tests for refreshCredentialsIfRequired. * Fix a merge issue. * Temporary add sonatype-snapshots repository and cel version to fix the build error. * Remove duplicated code. * Fix lint issue. * Fix: Propagate credential refresh exceptions in blocking refresh. * Change cel version * Change cel version * Add jsr305 dependency * Fix Javadoc error * Minor code readability enhancements. * Revert "Fix Javadoc error" This reverts commit 2157fdb. * Address comments (add javadoc and use assertThrows in tests) * Run format script * feat: Implement Client-Side CAB token generation. (#1598) * feat: Implement Client-Side CAB token generation. Change-Id: I2c217656584cf5805297f02340cbbabca471f609 * Use IllegalStateException(String, Throwable) to capture upstream exception during Tink initialization Change-Id: I12af5b84eae4dcec5865adfdad1f9396d54c0200 * Rethrow exceptions from tink and CEL Change-Id: If8c94c786ee39201029d9c27856fd2eafb61e51c * Add tests for invalid keys from upstream, and rename test cases. Change-Id: Ib41cb81c779534fc6efd74d66bf4728efd743906 * Add additional throws comment for generatToken method. Change-Id: I9cfc589ade8a91040fc9c447740493fd49e392af * Refactor tests for better readability. Change-Id: Icfd0bc24c1694f220bcbffc6cde41462c59119c4 * Catch and rethrow the exception of session key not being base64 encoded. Change-Id: I5fa0c25fe020e9612735e4ac5df2b85a2a5aab11 * Format the code using mvn com.coveo:fmt-maven-plugin:format. Change-Id: I46572488dcd28de450a6b1b2f732bee5baa86910 * Fix a typo in the javadoc comment. Change-Id: Icef9ef5f7c3567224ec507303543b78e61f43ec1 * chore: Update version tag in cab-token-generator pom.xml This commit updates the version tag in the pom.xml file. * feat: Add integration test for the client side cab * Remove volatile keyword and use refreshLock when reading intermediateCredentials. * Define new default values for refreshMargin and minimumTokenLifetime. * Update version in pom.xml * Run formatter to resolve lint errors * add missing dependency * Swap the assertEquals parameters so the expected value is first. * Docs: Added javadocs Improvements: Cleaned up code, resolved readability enhancements --------- Co-authored-by: Jiahua Huang <[email protected]> Co-authored-by: aeitzman <[email protected]>
1 parent cd1d160 commit f481123

File tree

15 files changed

+4427
-38
lines changed

15 files changed

+4427
-38
lines changed

cab-token-generator/java/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactory.java

Lines changed: 767 additions & 0 deletions
Large diffs are not rendered by default.

cab-token-generator/java/com/google/auth/credentialaccessboundary/protobuf/ClientSideAccessBoundaryProto.java

Lines changed: 2174 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cab-token-generator/javatests/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactoryTest.java

Lines changed: 989 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* Copyright 2025, Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
*
15+
* * Neither the name of Google LLC nor the names of its
16+
* contributors may be used to endorse or promote products derived from
17+
* this software without specific prior written permission.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
*/
31+
32+
package com.google.auth.credentialaccessboundary;
33+
34+
import static org.junit.Assert.assertEquals;
35+
import static org.junit.Assert.assertThrows;
36+
import static org.junit.Assert.assertTrue;
37+
38+
import com.google.api.client.http.GenericUrl;
39+
import com.google.api.client.http.HttpRequest;
40+
import com.google.api.client.http.HttpRequestFactory;
41+
import com.google.api.client.http.HttpResponse;
42+
import com.google.api.client.http.HttpResponseException;
43+
import com.google.api.client.http.javanet.NetHttpTransport;
44+
import com.google.api.client.json.JsonObjectParser;
45+
import com.google.api.client.json.gson.GsonFactory;
46+
import com.google.auth.Credentials;
47+
import com.google.auth.http.HttpCredentialsAdapter;
48+
import com.google.auth.oauth2.CredentialAccessBoundary;
49+
import com.google.auth.oauth2.GoogleCredentials;
50+
import com.google.auth.oauth2.OAuth2CredentialsWithRefresh;
51+
import com.google.auth.oauth2.ServiceAccountCredentials;
52+
import dev.cel.common.CelValidationException;
53+
import java.io.IOException;
54+
import java.security.GeneralSecurityException;
55+
import org.junit.Test;
56+
57+
/**
58+
* Integration tests for {@link ClientSideCredentialAccessBoundaryFactory}. *
59+
*
60+
* <p>The only requirements for this test suite to run is to set the environment variable
61+
* GOOGLE_APPLICATION_CREDENTIALS to point to the same service account configured in the setup
62+
* script (downscoping-with-cab-setup.sh).
63+
*/
64+
public final class ITClientSideCredentialAccessBoundaryTest {
65+
66+
// Output copied from the setup script (downscoping-with-cab-setup.sh).
67+
private static final String GCS_BUCKET_NAME = "cab-int-bucket-cbi3qrv5";
68+
private static final String GCS_OBJECT_NAME_WITH_PERMISSION = "cab-first-cbi3qrv5.txt";
69+
private static final String GCS_OBJECT_NAME_WITHOUT_PERMISSION = "cab-second-cbi3qrv5.txt";
70+
71+
// This Credential Access Boundary enables the objectViewer permission to the specified object in
72+
// the specified bucket.
73+
private static final CredentialAccessBoundary CREDENTIAL_ACCESS_BOUNDARY =
74+
CredentialAccessBoundary.newBuilder()
75+
.addRule(
76+
CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
77+
.setAvailableResource(
78+
String.format(
79+
"//storage.googleapis.com/projects/_/buckets/%s", GCS_BUCKET_NAME))
80+
.addAvailablePermission("inRole:roles/storage.objectViewer")
81+
.setAvailabilityCondition(
82+
CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition.newBuilder()
83+
.setExpression(
84+
String.format(
85+
"resource.name.startsWith('projects/_/buckets/%s/objects/%s')",
86+
GCS_BUCKET_NAME, GCS_OBJECT_NAME_WITH_PERMISSION))
87+
.build())
88+
.build())
89+
.build();
90+
91+
/**
92+
* A downscoped credential is obtained using ClientSideCredentialAccessBoundaryFactory with
93+
* permissions to access an object in the GCS bucket configured. We should only have access to
94+
* retrieve this object.
95+
*
96+
* <p>We confirm this by: 1. Validating that we can successfully retrieve this object with the
97+
* downscoped token. 2. Validating that we do not have permission to retrieve a different object
98+
* in the same bucket.
99+
*/
100+
@Test
101+
public void clientSideCredentialAccessBoundary_serviceAccountSource() throws IOException {
102+
OAuth2CredentialsWithRefresh.OAuth2RefreshHandler refreshHandler =
103+
() -> {
104+
ServiceAccountCredentials sourceCredentials =
105+
(ServiceAccountCredentials)
106+
GoogleCredentials.getApplicationDefault()
107+
.createScoped("https://www.googleapis.com/auth/cloud-platform");
108+
109+
ClientSideCredentialAccessBoundaryFactory factory =
110+
ClientSideCredentialAccessBoundaryFactory.newBuilder()
111+
.setSourceCredential(sourceCredentials)
112+
.build();
113+
114+
try {
115+
return factory.generateToken(CREDENTIAL_ACCESS_BOUNDARY);
116+
} catch (CelValidationException | GeneralSecurityException e) {
117+
throw new RuntimeException(e);
118+
}
119+
};
120+
121+
OAuth2CredentialsWithRefresh credentials =
122+
OAuth2CredentialsWithRefresh.newBuilder().setRefreshHandler(refreshHandler).build();
123+
124+
// Attempt to retrieve the object that the downscoped token has access to.
125+
retrieveObjectFromGcs(credentials, GCS_OBJECT_NAME_WITH_PERMISSION);
126+
127+
// Attempt to retrieve the object that the downscoped token does not have access to. This should
128+
// fail.
129+
HttpResponseException exception =
130+
assertThrows(
131+
HttpResponseException.class,
132+
() -> retrieveObjectFromGcs(credentials, GCS_OBJECT_NAME_WITHOUT_PERMISSION));
133+
assertEquals(403, exception.getStatusCode());
134+
}
135+
136+
private void retrieveObjectFromGcs(Credentials credentials, String objectName)
137+
throws IOException {
138+
String url =
139+
String.format(
140+
"https://storage.googleapis.com/storage/v1/b/%s/o/%s", GCS_BUCKET_NAME, objectName);
141+
142+
HttpCredentialsAdapter credentialsAdapter = new HttpCredentialsAdapter(credentials);
143+
HttpRequestFactory requestFactory =
144+
new NetHttpTransport().createRequestFactory(credentialsAdapter);
145+
HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(url));
146+
147+
JsonObjectParser parser = new JsonObjectParser(GsonFactory.getDefaultInstance());
148+
request.setParser(parser);
149+
150+
HttpResponse response = request.execute();
151+
assertTrue(response.isSuccessStatusCode());
152+
}
153+
}

cab-token-generator/pom.xml

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>com.google.auth</groupId>
8+
<artifactId>google-auth-library-parent</artifactId>
9+
<version>1.31.1-SNAPSHOT
10+
</version><!-- {x-version-update:google-auth-library-parent:current} -->
11+
</parent>
12+
13+
<artifactId>google-auth-library-cab-token-generator</artifactId>
14+
<name>Google Auth Library for Java - Cab Token Generator</name>
15+
16+
<build>
17+
<sourceDirectory>java</sourceDirectory>
18+
<testSourceDirectory>javatests</testSourceDirectory>
19+
</build>
20+
21+
<dependencies>
22+
<!-- Compile and Runtime Dependencies -->
23+
<dependency>
24+
<groupId>com.google.auth</groupId>
25+
<artifactId>google-auth-library-oauth2-http</artifactId>
26+
</dependency>
27+
<dependency>
28+
<groupId>com.google.auth</groupId>
29+
<artifactId>google-auth-library-credentials</artifactId>
30+
</dependency>
31+
<dependency>
32+
<groupId>com.google.http-client</groupId>
33+
<artifactId>google-http-client</artifactId>
34+
</dependency>
35+
<dependency>
36+
<groupId>com.google.errorprone</groupId>
37+
<artifactId>error_prone_annotations</artifactId>
38+
</dependency>
39+
<dependency>
40+
<groupId>com.google.guava</groupId>
41+
<artifactId>guava</artifactId>
42+
</dependency>
43+
<dependency>
44+
<groupId>com.google.protobuf</groupId>
45+
<artifactId>protobuf-java</artifactId>
46+
</dependency>
47+
<dependency>
48+
<groupId>dev.cel</groupId>
49+
<artifactId>cel</artifactId>
50+
</dependency>
51+
<dependency>
52+
<groupId>com.google.code.findbugs</groupId>
53+
<artifactId>jsr305</artifactId>
54+
</dependency>
55+
<dependency>
56+
<groupId>com.google.crypto.tink</groupId>
57+
<artifactId>tink</artifactId>
58+
</dependency>
59+
60+
<!-- Test Dependencies -->
61+
<dependency>
62+
<groupId>junit</groupId>
63+
<artifactId>junit</artifactId>
64+
<scope>test</scope>
65+
</dependency>
66+
<dependency>
67+
<groupId>com.google.auth</groupId>
68+
<artifactId>google-auth-library-oauth2-http</artifactId>
69+
<scope>test</scope>
70+
<type>test-jar</type>
71+
<classifier>testlib</classifier>
72+
</dependency>
73+
<dependency>
74+
<groupId>org.mockito</groupId>
75+
<artifactId>mockito-core</artifactId>
76+
<version>4.11.0</version>
77+
<scope>test</scope>
78+
</dependency>
79+
<dependency>
80+
<groupId>com.google.http-client</groupId>
81+
<artifactId>google-http-client-gson</artifactId>
82+
<scope>test</scope>
83+
</dependency>
84+
</dependencies>
85+
86+
</project>

oauth2_http/java/com/google/auth/oauth2/DownscopedCredentials.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
package com.google.auth.oauth2;
3333

34+
import static com.google.auth.oauth2.OAuth2Utils.TOKEN_EXCHANGE_URL_FORMAT;
3435
import static com.google.common.base.MoreObjects.firstNonNull;
3536
import static com.google.common.base.Preconditions.checkNotNull;
3637

@@ -44,6 +45,13 @@
4445
* DownscopedCredentials enables the ability to downscope, or restrict, the Identity and Access
4546
* Management (IAM) permissions that a short-lived credential can use for Cloud Storage.
4647
*
48+
* <p>This class provides a server-side approach for generating downscoped tokens, suitable for
49+
* situations where Credential Access Boundary rules change infrequently or a single downscoped
50+
* credential is reused many times. For scenarios where rules change frequently, or you need to
51+
* generate many unique downscoped tokens, the client-side approach using {@code
52+
* com.google.auth.credentialaccessboundary.ClientSideCredentialAccessBoundaryFactory} is more
53+
* efficient.
54+
*
4755
* <p>To downscope permissions you must define a {@link CredentialAccessBoundary} which specifies
4856
* the upper bound of permissions that the credential can access. You must also provide a source
4957
* credential which will be used to acquire the downscoped credential.
@@ -88,7 +96,6 @@
8896
*/
8997
public final class DownscopedCredentials extends OAuth2Credentials {
9098

91-
private final String TOKEN_EXCHANGE_URL_FORMAT = "https://sts.{universe_domain}/v1/token";
9299
private final GoogleCredentials sourceCredential;
93100
private final CredentialAccessBoundary credentialAccessBoundary;
94101
private final String universeDomain;
@@ -125,8 +132,7 @@ private DownscopedCredentials(Builder builder) {
125132
throw new IllegalStateException(
126133
"Error occurred when attempting to retrieve source credential universe domain.", e);
127134
}
128-
this.tokenExchangeEndpoint =
129-
TOKEN_EXCHANGE_URL_FORMAT.replace("{universe_domain}", universeDomain);
135+
this.tokenExchangeEndpoint = String.format(TOKEN_EXCHANGE_URL_FORMAT, universeDomain);
130136
}
131137

132138
@Override

oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,16 @@ protected static <T> T newInstance(String className) throws IOException, ClassNo
484484
}
485485
}
486486

487-
protected static <T> T getFromServiceLoader(Class<? extends T> clazz, T defaultInstance) {
487+
/**
488+
* Returns the first service provider from the given service loader.
489+
*
490+
* @param clazz The class of the service provider to load.
491+
* @param defaultInstance The default instance to return if no service providers are found.
492+
* @param <T> The type of the service provider.
493+
* @return The first service provider from the service loader, or the {@code defaultInstance} if
494+
* no service providers are found.
495+
*/
496+
public static <T> T getFromServiceLoader(Class<? extends T> clazz, T defaultInstance) {
488497
return Iterables.getFirst(ServiceLoader.load(clazz), defaultInstance);
489498
}
490499

oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,22 +69,36 @@
6969
import java.util.Map;
7070
import java.util.Set;
7171

72-
/** Internal utilities for the com.google.auth.oauth2 namespace. */
73-
class OAuth2Utils {
72+
/**
73+
* Internal utilities for the com.google.auth.oauth2 namespace.
74+
*
75+
* <p>These classes are marked public but should be treated effectively as internal classes only.
76+
* They are not subject to any backwards compatibility guarantees and might change or be removed at
77+
* any time. They are provided only as a convenience for other libraries within the {@code
78+
* com.google.auth} family. Application developers should avoid using these classes directly; they
79+
* are not part of the public API.
80+
*/
81+
public class OAuth2Utils {
82+
7483
static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
7584

76-
static final String TOKEN_TYPE_ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token";
85+
public static final String TOKEN_TYPE_ACCESS_TOKEN =
86+
"urn:ietf:params:oauth:token-type:access_token";
7787
static final String TOKEN_TYPE_TOKEN_EXCHANGE = "urn:ietf:params:oauth:token-type:token-exchange";
88+
public static final String TOKEN_TYPE_ACCESS_BOUNDARY_INTERMEDIARY_TOKEN =
89+
"urn:ietf:params:oauth:token-type:access_boundary_intermediary_token";
7890
static final String GRANT_TYPE_JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer";
7991

92+
public static final String TOKEN_EXCHANGE_URL_FORMAT = "https://sts.%s/v1/token";
8093
static final URI TOKEN_SERVER_URI = URI.create("https://oauth2.googleapis.com/token");
8194

8295
static final URI TOKEN_REVOKE_URI = URI.create("https://oauth2.googleapis.com/revoke");
8396
static final URI USER_AUTH_URI = URI.create("https://accounts.google.com/o/oauth2/auth");
8497

8598
static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
8699

87-
static final HttpTransportFactory HTTP_TRANSPORT_FACTORY = new DefaultHttpTransportFactory();
100+
public static final HttpTransportFactory HTTP_TRANSPORT_FACTORY =
101+
new DefaultHttpTransportFactory();
88102

89103
static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();
90104

@@ -241,8 +255,15 @@ static Map<String, Object> validateMap(Map<String, Object> map, String key, Stri
241255
return (Map) value;
242256
}
243257

244-
/** Helper to convert from a PKCS#8 String to an RSA private key */
245-
static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8) throws IOException {
258+
/**
259+
* Converts a PKCS#8 string to an RSA private key.
260+
*
261+
* @param privateKeyPkcs8 the PKCS#8 string.
262+
* @return the RSA private key.
263+
* @throws IOException if the PKCS#8 data is invalid or if an unexpected exception occurs during
264+
* key creation.
265+
*/
266+
public static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8) throws IOException {
246267
Reader reader = new StringReader(privateKeyPkcs8);
247268
Section section = PemReader.readFirstSectionAndClose(reader, "PRIVATE KEY");
248269
if (section == null) {

0 commit comments

Comments
 (0)