Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Copyright 2025, Google LLC
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google LLC nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package com.google.auth.credentialaccessboundary;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpResponseException;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.json.gson.GsonFactory;
import com.google.auth.Credentials;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.oauth2.CredentialAccessBoundary;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.OAuth2CredentialsWithRefresh;
import com.google.auth.oauth2.ServiceAccountCredentials;
import dev.cel.common.CelValidationException;
import java.io.IOException;
import java.security.GeneralSecurityException;
import org.junit.Test;

/**
* Integration tests for {@link ClientSideCredentialAccessBoundaryFactory}. *
*
* <p>The only requirements for this test suite to run is to set the environment variable
* GOOGLE_APPLICATION_CREDENTIALS to point to the same service account configured in the setup
* script (downscoping-with-cab-setup.sh).
*/
public final class ITClientSideCredentialAccessBoundaryTest {

// Output copied from the setup script (downscoping-with-cab-setup.sh).
private static final String GCS_BUCKET_NAME = "cab-int-bucket-cbi3qrv5";
private static final String GCS_OBJECT_NAME_WITH_PERMISSION = "cab-first-cbi3qrv5.txt";
private static final String GCS_OBJECT_NAME_WITHOUT_PERMISSION = "cab-second-cbi3qrv5.txt";

// This Credential Access Boundary enables the objectViewer permission to the specified object in
// the specified bucket.
private static final CredentialAccessBoundary CREDENTIAL_ACCESS_BOUNDARY =
CredentialAccessBoundary.newBuilder()
.addRule(
CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
.setAvailableResource(
String.format(
"//storage.googleapis.com/projects/_/buckets/%s", GCS_BUCKET_NAME))
.addAvailablePermission("inRole:roles/storage.objectViewer")
.setAvailabilityCondition(
CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition.newBuilder()
.setExpression(
String.format(
"resource.name.startsWith('projects/_/buckets/%s/objects/%s')",
GCS_BUCKET_NAME, GCS_OBJECT_NAME_WITH_PERMISSION))
.build())
.build())
.build();

/**
* A downscoped credential is obtained using ClientSideCredentialAccessBoundaryFactory with
* permissions to access an object in the GCS bucket configured. We should only have access to
* retrieve this object.
*
* <p>We confirm this by: 1. Validating that we can successfully retrieve this object with the
* downscoped token. 2. Validating that we do not have permission to retrieve a different object
* in the same bucket.
*/
@Test
public void clientSideCredentialAccessBoundary_serviceAccountSource() throws IOException {
OAuth2CredentialsWithRefresh.OAuth2RefreshHandler refreshHandler =
() -> {
ServiceAccountCredentials sourceCredentials =
(ServiceAccountCredentials)
GoogleCredentials.getApplicationDefault()
.createScoped("https://www.googleapis.com/auth/cloud-platform");

ClientSideCredentialAccessBoundaryFactory factory =
ClientSideCredentialAccessBoundaryFactory.newBuilder()
.setSourceCredential(sourceCredentials)
.build();

try {
return factory.generateToken(CREDENTIAL_ACCESS_BOUNDARY);
} catch (CelValidationException | GeneralSecurityException e) {
throw new RuntimeException(e);
}
};

OAuth2CredentialsWithRefresh credentials =
OAuth2CredentialsWithRefresh.newBuilder().setRefreshHandler(refreshHandler).build();

// Attempt to retrieve the object that the downscoped token has access to.
retrieveObjectFromGcs(credentials, GCS_OBJECT_NAME_WITH_PERMISSION);

// Attempt to retrieve the object that the downscoped token does not have access to. This should
// fail.
HttpResponseException exception =
assertThrows(
HttpResponseException.class,
() -> retrieveObjectFromGcs(credentials, GCS_OBJECT_NAME_WITHOUT_PERMISSION));
assertEquals(403, exception.getStatusCode());
}

private void retrieveObjectFromGcs(Credentials credentials, String objectName)
throws IOException {
String url =
String.format(
"https://storage.googleapis.com/storage/v1/b/%s/o/%s", GCS_BUCKET_NAME, objectName);

HttpCredentialsAdapter credentialsAdapter = new HttpCredentialsAdapter(credentials);
HttpRequestFactory requestFactory =
new NetHttpTransport().createRequestFactory(credentialsAdapter);
HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(url));

JsonObjectParser parser = new JsonObjectParser(GsonFactory.getDefaultInstance());
request.setParser(parser);

HttpResponse response = request.execute();
assertTrue(response.isSuccessStatusCode());
}
}
86 changes: 86 additions & 0 deletions cab-token-generator/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-parent</artifactId>
<version>1.31.1-SNAPSHOT
</version><!-- {x-version-update:google-auth-library-parent:current} -->
</parent>

