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
Original file line number Diff line number Diff line change
Expand Up @@ -490,14 +490,19 @@ public AccessToken refreshAccessToken() throws IOException {
* <br>
* IdTokenProvider.Option.FORMAT_FULL<br>
* IdTokenProvider.Option.LICENSES_TRUE<br>
* IdTokenProvider.Option.DISABLE_BOUND_ID_TOKEN (request an ID token that is not bound to the
* agent identity certificate)<br>
* If no options are set, the defaults are "&amp;format=standard&amp;licenses=false"
* @throws IOException if the attempt to get an IdToken failed
* @return IdToken object which includes the raw id_token, JsonWebSignature
*/
@Override
public IdToken idTokenWithAudience(
String targetAudience, @Nullable List<IdTokenProvider.Option> options) throws IOException {
String boundTokenPayload = AgentIdentityUtils.getBoundTokenPayload();
// Checked before getBoundTokenPayload() so an opted-out target skips the certificate lookup.
boolean bindIdToken =
options == null || !options.contains(IdTokenProvider.Option.DISABLE_BOUND_ID_TOKEN);
String boundTokenPayload = bindIdToken ? AgentIdentityUtils.getBoundTokenPayload() : null;
GenericUrl documentUrl = new GenericUrl(getIdentityDocumentUrl());
if (boundTokenPayload != null) {
documentUrl.set("format", "full");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public interface IdTokenProvider {
* <ul>
* <li>FORMAT_FULL
* <li>LICENSES_TRUE
* <li>DISABLE_BOUND_ID_TOKEN
* </ul>
*
* <br>
Expand All @@ -60,7 +61,28 @@ public interface IdTokenProvider {
public enum Option {
FORMAT_FULL("formatFull"),
LICENSES_TRUE("licensesTrue"),
INCLUDE_EMAIL("includeEmail");
INCLUDE_EMAIL("includeEmail"),
/**
* Requests an ID token that is not bound to the workload's agent identity certificate.
*
* <p>This option is only supported by {@link ComputeEngineCredentials}; other {@link
* IdTokenProvider} implementations do not request bound ID tokens and ignore this option.
*
* <p>When an agent identity certificate is available, {@link ComputeEngineCredentials} requests
* certificate-bound ID tokens by default. A bound ID token is only accepted by targets that
* authenticate the caller over mTLS with the same certificate. Pass this option for targets
* that are called over standard (non-mTLS) HTTPS, for example a Cloud Run service reached
* through its {@code *.run.app} URL or a custom domain.
*
* <p>Unlike the {@code GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false} environment variable, which
* globally disables certificate-bound access tokens and ID tokens across the entire process,
* this option applies per call (or per {@link IdTokenCredentials} instance) and leaves access
* tokens and other ID token requests bound. If this option is not set, the library decides
* whether to bind the ID token. Currently, it is bound whenever an agent identity certificate
* is available and token binding is not globally disabled by {@code
* GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false}.
*/
DISABLE_BOUND_ID_TOKEN("disableBoundIdToken");

private final String option;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1386,6 +1386,99 @@ void idTokenWithAudience_withValidCertAndKey_requestsBoundToken() throws IOExcep
assertEquals(expectedCert, ((String) bodyJson.get("certificate_chain")).trim());
}

@Test
void idTokenWithAudience_disableBoundIdToken_requestsUnboundToken() throws IOException {
setupCertAndKeyConfig();
envProvider.setEnv(AgentIdentityUtils.GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN, "true");
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL);
transportFactory.transport.setIdToken(STANDARD_ID_TOKEN);

ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
IdToken token =
credentials.idTokenWithAudience(
"https://foo.bar", Arrays.asList(IdTokenProvider.Option.DISABLE_BOUND_ID_TOKEN));

assertNotNull(token);
MockLowLevelHttpRequest request = transportFactory.transport.getRequest();
assertEquals("GET", transportFactory.transport.getRequestMethod());
assertTrue(request.getUrl().contains("audience=https://foo.bar"));
assertFalse(request.getUrl().contains("format=full"));
assertNull(request.getStreamingContent());
}

@Test
void idTokenWithAudience_disableBoundIdTokenWithFormatFull_requestsUnboundFullToken()
throws IOException {
setupCertAndKeyConfig();
envProvider.setEnv(AgentIdentityUtils.GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN, "true");
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL);
transportFactory.transport.setIdToken(FULL_ID_TOKEN);

ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
credentials.idTokenWithAudience(
"https://foo.bar",
Arrays.asList(
IdTokenProvider.Option.FORMAT_FULL, IdTokenProvider.Option.DISABLE_BOUND_ID_TOKEN));

MockLowLevelHttpRequest request = transportFactory.transport.getRequest();
assertEquals("GET", transportFactory.transport.getRequestMethod());
assertTrue(request.getUrl().contains("format=full"));
assertNull(request.getStreamingContent());
}

@Test
void idTokenWithAudience_disableBoundIdToken_skipsCertificateLookup() throws IOException {
// The certificate config points to a missing file, which fails a bound token request. With
// DISABLE_BOUND_ID_TOKEN the certificate is never looked up, so the unbound request succeeds.
envProvider.setEnv(AgentIdentityUtils.GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN, "true");
envProvider.setEnv(
AgentIdentityUtils.GOOGLE_API_CERTIFICATE_CONFIG,
tempDir.resolve("missing_config.json").toAbsolutePath().toString());
AgentIdentityUtils.setWellKnownDir(tempDir.toAbsolutePath().toString() + "/");
AgentIdentityUtils.setTimeService(millis -> {});
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL);
transportFactory.transport.setIdToken(STANDARD_ID_TOKEN);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();

assertThrows(IOException.class, () -> credentials.idTokenWithAudience("https://foo.bar", null));

IdToken token =
credentials.idTokenWithAudience(
"https://foo.bar", Arrays.asList(IdTokenProvider.Option.DISABLE_BOUND_ID_TOKEN));
assertNotNull(token);
assertEquals("GET", transportFactory.transport.getRequestMethod());
}

@Test
void idTokenCredentials_withDisableBoundIdTokenOption_requestsUnboundToken() throws IOException {
setupCertAndKeyConfig();
envProvider.setEnv(AgentIdentityUtils.GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN, "true");
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL);
transportFactory.transport.setIdToken(STANDARD_ID_TOKEN);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();

IdTokenCredentials idTokenCredentials =
IdTokenCredentials.newBuilder()
.setIdTokenProvider(credentials)
.setTargetAudience("https://foo.bar")
.setOptions(Arrays.asList(IdTokenProvider.Option.DISABLE_BOUND_ID_TOKEN))
.build();
idTokenCredentials.refresh();

MockLowLevelHttpRequest request = transportFactory.transport.getRequest();
assertEquals("GET", transportFactory.transport.getRequestMethod());
assertTrue(request.getUrl().contains("audience=https://foo.bar"));
assertNull(request.getStreamingContent());
}

@Test
void refreshAccessToken_boundToken404_throwsEndpointDoesNotSupportBoundTokensMessage()
throws IOException {
Expand Down
Loading