<artifactId>google-auth-library-cab-token-generator</artifactId>
<name>Google Auth Library for Java - Cab Token Generator</name>

<build>
<sourceDirectory>java</sourceDirectory>
<testSourceDirectory>javatests</testSourceDirectory>
</build>

<dependencies>
<!-- Compile and Runtime Dependencies -->
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
</dependency>
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-credentials</artifactId>
</dependency>
<dependency>
<groupId>com.google.http-client</groupId>
<artifactId>google-http-client</artifactId>
</dependency>
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_annotations</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
</dependency>
<dependency>
<groupId>dev.cel</groupId>
<artifactId>cel</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
</dependency>
<dependency>
<groupId>com.google.crypto.tink</groupId>
<artifactId>tink</artifactId>
</dependency>

<!-- Test Dependencies -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
<scope>test</scope>
<type>test-jar</type>
<classifier>testlib</classifier>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.http-client</groupId>
<artifactId>google-http-client-gson</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

package com.google.auth.oauth2;

import static com.google.auth.oauth2.OAuth2Utils.TOKEN_EXCHANGE_URL_FORMAT;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkNotNull;

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

private final String TOKEN_EXCHANGE_URL_FORMAT = "https://sts.{universe_domain}/v1/token";
private final GoogleCredentials sourceCredential;
private final CredentialAccessBoundary credentialAccessBoundary;
private final String universeDomain;
Expand Down Expand Up @@ -125,8 +132,7 @@ private DownscopedCredentials(Builder builder) {
throw new IllegalStateException(
"Error occurred when attempting to retrieve source credential universe domain.", e);
}
this.tokenExchangeEndpoint =
TOKEN_EXCHANGE_URL_FORMAT.replace("{universe_domain}", universeDomain);
this.tokenExchangeEndpoint = String.format(TOKEN_EXCHANGE_URL_FORMAT, universeDomain);
}

@Override
Expand Down
11 changes: 10 additions & 1 deletion oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,16 @@ protected static <T> T newInstance(String className) throws IOException, ClassNo
}
}

protected static <T> T getFromServiceLoader(Class<? extends T> clazz, T defaultInstance) {
/**
* Returns the first service provider from the given service loader.
*
* @param clazz The class of the service provider to load.
* @param defaultInstance The default instance to return if no service providers are found.
* @param <T> The type of the service provider.
* @return The first service provider from the service loader, or the {@code defaultInstance} if
* no service providers are found.
*/
public static <T> T getFromServiceLoader(Class<? extends T> clazz, T defaultInstance) {
return Iterables.getFirst(ServiceLoader.load(clazz), defaultInstance);
}

Expand Down
33 changes: 27 additions & 6 deletions oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,36 @@
import java.util.Map;
import java.util.Set;

/** Internal utilities for the com.google.auth.oauth2 namespace. */
class OAuth2Utils {
/**
* Internal utilities for the com.google.auth.oauth2 namespace.
*
* <p>These classes are marked public but should be treated effectively as internal classes only.
* They are not subject to any backwards compatibility guarantees and might change or be removed at
* any time. They are provided only as a convenience for other libraries within the {@code
* com.google.auth} family. Application developers should avoid using these classes directly; they
* are not part of the public API.
*/
public class OAuth2Utils {

static final String SIGNATURE_ALGORITHM = "SHA256withRSA";

static final String TOKEN_TYPE_ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token";
public static final String TOKEN_TYPE_ACCESS_TOKEN =
"urn:ietf:params:oauth:token-type:access_token";
static final String TOKEN_TYPE_TOKEN_EXCHANGE = "urn:ietf:params:oauth:token-type:token-exchange";
public static final String TOKEN_TYPE_ACCESS_BOUNDARY_INTERMEDIARY_TOKEN =
"urn:ietf:params:oauth:token-type:access_boundary_intermediary_token";
static final String GRANT_TYPE_JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer";

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

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

static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();

static final HttpTransportFactory HTTP_TRANSPORT_FACTORY = new DefaultHttpTransportFactory();
public static final HttpTransportFactory HTTP_TRANSPORT_FACTORY =
new DefaultHttpTransportFactory();

static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();

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

/** Helper to convert from a PKCS#8 String to an RSA private key */
static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8) throws IOException {
/**
* Converts a PKCS#8 string to an RSA private key.
*
* @param privateKeyPkcs8 the PKCS#8 string.
* @return the RSA private key.
* @throws IOException if the PKCS#8 data is invalid or if an unexpected exception occurs during
* key creation.
*/
public static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8) throws IOException {
Reader reader = new StringReader(privateKeyPkcs8);
Section section = PemReader.readFirstSectionAndClose(reader, "PRIVATE KEY");
if (section == null) {
Expand Down
Loading
Loading