diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b25d5f6db..08eaf507a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -18,32 +18,37 @@ samples/**/*.java @googleapis/java-samples-reviewers samples/snippets/generated/ @googleapis/yoshi-java # 3PI-related files and related base classes - joint ownership between googleapis-auth and aion-sdk -oauth2_http/java/com/google/auth/oauth2/ActingParty.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/AwsDates.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/AwsRequestSignature.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/CredentialAccessBoundary.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/DefaultCredentialsProvider.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/DownscopedCredentials.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/EnvironmentProvider.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/ExecutableHandler.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/ExecutableResponse.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/OAuth2CredentialsWithRefresh.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/OAuthException.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/PluggableAuthException.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/PluggableAuthHandler.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java @googleapis/googleapis-auth @googleapis/aion-sdk -oauth2_http/java/com/google/auth/oauth2/SystemEnvironmentProvider.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/ActingParty.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentialsSupplier.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/AwsDates.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/AwsRequestSignature.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/CredentialAccessBoundary.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/DefaultCredentialsProvider.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/DownscopedCredentials.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/EnvironmentProvider.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/ExecutableHandler.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/ExecutableResponse.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolSubjectTokenSupplier.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/IdentityPoolSubjectTokenSupplier.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/InternalAwsSecurityCredentialsSupplier.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/OAuth2CredentialsWithRefresh.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/OAuthException.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/PluggableAuthException.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/PluggableAuthHandler.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/SystemEnvironmentProvider.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/java/com/google/auth/oauth2/UrlIdentityPoolSubjectTokenSupplier.java @googleapis/googleapis-auth @googleapis/aion-sdk oauth2_http/javatests/com/google/auth/AwsCredentialsTest.java @googleapis/googleapis-auth @googleapis/aion-sdk oauth2_http/javatests/com/google/auth/AwsRequestSignerTest.java @googleapis/googleapis-auth @googleapis/aion-sdk oauth2_http/javatests/com/google/auth/CredentialAccessBoundaryTest.java @googleapis/googleapis-auth @googleapis/aion-sdk @@ -55,6 +60,7 @@ oauth2_http/javatests/com/google/auth/ITDownscopingTest.java oauth2_http/javatests/com/google/auth/ITWorkloadIdentityFederationTest.java @googleapis/googleapis-auth @googleapis/aion-sdk oauth2_http/javatests/com/google/auth/IdentityPoolCredentialsTest.java @googleapis/googleapis-auth @googleapis/aion-sdk oauth2_http/javatests/com/google/auth/ImpersonatedCredentialsTest.java @googleapis/googleapis-auth @googleapis/aion-sdk +oauth2_http/javatests/com/google/auth/InternalAwsSecurityCredentialsSupplierTest.java @googleapis/googleapis-auth @googleapis/aion-sdk oauth2_http/javatests/com/google/auth/MockExternalAccountCredentialsTransport.java @googleapis/googleapis-auth @googleapis/aion-sdk oauth2_http/javatests/com/google/auth/MockStsTransport.java @googleapis/googleapis-auth @googleapis/aion-sdk oauth2_http/javatests/com/google/auth/OAuth2CredentialsTest.java @googleapis/googleapis-auth @googleapis/aion-sdk @@ -65,4 +71,4 @@ oauth2_http/javatests/com/google/auth/PluggableAuthExceptionTest.java oauth2_http/javatests/com/google/auth/PluggableAuthHandlerTest.java @googleapis/googleapis-auth @googleapis/aion-sdk oauth2_http/javatests/com/google/auth/StsRequestHandlerTest.java @googleapis/googleapis-auth @googleapis/aion-sdk oauth2_http/javatests/com/google/auth/TestEnvironmentProvider.java @googleapis/googleapis-auth @googleapis/aion-sdk -README.md @googleapis/googleapis-auth @googleapis/aion-sdk \ No newline at end of file +README.md @googleapis/googleapis-auth @googleapis/aion-sdk diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java index e47bbe167..215c77a64 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java @@ -31,17 +31,10 @@ package com.google.auth.oauth2; -import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.HttpContent; -import com.google.api.client.http.HttpHeaders; -import com.google.api.client.http.HttpMethods; -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.json.GenericJson; -import com.google.api.client.json.JsonParser; +import com.google.auth.http.HttpTransportFactory; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; @@ -53,30 +46,66 @@ import javax.annotation.Nullable; /** - * AWS credentials representing a third-party identity for calling Google APIs. + * Credentials representing an AWS third-party identity for calling Google APIs. AWS security + * credentials are either sourced by calling EC2 metadata endpoints, environment variables, or a + * user provided supplier method. * *

By default, attempts to exchange the external credential for a GCP access token. */ public class AwsCredentials extends ExternalAccountCredentials { - // Supported environment variables. - static final String AWS_REGION = "AWS_REGION"; - static final String AWS_DEFAULT_REGION = "AWS_DEFAULT_REGION"; - static final String AWS_ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID"; - static final String AWS_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY"; - static final String AWS_SESSION_TOKEN = "AWS_SESSION_TOKEN"; + static final String DEFAULT_REGIONAL_CREDENTIAL_VERIFICATION_URL = + "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"; + + static final String AWS_METRICS_HEADER_VALUE = "aws"; - static final String AWS_IMDSV2_SESSION_TOKEN_HEADER = "x-aws-ec2-metadata-token"; - static final String AWS_IMDSV2_SESSION_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds"; - static final String AWS_IMDSV2_SESSION_TOKEN_TTL = "300"; private static final long serialVersionUID = -3670131891574618105L; - private final AwsCredentialSource awsCredentialSource; + @Nullable private final AwsSecurityCredentialsSupplier awsSecurityCredentialsSupplier; + // Regional credential verification url override. This needs to be its own value so we can + // correctly pass it to a builder. + @Nullable private final String regionalCredentialVerificationUrlOverride; + @Nullable private final String regionalCredentialVerificationUrl; + private final String metricsHeaderValue; /** Internal constructor. See {@link AwsCredentials.Builder}. */ AwsCredentials(Builder builder) { super(builder); - this.awsCredentialSource = (AwsCredentialSource) builder.credentialSource; + // Check that one and only one of supplier or credential source are provided. + if (builder.awsSecurityCredentialsSupplier != null && builder.credentialSource != null) { + throw new IllegalArgumentException( + "AwsCredentials cannot have both an awsSecurityCredentialsSupplier and a credentialSource."); + } + if (builder.awsSecurityCredentialsSupplier == null && builder.credentialSource == null) { + throw new IllegalArgumentException( + "An awsSecurityCredentialsSupplier or a credentialSource must be provided."); + } + + AwsCredentialSource credentialSource = (AwsCredentialSource) builder.credentialSource; + // Set regional credential verification url override if provided. + this.regionalCredentialVerificationUrlOverride = + builder.regionalCredentialVerificationUrlOverride; + + // Set regional credential verification url depending on inputs. + if (this.regionalCredentialVerificationUrlOverride != null) { + this.regionalCredentialVerificationUrl = this.regionalCredentialVerificationUrlOverride; + } else if (credentialSource != null) { + this.regionalCredentialVerificationUrl = credentialSource.regionalCredentialVerificationUrl; + } else { + this.regionalCredentialVerificationUrl = DEFAULT_REGIONAL_CREDENTIAL_VERIFICATION_URL; + } + + // If user has provided a security credential supplier, use that to retrieve the AWS security + // credentials. + if (builder.awsSecurityCredentialsSupplier != null) { + this.awsSecurityCredentialsSupplier = builder.awsSecurityCredentialsSupplier; + this.metricsHeaderValue = PROGRAMMATIC_METRICS_HEADER_VALUE; + } else { + this.awsSecurityCredentialsSupplier = + new InternalAwsSecurityCredentialsSupplier( + credentialSource, this.getEnvironmentProvider(), this.transportFactory); + this.metricsHeaderValue = AWS_METRICS_HEADER_VALUE; + } } @Override @@ -96,16 +125,12 @@ public AccessToken refreshAccessToken() throws IOException { @Override public String retrieveSubjectToken() throws IOException { - Map metadataRequestHeaders = new HashMap<>(); - if (shouldUseMetadataServer()) { - metadataRequestHeaders = createMetadataRequestHeaders(awsCredentialSource); - } // The targeted region is required to generate the signed request. The regional // endpoint must also be used. - String region = getAwsRegion(metadataRequestHeaders); + String region = awsSecurityCredentialsSupplier.getRegion(); - AwsSecurityCredentials credentials = getAwsSecurityCredentials(metadataRequestHeaders); + AwsSecurityCredentials credentials = awsSecurityCredentialsSupplier.getCredentials(); // Generate the signed request to the AWS STS GetCallerIdentity API. Map headers = new HashMap<>(); @@ -115,7 +140,7 @@ public String retrieveSubjectToken() throws IOException { AwsRequestSigner.newBuilder( credentials, "POST", - awsCredentialSource.regionalCredentialVerificationUrl.replace("{region}", region), + this.regionalCredentialVerificationUrl.replace("{region}", region), region) .setAdditionalHeaders(headers) .build(); @@ -132,36 +157,7 @@ public GoogleCredentials createScoped(Collection newScopes) { @Override String getCredentialSourceType() { - return "aws"; - } - - private String retrieveResource(String url, String resourceName, Map headers) - throws IOException { - return retrieveResource(url, resourceName, HttpMethods.GET, headers, /* content= */ null); - } - - private String retrieveResource( - String url, - String resourceName, - String requestMethod, - Map headers, - @Nullable HttpContent content) - throws IOException { - try { - HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory(); - HttpRequest request = - requestFactory.buildRequest(requestMethod, new GenericUrl(url), content); - - HttpHeaders requestHeaders = request.getHeaders(); - for (Map.Entry header : headers.entrySet()) { - requestHeaders.set(header.getKey(), header.getValue()); - } - - HttpResponse response = request.execute(); - return response.parseAsString(); - } catch (IOException e) { - throw new IOException(String.format("Failed to retrieve AWS %s.", resourceName), e); - } + return this.metricsHeaderValue; } private String buildSubjectToken(AwsRequestSignature signature) @@ -183,145 +179,28 @@ private String buildSubjectToken(AwsRequestSignature signature) token.put("headers", headerList); token.put("method", signature.getHttpMethod()); token.put( - "url", - awsCredentialSource.regionalCredentialVerificationUrl.replace( - "{region}", signature.getRegion())); + "url", this.regionalCredentialVerificationUrl.replace("{region}", signature.getRegion())); return URLEncoder.encode(token.toString(), "UTF-8"); } - private boolean canRetrieveRegionFromEnvironment() { - // The AWS region can be provided through AWS_REGION or AWS_DEFAULT_REGION. Only one is - // required. - List keys = ImmutableList.of(AWS_REGION, AWS_DEFAULT_REGION); - for (String env : keys) { - String value = getEnvironmentProvider().getEnv(env); - if (value != null && value.trim().length() > 0) { - // Region available. - return true; - } - } - return false; - } - - private boolean canRetrieveSecurityCredentialsFromEnvironment() { - // Check if both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are available. - List keys = ImmutableList.of(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY); - for (String env : keys) { - String value = getEnvironmentProvider().getEnv(env); - if (value == null || value.trim().length() == 0) { - // Return false if one of them are missing. - return false; - } - } - return true; - } - @VisibleForTesting - boolean shouldUseMetadataServer() { - return !canRetrieveRegionFromEnvironment() || !canRetrieveSecurityCredentialsFromEnvironment(); + String getRegionalCredentialVerificationUrl() { + return this.regionalCredentialVerificationUrl; } @VisibleForTesting - Map createMetadataRequestHeaders(AwsCredentialSource awsCredentialSource) - throws IOException { - Map metadataRequestHeaders = new HashMap<>(); - - // AWS IDMSv2 introduced a requirement for a session token to be present - // with the requests made to metadata endpoints. This requirement is to help - // prevent SSRF attacks. - // Presence of "imdsv2_session_token_url" in Credential Source of config file - // will trigger a flow with session token, else there will not be a session - // token with the metadata requests. - // Both flows work for IDMS v1 and v2. But if IDMSv2 is enabled, then if - // session token is not present, Unauthorized exception will be thrown. - if (awsCredentialSource.imdsv2SessionTokenUrl != null) { - Map tokenRequestHeaders = - new HashMap() { - { - put(AWS_IMDSV2_SESSION_TOKEN_TTL_HEADER, AWS_IMDSV2_SESSION_TOKEN_TTL); - } - }; - - String imdsv2SessionToken = - retrieveResource( - awsCredentialSource.imdsv2SessionTokenUrl, - "Session Token", - HttpMethods.PUT, - tokenRequestHeaders, - /* content= */ null); - - metadataRequestHeaders.put(AWS_IMDSV2_SESSION_TOKEN_HEADER, imdsv2SessionToken); - } - - return metadataRequestHeaders; + String getEnv(String name) { + return System.getenv(name); } @VisibleForTesting - String getAwsRegion(Map metadataRequestHeaders) throws IOException { - String region; - if (canRetrieveRegionFromEnvironment()) { - // For AWS Lambda, the region is retrieved through the AWS_REGION environment variable. - region = getEnvironmentProvider().getEnv(AWS_REGION); - if (region != null && region.trim().length() > 0) { - return region; - } - return getEnvironmentProvider().getEnv(AWS_DEFAULT_REGION); - } - - if (awsCredentialSource.regionUrl == null || awsCredentialSource.regionUrl.isEmpty()) { - throw new IOException( - "Unable to determine the AWS region. The credential source does not contain the region URL."); - } - - region = retrieveResource(awsCredentialSource.regionUrl, "region", metadataRequestHeaders); - - // There is an extra appended character that must be removed. If `us-east-1b` is returned, - // we want `us-east-1`. - return region.substring(0, region.length() - 1); + AwsSecurityCredentialsSupplier getAwsSecurityCredentialsSupplier() { + return this.awsSecurityCredentialsSupplier; } - @VisibleForTesting - AwsSecurityCredentials getAwsSecurityCredentials(Map metadataRequestHeaders) - throws IOException { - // Check environment variables for credentials first. - if (canRetrieveSecurityCredentialsFromEnvironment()) { - String accessKeyId = getEnvironmentProvider().getEnv(AWS_ACCESS_KEY_ID); - String secretAccessKey = getEnvironmentProvider().getEnv(AWS_SECRET_ACCESS_KEY); - String token = getEnvironmentProvider().getEnv(AWS_SESSION_TOKEN); - return new AwsSecurityCredentials(accessKeyId, secretAccessKey, token); - } - - // Credentials not retrievable from environment variables - call metadata server. - // Retrieve the IAM role that is attached to the VM. This is required to retrieve the AWS - // security credentials. - if (awsCredentialSource.url == null || awsCredentialSource.url.isEmpty()) { - throw new IOException( - "Unable to determine the AWS IAM role name. The credential source does not contain the" - + " url field."); - } - String roleName = retrieveResource(awsCredentialSource.url, "IAM role", metadataRequestHeaders); - - // Retrieve the AWS security credentials by calling the endpoint specified by the credential - // source. - String awsCredentials = - retrieveResource( - awsCredentialSource.url + "/" + roleName, "credentials", metadataRequestHeaders); - - JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser(awsCredentials); - GenericJson genericJson = parser.parseAndClose(GenericJson.class); - - String accessKeyId = (String) genericJson.get("AccessKeyId"); - String secretAccessKey = (String) genericJson.get("SecretAccessKey"); - String token = (String) genericJson.get("Token"); - - // These credentials last for a few hours - we may consider caching these in the - // future. - return new AwsSecurityCredentials(accessKeyId, secretAccessKey, token); - } - - @VisibleForTesting - String getEnv(String name) { - return System.getenv(name); + @Nullable + public String getRegionalCredentialVerificationUrlOverride() { + return this.regionalCredentialVerificationUrlOverride; } private static GenericJson formatTokenHeaderForSts(String key, String value) { @@ -348,10 +227,145 @@ public static AwsCredentials.Builder newBuilder(AwsCredentials awsCredentials) { public static class Builder extends ExternalAccountCredentials.Builder { + private AwsSecurityCredentialsSupplier awsSecurityCredentialsSupplier; + + private String regionalCredentialVerificationUrlOverride; + Builder() {} Builder(AwsCredentials credentials) { super(credentials); + if (this.credentialSource == null) { + this.awsSecurityCredentialsSupplier = credentials.awsSecurityCredentialsSupplier; + } + this.regionalCredentialVerificationUrlOverride = + credentials.regionalCredentialVerificationUrlOverride; + } + + /** + * Sets the AWS security credentials supplier. The supplier should return a valid {@code + * AwsSecurityCredentials} object and a valid AWS region. + * + * @param awsSecurityCredentialsSupplier the supplier to use. + * @return this {@code Builder} object + */ + @CanIgnoreReturnValue + public Builder setAwsSecurityCredentialsSupplier( + AwsSecurityCredentialsSupplier awsSecurityCredentialsSupplier) { + this.awsSecurityCredentialsSupplier = awsSecurityCredentialsSupplier; + return this; + } + + /** + * Sets the AWS regional credential verification URL. If set, will override any credential + * verification URL provided in the credential source. If not set, the credential verification + * URL will default to + * https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15" + * + * @param regionalCredentialVerificationUrlOverride the AWS credential verification url to set. + * @return this {@code Builder} object + */ + @CanIgnoreReturnValue + public Builder setRegionalCredentialVerificationUrlOverride( + String regionalCredentialVerificationUrlOverride) { + this.regionalCredentialVerificationUrlOverride = regionalCredentialVerificationUrlOverride; + return this; + } + + @CanIgnoreReturnValue + public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) { + super.setHttpTransportFactory(transportFactory); + return this; + } + + @CanIgnoreReturnValue + public Builder setAudience(String audience) { + super.setAudience(audience); + return this; + } + + @CanIgnoreReturnValue + public Builder setSubjectTokenType(String subjectTokenType) { + super.setSubjectTokenType(subjectTokenType); + return this; + } + + @CanIgnoreReturnValue + public Builder setSubjectTokenType(SubjectTokenTypes subjectTokenType) { + super.setSubjectTokenType(subjectTokenType); + return this; + } + + @CanIgnoreReturnValue + public Builder setTokenUrl(String tokenUrl) { + super.setTokenUrl(tokenUrl); + return this; + } + + @CanIgnoreReturnValue + public Builder setCredentialSource(AwsCredentialSource credentialSource) { + super.setCredentialSource(credentialSource); + return this; + } + + @CanIgnoreReturnValue + public Builder setServiceAccountImpersonationUrl(String serviceAccountImpersonationUrl) { + super.setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl); + return this; + } + + @CanIgnoreReturnValue + public Builder setTokenInfoUrl(String tokenInfoUrl) { + super.setTokenInfoUrl(tokenInfoUrl); + return this; + } + + @CanIgnoreReturnValue + public Builder setQuotaProjectId(String quotaProjectId) { + super.setQuotaProjectId(quotaProjectId); + return this; + } + + @CanIgnoreReturnValue + public Builder setClientId(String clientId) { + super.setClientId(clientId); + return this; + } + + @CanIgnoreReturnValue + public Builder setClientSecret(String clientSecret) { + super.setClientSecret(clientSecret); + return this; + } + + @CanIgnoreReturnValue + public Builder setScopes(Collection scopes) { + super.setScopes(scopes); + return this; + } + + @CanIgnoreReturnValue + public Builder setWorkforcePoolUserProject(String workforcePoolUserProject) { + super.setWorkforcePoolUserProject(workforcePoolUserProject); + return this; + } + + @CanIgnoreReturnValue + public Builder setServiceAccountImpersonationOptions(Map optionsMap) { + super.setServiceAccountImpersonationOptions(optionsMap); + return this; + } + + @CanIgnoreReturnValue + public Builder setUniverseDomain(String universeDomain) { + super.setUniverseDomain(universeDomain); + return this; + } + + @CanIgnoreReturnValue + Builder setEnvironmentProvider(EnvironmentProvider environmentProvider) { + super.setEnvironmentProvider(environmentProvider); + return this; } @Override diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java index a890c8814..275c15105 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java @@ -237,8 +237,9 @@ private Map getCanonicalHeaders(String defaultDate) { headers.put("x-amz-date", defaultDate); } - if (awsSecurityCredentials.getToken() != null && !awsSecurityCredentials.getToken().isEmpty()) { - headers.put("x-amz-security-token", awsSecurityCredentials.getToken()); + if (awsSecurityCredentials.getSessionToken() != null + && !awsSecurityCredentials.getSessionToken().isEmpty()) { + headers.put("x-amz-security-token", awsSecurityCredentials.getSessionToken()); } // Add all additional headers. diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java b/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java index b7865049a..7101dda3e 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java @@ -37,29 +37,52 @@ * Defines AWS security credentials. These are either retrieved from the AWS security_credentials * endpoint or AWS environment variables. */ -class AwsSecurityCredentials { +public class AwsSecurityCredentials { private final String accessKeyId; private final String secretAccessKey; - @Nullable private final String token; + @Nullable private final String sessionToken; - AwsSecurityCredentials(String accessKeyId, String secretAccessKey, @Nullable String token) { + /** + * Constructor for AWSSecurityCredentials. + * + * @param accessKeyId the AWS access Key Id. + * @param secretAccessKey the AWS secret access key. + * @param sessionToken the AWS session token. Optional. + */ + public AwsSecurityCredentials( + String accessKeyId, String secretAccessKey, @Nullable String sessionToken) { this.accessKeyId = accessKeyId; this.secretAccessKey = secretAccessKey; - this.token = token; + this.sessionToken = sessionToken; } - String getAccessKeyId() { + /** + * Gets the AWS access key id. + * + * @return the AWS access key id. + */ + public String getAccessKeyId() { return accessKeyId; } - String getSecretAccessKey() { + /** + * Gets the AWS secret access key. + * + * @return the AWS secret access key. + */ + public String getSecretAccessKey() { return secretAccessKey; } + /** + * Gets the AWS session token. + * + * @return the AWS session token. + */ @Nullable - String getToken() { - return token; + public String getSessionToken() { + return sessionToken; } } diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentialsSupplier.java b/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentialsSupplier.java new file mode 100644 index 000000000..b28dd858d --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentialsSupplier.java @@ -0,0 +1,58 @@ +/* + * Copyright 2024 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.oauth2; + +import java.io.IOException; +import java.io.Serializable; + +/** + * Supplier for retrieving AWS Security credentials for {@Link AwsCredentials} to exchange for GCP + * access tokens. + */ +public interface AwsSecurityCredentialsSupplier extends Serializable { + + /** + * Gets the AWS region to use. + * + * @return the AWS region that should be used for the credential. + * @throws IOException + */ + String getRegion() throws IOException; + + /** + * Gets AWS security credentials. + * + * @return valid AWS security credentials that can be exchanged for a GCP access token. + * @throws IOException + */ + AwsSecurityCredentials getCredentials() throws IOException; +} diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index 6e6bb2e8b..ad9633da8 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -67,22 +67,15 @@ public abstract class ExternalAccountCredentials extends GoogleCredentials { private static final long serialVersionUID = 8049126194174465023L; - /** Base credential source class. Dictates the retrieval method of the external credential. */ - abstract static class CredentialSource implements java.io.Serializable { - - private static final long serialVersionUID = 8204657811562399944L; - - CredentialSource(Map credentialSourceMap) { - checkNotNull(credentialSourceMap); - } - } - private static final String CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform"; static final String EXTERNAL_ACCOUNT_FILE_TYPE = "external_account"; static final String EXECUTABLE_SOURCE_KEY = "executable"; + static final String DEFAULT_TOKEN_URL = "https://sts.googleapis.com/v1/token"; + static final String PROGRAMMATIC_METRICS_HEADER_VALUE = "programmatic"; + private final String transportFactoryClassName; private final String audience; private final String subjectTokenType; @@ -104,11 +97,7 @@ abstract static class CredentialSource implements java.io.Serializable { protected transient HttpTransportFactory transportFactory; - @Nullable protected final ImpersonatedCredentials impersonatedCredentials; - - // Internal override for impersonated credentials. This is done to keep - // impersonatedCredentials final. - @Nullable private ImpersonatedCredentials impersonatedCredentialsOverride; + @Nullable protected ImpersonatedCredentials impersonatedCredentials; private EnvironmentProvider environmentProvider; @@ -224,8 +213,6 @@ protected ExternalAccountCredentials( } this.metricsHandler = new ExternalAccountMetricsHandler(this); - - this.impersonatedCredentials = buildImpersonatedCredentials(); } /** @@ -243,12 +230,12 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder) this.transportFactoryClassName = checkNotNull(this.transportFactory.getClass().getName()); this.audience = checkNotNull(builder.audience); this.subjectTokenType = checkNotNull(builder.subjectTokenType); - this.tokenUrl = checkNotNull(builder.tokenUrl); - this.credentialSource = checkNotNull(builder.credentialSource); + this.credentialSource = builder.credentialSource; this.tokenInfoUrl = builder.tokenInfoUrl; this.serviceAccountImpersonationUrl = builder.serviceAccountImpersonationUrl; this.clientId = builder.clientId; this.clientSecret = builder.clientSecret; + this.tokenUrl = builder.tokenUrl == null ? DEFAULT_TOKEN_URL : builder.tokenUrl; this.scopes = (builder.scopes == null || builder.scopes.isEmpty()) ? Arrays.asList(CLOUD_PLATFORM_SCOPE) @@ -277,8 +264,6 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder) builder.metricsHandler == null ? new ExternalAccountMetricsHandler(this) : builder.metricsHandler; - - this.impersonatedCredentials = buildImpersonatedCredentials(); } ImpersonatedCredentials buildImpersonatedCredentials() { @@ -316,10 +301,6 @@ ImpersonatedCredentials buildImpersonatedCredentials() { .build(); } - void overrideImpersonatedCredentials(ImpersonatedCredentials credentials) { - this.impersonatedCredentialsOverride = credentials; - } - @Override public void getRequestMetadata( URI uri, Executor executor, final RequestMetadataCallback callback) { @@ -479,6 +460,10 @@ private static boolean isAwsCredential(Map credentialSource) { && ((String) credentialSource.get("environment_id")).startsWith("aws"); } + private boolean shouldBuildImpersonatedCredential() { + return this.serviceAccountImpersonationUrl != null && this.impersonatedCredentials == null; + } + /** * Exchanges the external credential for a Google Cloud access token. * @@ -489,11 +474,11 @@ private static boolean isAwsCredential(Map credentialSource) { protected AccessToken exchangeExternalCredentialForAccessToken( StsTokenExchangeRequest stsTokenExchangeRequest) throws IOException { // Handle service account impersonation if necessary. - // Internal override takes priority. - if (impersonatedCredentialsOverride != null) { - return impersonatedCredentialsOverride.refreshAccessToken(); - } else if (impersonatedCredentials != null) { - return impersonatedCredentials.refreshAccessToken(); + if (this.shouldBuildImpersonatedCredential()) { + this.impersonatedCredentials = this.buildImpersonatedCredentials(); + } + if (this.impersonatedCredentials != null) { + return this.impersonatedCredentials.refreshAccessToken(); } StsRequestHandler.Builder requestHandler = @@ -799,6 +784,19 @@ public Builder setSubjectTokenType(String subjectTokenType) { return this; } + /** + * Sets the Security Token Service subject token type based on the OAuth 2.0 token exchange + * spec. Indicates the type of the security token in the credential file. + * + * @param subjectTokenType the {@code SubjectTokenType} to set + * @return this {@code Builder} object + */ + @CanIgnoreReturnValue + public Builder setSubjectTokenType(SubjectTokenTypes subjectTokenType) { + this.subjectTokenType = subjectTokenType.value; + return this; + } + /** * Sets the Security Token Service token exchange endpoint. * @@ -953,4 +951,30 @@ Builder setEnvironmentProvider(EnvironmentProvider environmentProvider) { @Override public abstract ExternalAccountCredentials build(); } + + /** + * Enum specifying values for the subjectTokenType field in {@code ExternalAccountCredentials}. + */ + public enum SubjectTokenTypes { + AWS4("urn:ietf:params:aws:token-type:aws4_request"), + JWT("urn:ietf:params:oauth:token-type:jwt"), + SAML2("urn:ietf:params:oauth:token-type:saml2"), + ID_TOKEN("urn:ietf:params:oauth:token-type:id_token"); + + public final String value; + + private SubjectTokenTypes(String value) { + this.value = value; + } + } + + /** Base credential source class. Dictates the retrieval method of the external credential. */ + abstract static class CredentialSource implements java.io.Serializable { + + private static final long serialVersionUID = 8204657811562399944L; + + CredentialSource(Map credentialSourceMap) { + checkNotNull(credentialSourceMap); + } + } } diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountMetricsHandler.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountMetricsHandler.java index fcb656b5d..18fc124b8 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountMetricsHandler.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountMetricsHandler.java @@ -43,7 +43,7 @@ class ExternalAccountMetricsHandler implements java.io.Serializable { private final boolean configLifetime; private final boolean saImpersonation; - private String credentialSourceType; + private ExternalAccountCredentials credentials; /** * Constructor for the external account metrics handler. @@ -55,7 +55,7 @@ class ExternalAccountMetricsHandler implements java.io.Serializable { this.saImpersonation = creds.getServiceAccountImpersonationUrl() != null; this.configLifetime = creds.getServiceAccountImpersonationOptions().customTokenLifetimeRequested; - this.credentialSourceType = creds.getCredentialSourceType(); + this.credentials = creds; } /** @@ -69,7 +69,7 @@ String getExternalAccountMetricsHeader() { MetricsUtils.getLanguageAndAuthLibraryVersions(), BYOID_METRICS_SECTION, SOURCE_KEY, - this.credentialSourceType, + this.credentials.getCredentialSourceType(), IMPERSONATION_KEY, this.saImpersonation, CONFIG_LIFETIME_KEY, diff --git a/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolSubjectTokenSupplier.java b/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolSubjectTokenSupplier.java new file mode 100644 index 000000000..e46df2d9e --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolSubjectTokenSupplier.java @@ -0,0 +1,101 @@ +/* + * Copyright 2024 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.oauth2; + +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.JsonObjectParser; +import com.google.auth.oauth2.IdentityPoolCredentialSource.CredentialFormatType; +import com.google.common.io.CharStreams; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Paths; + +/** + * Internal provider for retrieving subject tokens for {@Link IdentityPoolCredentials} to exchange + * for GCP access tokens via a local file. + */ +class FileIdentityPoolSubjectTokenSupplier implements IdentityPoolSubjectTokenSupplier { + + private final long serialVersionUID = 2475549052347431992L; + + private final IdentityPoolCredentialSource credentialSource; + + /** + * Constructor for FileIdentitySubjectTokenProvider + * + * @param credentialSource the credential source to use. + */ + FileIdentityPoolSubjectTokenSupplier(IdentityPoolCredentialSource credentialSource) { + this.credentialSource = credentialSource; + } + + @Override + public String getSubjectToken() throws IOException { + String credentialFilePath = this.credentialSource.credentialLocation; + if (!Files.exists(Paths.get(credentialFilePath), LinkOption.NOFOLLOW_LINKS)) { + throw new IOException( + String.format( + "Invalid credential location. The file at %s does not exist.", credentialFilePath)); + } + try { + return parseToken(new FileInputStream(new File(credentialFilePath)), this.credentialSource); + } catch (IOException e) { + throw new IOException( + "Error when attempting to read the subject token from the credential file.", e); + } + } + + static String parseToken(InputStream inputStream, IdentityPoolCredentialSource credentialSource) + throws IOException { + if (credentialSource.credentialFormatType == CredentialFormatType.TEXT) { + BufferedReader reader = + new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + return CharStreams.toString(reader); + } + + JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); + GenericJson fileContents = + parser.parseAndClose(inputStream, StandardCharsets.UTF_8, GenericJson.class); + + if (!fileContents.containsKey(credentialSource.subjectTokenFieldName)) { + throw new IOException("Invalid subject token field name. No subject token was found."); + } + return (String) fileContents.get(credentialSource.subjectTokenFieldName); + } +} diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java index 776a01e05..fea188ccf 100644 --- a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @@ -31,44 +31,55 @@ package com.google.auth.oauth2; -import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.HttpHeaders; -import com.google.api.client.http.HttpRequest; -import com.google.api.client.http.HttpResponse; -import com.google.api.client.json.GenericJson; -import com.google.api.client.json.JsonObjectParser; -import com.google.auth.oauth2.IdentityPoolCredentialSource.CredentialFormatType; -import com.google.auth.oauth2.IdentityPoolCredentialSource.IdentityPoolCredentialSourceType; -import com.google.common.io.CharStreams; +import com.google.auth.http.HttpTransportFactory; +import com.google.common.annotations.VisibleForTesting; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.LinkOption; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; +import java.util.Map; /** - * Url-sourced and file-sourced external account credentials. + * Url-sourced, file-sourced, or user provided supplier method-sourced external account credentials. * *

By default, attempts to exchange the external credential for a GCP access token. */ public class IdentityPoolCredentials extends ExternalAccountCredentials { + static final String FILE_METRICS_HEADER_VALUE = "file"; + static final String URL_METRICS_HEADER_VALUE = "url"; private static final long serialVersionUID = 2471046175477275881L; - private final IdentityPoolCredentialSource identityPoolCredentialSource; + private final IdentityPoolSubjectTokenSupplier subjectTokenSupplier; + private final String metricsHeaderValue; /** Internal constructor. See {@link Builder}. */ IdentityPoolCredentials(Builder builder) { super(builder); - this.identityPoolCredentialSource = (IdentityPoolCredentialSource) builder.credentialSource; + IdentityPoolCredentialSource credentialSource = + (IdentityPoolCredentialSource) builder.credentialSource; + + // Check that one and only one of supplier or credential source are provided. + if (builder.subjectTokenSupplier != null && credentialSource != null) { + throw new IllegalArgumentException( + "IdentityPoolCredentials cannot have both a subjectTokenSupplier and a credentialSource."); + } + if (builder.subjectTokenSupplier == null && credentialSource == null) { + throw new IllegalArgumentException( + "A subjectTokenSupplier or a credentialSource must be provided."); + } + if (builder.subjectTokenSupplier != null) { + this.subjectTokenSupplier = builder.subjectTokenSupplier; + this.metricsHeaderValue = PROGRAMMATIC_METRICS_HEADER_VALUE; + } else if (credentialSource.credentialSourceType + == IdentityPoolCredentialSource.IdentityPoolCredentialSourceType.FILE) { + this.subjectTokenSupplier = new FileIdentityPoolSubjectTokenSupplier(credentialSource); + this.metricsHeaderValue = FILE_METRICS_HEADER_VALUE; + } else { + this.subjectTokenSupplier = + new UrlIdentityPoolSubjectTokenSupplier(credentialSource, this.transportFactory); + this.metricsHeaderValue = URL_METRICS_HEADER_VALUE; + } } @Override @@ -88,76 +99,17 @@ public AccessToken refreshAccessToken() throws IOException { @Override public String retrieveSubjectToken() throws IOException { - if (identityPoolCredentialSource.credentialSourceType - == IdentityPoolCredentialSource.IdentityPoolCredentialSourceType.FILE) { - return retrieveSubjectTokenFromCredentialFile(); - } - return getSubjectTokenFromMetadataServer(); + return this.subjectTokenSupplier.getSubjectToken(); } @Override String getCredentialSourceType() { - if (((IdentityPoolCredentialSource) this.getCredentialSource()).credentialSourceType - == IdentityPoolCredentialSourceType.FILE) { - return "file"; - } else { - return "url"; - } + return this.metricsHeaderValue; } - private String retrieveSubjectTokenFromCredentialFile() throws IOException { - String credentialFilePath = identityPoolCredentialSource.credentialLocation; - if (!Files.exists(Paths.get(credentialFilePath), LinkOption.NOFOLLOW_LINKS)) { - throw new IOException( - String.format( - "Invalid credential location. The file at %s does not exist.", credentialFilePath)); - } - try { - return parseToken(new FileInputStream(new File(credentialFilePath))); - } catch (IOException e) { - throw new IOException( - "Error when attempting to read the subject token from the credential file.", e); - } - } - - private String parseToken(InputStream inputStream) throws IOException { - if (identityPoolCredentialSource.credentialFormatType == CredentialFormatType.TEXT) { - BufferedReader reader = - new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); - return CharStreams.toString(reader); - } - - JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); - GenericJson fileContents = - parser.parseAndClose(inputStream, StandardCharsets.UTF_8, GenericJson.class); - - if (!fileContents.containsKey(identityPoolCredentialSource.subjectTokenFieldName)) { - throw new IOException("Invalid subject token field name. No subject token was found."); - } - return (String) fileContents.get(identityPoolCredentialSource.subjectTokenFieldName); - } - - private String getSubjectTokenFromMetadataServer() throws IOException { - HttpRequest request = - transportFactory - .create() - .createRequestFactory() - .buildGetRequest(new GenericUrl(identityPoolCredentialSource.credentialLocation)); - request.setParser(new JsonObjectParser(OAuth2Utils.JSON_FACTORY)); - - if (identityPoolCredentialSource.hasHeaders()) { - HttpHeaders headers = new HttpHeaders(); - headers.putAll(identityPoolCredentialSource.headers); - request.setHeaders(headers); - } - - try { - HttpResponse response = request.execute(); - return parseToken(response.getContent()); - } catch (IOException e) { - throw new IOException( - String.format("Error getting subject token from metadata server: %s", e.getMessage()), e); - } + @VisibleForTesting + IdentityPoolSubjectTokenSupplier getIdentityPoolSubjectTokenSupplier() { + return this.subjectTokenSupplier; } /** Clones the IdentityPoolCredentials with the specified scopes. */ @@ -177,10 +129,99 @@ public static Builder newBuilder(IdentityPoolCredentials identityPoolCredentials public static class Builder extends ExternalAccountCredentials.Builder { + private IdentityPoolSubjectTokenSupplier subjectTokenSupplier; + Builder() {} Builder(IdentityPoolCredentials credentials) { super(credentials); + if (this.credentialSource == null) { + this.subjectTokenSupplier = credentials.subjectTokenSupplier; + } + } + + /** + * Sets the subject token supplier. The supplier should return a valid subject token string. + * + * @param subjectTokenSupplier the supplier to use. + * @return this {@code Builder} object + */ + @CanIgnoreReturnValue + public Builder setSubjectTokenSupplier(IdentityPoolSubjectTokenSupplier subjectTokenSupplier) { + this.subjectTokenSupplier = subjectTokenSupplier; + return this; + } + + @CanIgnoreReturnValue + public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) { + super.setHttpTransportFactory(transportFactory); + return this; + } + + @CanIgnoreReturnValue + public Builder setAudience(String audience) { + super.setAudience(audience); + return this; + } + + @CanIgnoreReturnValue + public Builder setSubjectTokenType(String subjectTokenType) { + super.setSubjectTokenType(subjectTokenType); + return this; + } + + @CanIgnoreReturnValue + public Builder setSubjectTokenType(SubjectTokenTypes subjectTokenType) { + super.setSubjectTokenType(subjectTokenType); + return this; + } + + @CanIgnoreReturnValue + public Builder setTokenUrl(String tokenUrl) { + super.setTokenUrl(tokenUrl); + return this; + } + + @CanIgnoreReturnValue + public Builder setCredentialSource(IdentityPoolCredentialSource credentialSource) { + super.setCredentialSource(credentialSource); + return this; + } + + @CanIgnoreReturnValue + public Builder setServiceAccountImpersonationUrl(String serviceAccountImpersonationUrl) { + super.setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl); + return this; + } + + @CanIgnoreReturnValue + public Builder setTokenInfoUrl(String tokenInfoUrl) { + super.setTokenInfoUrl(tokenInfoUrl); + return this; + } + + @CanIgnoreReturnValue + public Builder setQuotaProjectId(String quotaProjectId) { + super.setQuotaProjectId(quotaProjectId); + return this; + } + + @CanIgnoreReturnValue + public Builder setClientId(String clientId) { + super.setClientId(clientId); + return this; + } + + @CanIgnoreReturnValue + public Builder setClientSecret(String clientSecret) { + super.setClientSecret(clientSecret); + return this; + } + + @CanIgnoreReturnValue + public Builder setScopes(Collection scopes) { + super.setScopes(scopes); + return this; } @Override @@ -190,6 +231,24 @@ public Builder setWorkforcePoolUserProject(String workforcePoolUserProject) { return this; } + @CanIgnoreReturnValue + public Builder setServiceAccountImpersonationOptions(Map optionsMap) { + super.setServiceAccountImpersonationOptions(optionsMap); + return this; + } + + @CanIgnoreReturnValue + public Builder setUniverseDomain(String universeDomain) { + super.setUniverseDomain(universeDomain); + return this; + } + + @CanIgnoreReturnValue + Builder setEnvironmentProvider(EnvironmentProvider environmentProvider) { + super.setEnvironmentProvider(environmentProvider); + return this; + } + @Override public IdentityPoolCredentials build() { return new IdentityPoolCredentials(this); diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolSubjectTokenSupplier.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolSubjectTokenSupplier.java new file mode 100644 index 000000000..a057bba48 --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolSubjectTokenSupplier.java @@ -0,0 +1,51 @@ +/* + * Copyright 2024 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.oauth2; + +import java.io.IOException; +import java.io.Serializable; + +@FunctionalInterface +/** + * Provider for retrieving subject tokens for {@Link IdentityPoolCredentials} to exchange for GCP + * access tokens. + */ +public interface IdentityPoolSubjectTokenSupplier extends Serializable { + + /** + * Gets a subject token that can be exchanged for a GCP access token. + * + * @return a valid subject token. + * @throws IOException + */ + String getSubjectToken() throws IOException; +} diff --git a/oauth2_http/java/com/google/auth/oauth2/InternalAwsSecurityCredentialsSupplier.java b/oauth2_http/java/com/google/auth/oauth2/InternalAwsSecurityCredentialsSupplier.java new file mode 100644 index 000000000..308717470 --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/InternalAwsSecurityCredentialsSupplier.java @@ -0,0 +1,253 @@ +/* + * Copyright 2024 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.oauth2; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpContent; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpMethods; +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.json.GenericJson; +import com.google.api.client.json.JsonParser; +import com.google.auth.http.HttpTransportFactory; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * Internal provider for retrieving AWS security credentials for {@Link AwsCredentials} to exchange + * for GCP access tokens. The credentials are retrieved either via environment variables or metadata + * endpoints. + */ +class InternalAwsSecurityCredentialsSupplier implements AwsSecurityCredentialsSupplier { + private static final long serialVersionUID = 4438370785261365013L; + + // Supported environment variables. + static final String AWS_REGION = "AWS_REGION"; + static final String AWS_DEFAULT_REGION = "AWS_DEFAULT_REGION"; + static final String AWS_ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID"; + static final String AWS_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY"; + static final String AWS_SESSION_TOKEN = "AWS_SESSION_TOKEN"; + + static final String AWS_IMDSV2_SESSION_TOKEN_HEADER = "x-aws-ec2-metadata-token"; + static final String AWS_IMDSV2_SESSION_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds"; + static final String AWS_IMDSV2_SESSION_TOKEN_TTL = "300"; + + private final AwsCredentialSource awsCredentialSource; + private EnvironmentProvider environmentProvider; + private transient HttpTransportFactory transportFactory; + + /** + * Constructor for InternalAwsSecurityCredentialsProvider + * + * @param awsCredentialSource the credential source to use. + * @param environmentProvider the environment provider to use for environment variables. + * @param transportFactory the transport factory to use for metadata requests. + */ + InternalAwsSecurityCredentialsSupplier( + AwsCredentialSource awsCredentialSource, + EnvironmentProvider environmentProvider, + HttpTransportFactory transportFactory) { + this.environmentProvider = environmentProvider; + this.awsCredentialSource = awsCredentialSource; + this.transportFactory = transportFactory; + } + + @Override + public AwsSecurityCredentials getCredentials() throws IOException { + // Check environment variables for credentials first. + if (canRetrieveSecurityCredentialsFromEnvironment()) { + String accessKeyId = environmentProvider.getEnv(AWS_ACCESS_KEY_ID); + String secretAccessKey = environmentProvider.getEnv(AWS_SECRET_ACCESS_KEY); + String token = environmentProvider.getEnv(AWS_SESSION_TOKEN); + return new AwsSecurityCredentials(accessKeyId, secretAccessKey, token); + } + + Map metadataRequestHeaders = createMetadataRequestHeaders(awsCredentialSource); + + // Credentials not retrievable from environment variables - call metadata server. + // Retrieve the IAM role that is attached to the VM. This is required to retrieve the AWS + // security credentials. + if (awsCredentialSource.url == null || awsCredentialSource.url.isEmpty()) { + throw new IOException( + "Unable to determine the AWS IAM role name. The credential source does not contain the" + + " url field."); + } + String roleName = retrieveResource(awsCredentialSource.url, "IAM role", metadataRequestHeaders); + + // Retrieve the AWS security credentials by calling the endpoint specified by the credential + // source. + String awsCredentials = + retrieveResource( + awsCredentialSource.url + "/" + roleName, "credentials", metadataRequestHeaders); + + JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser(awsCredentials); + GenericJson genericJson = parser.parseAndClose(GenericJson.class); + + String accessKeyId = (String) genericJson.get("AccessKeyId"); + String secretAccessKey = (String) genericJson.get("SecretAccessKey"); + String token = (String) genericJson.get("Token"); + + // These credentials last for a few hours - we may consider caching these in the + // future. + return new AwsSecurityCredentials(accessKeyId, secretAccessKey, token); + } + + @Override + public String getRegion() throws IOException { + String region; + if (canRetrieveRegionFromEnvironment()) { + // For AWS Lambda, the region is retrieved through the AWS_REGION environment variable. + region = environmentProvider.getEnv(AWS_REGION); + if (region != null && region.trim().length() > 0) { + return region; + } + return environmentProvider.getEnv(AWS_DEFAULT_REGION); + } + + Map metadataRequestHeaders = createMetadataRequestHeaders(awsCredentialSource); + + if (awsCredentialSource.regionUrl == null || awsCredentialSource.regionUrl.isEmpty()) { + throw new IOException( + "Unable to determine the AWS region. The credential source does not contain the region URL."); + } + + region = retrieveResource(awsCredentialSource.regionUrl, "region", metadataRequestHeaders); + + // There is an extra appended character that must be removed. If `us-east-1b` is returned, + // we want `us-east-1`. + return region.substring(0, region.length() - 1); + } + + private boolean canRetrieveRegionFromEnvironment() { + // The AWS region can be provided through AWS_REGION or AWS_DEFAULT_REGION. Only one is + // required. + List keys = ImmutableList.of(AWS_REGION, AWS_DEFAULT_REGION); + for (String env : keys) { + String value = environmentProvider.getEnv(env); + if (value != null && value.trim().length() > 0) { + // Region available. + return true; + } + } + return false; + } + + private boolean canRetrieveSecurityCredentialsFromEnvironment() { + // Check if both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are available. + List keys = ImmutableList.of(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY); + for (String env : keys) { + String value = environmentProvider.getEnv(env); + if (value == null || value.trim().length() == 0) { + // Return false if one of them are missing. + return false; + } + } + return true; + } + + @VisibleForTesting + boolean shouldUseMetadataServer() { + return (!canRetrieveRegionFromEnvironment() + || !canRetrieveSecurityCredentialsFromEnvironment()); + } + + private String retrieveResource(String url, String resourceName, Map headers) + throws IOException { + return retrieveResource(url, resourceName, HttpMethods.GET, headers, /* content= */ null); + } + + private String retrieveResource( + String url, + String resourceName, + String requestMethod, + Map headers, + @Nullable HttpContent content) + throws IOException { + try { + HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory(); + HttpRequest request = + requestFactory.buildRequest(requestMethod, new GenericUrl(url), content); + + HttpHeaders requestHeaders = request.getHeaders(); + for (Map.Entry header : headers.entrySet()) { + requestHeaders.set(header.getKey(), header.getValue()); + } + + HttpResponse response = request.execute(); + return response.parseAsString(); + } catch (IOException e) { + throw new IOException(String.format("Failed to retrieve AWS %s.", resourceName), e); + } + } + + @VisibleForTesting + Map createMetadataRequestHeaders(AwsCredentialSource awsCredentialSource) + throws IOException { + Map metadataRequestHeaders = new HashMap<>(); + + // AWS IDMSv2 introduced a requirement for a session token to be present + // with the requests made to metadata endpoints. This requirement is to help + // prevent SSRF attacks. + // Presence of "imdsv2_session_token_url" in Credential Source of config file + // will trigger a flow with session token, else there will not be a session + // token with the metadata requests. + // Both flows work for IDMS v1 and v2. But if IDMSv2 is enabled, then if + // session token is not present, Unauthorized exception will be thrown. + if (awsCredentialSource.imdsv2SessionTokenUrl != null) { + Map tokenRequestHeaders = + new HashMap() { + { + put(AWS_IMDSV2_SESSION_TOKEN_TTL_HEADER, AWS_IMDSV2_SESSION_TOKEN_TTL); + } + }; + + String imdsv2SessionToken = + retrieveResource( + awsCredentialSource.imdsv2SessionTokenUrl, + "Session Token", + HttpMethods.PUT, + tokenRequestHeaders, + /* content= */ null); + + metadataRequestHeaders.put(AWS_IMDSV2_SESSION_TOKEN_HEADER, imdsv2SessionToken); + } + + return metadataRequestHeaders; + } +} diff --git a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java index 5caa567c1..e984c8173 100644 --- a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java @@ -31,6 +31,7 @@ package com.google.auth.oauth2; +import com.google.auth.http.HttpTransportFactory; import com.google.auth.oauth2.ExecutableHandler.ExecutableOptions; import com.google.common.annotations.VisibleForTesting; import com.google.errorprone.annotations.CanIgnoreReturnValue; @@ -97,6 +98,9 @@ *

Please see this repositories README for a complete executable request/response specification. */ public class PluggableAuthCredentials extends ExternalAccountCredentials { + + static final String PLUGGABLE_AUTH_METRICS_HEADER_VALUE = "executable"; + private final PluggableAuthCredentialSource config; private final ExecutableHandler handler; @@ -111,10 +115,6 @@ public class PluggableAuthCredentials extends ExternalAccountCredentials { } else { handler = new PluggableAuthHandler(getEnvironmentProvider()); } - - // Re-initialize impersonated credentials as the handler hasn't been set yet when - // this is called in the base class. - overrideImpersonatedCredentials(buildImpersonatedCredentials()); } @Override @@ -192,7 +192,7 @@ public PluggableAuthCredentials createScoped(Collection newScopes) { @Override String getCredentialSourceType() { - return "executable"; + return PLUGGABLE_AUTH_METRICS_HEADER_VALUE; } public static Builder newBuilder() { @@ -226,6 +226,102 @@ public Builder setExecutableHandler(ExecutableHandler handler) { return this; } + @CanIgnoreReturnValue + public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) { + super.setHttpTransportFactory(transportFactory); + return this; + } + + @CanIgnoreReturnValue + public Builder setAudience(String audience) { + super.setAudience(audience); + return this; + } + + @CanIgnoreReturnValue + public Builder setSubjectTokenType(String subjectTokenType) { + super.setSubjectTokenType(subjectTokenType); + return this; + } + + @CanIgnoreReturnValue + public Builder setSubjectTokenType(SubjectTokenTypes subjectTokenType) { + super.setSubjectTokenType(subjectTokenType); + return this; + } + + @CanIgnoreReturnValue + public Builder setTokenUrl(String tokenUrl) { + super.setTokenUrl(tokenUrl); + return this; + } + + @CanIgnoreReturnValue + public Builder setCredentialSource(PluggableAuthCredentialSource credentialSource) { + super.setCredentialSource(credentialSource); + return this; + } + + @CanIgnoreReturnValue + public Builder setServiceAccountImpersonationUrl(String serviceAccountImpersonationUrl) { + super.setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl); + return this; + } + + @CanIgnoreReturnValue + public Builder setTokenInfoUrl(String tokenInfoUrl) { + super.setTokenInfoUrl(tokenInfoUrl); + return this; + } + + @CanIgnoreReturnValue + public Builder setQuotaProjectId(String quotaProjectId) { + super.setQuotaProjectId(quotaProjectId); + return this; + } + + @CanIgnoreReturnValue + public Builder setClientId(String clientId) { + super.setClientId(clientId); + return this; + } + + @CanIgnoreReturnValue + public Builder setClientSecret(String clientSecret) { + super.setClientSecret(clientSecret); + return this; + } + + @CanIgnoreReturnValue + public Builder setScopes(Collection scopes) { + super.setScopes(scopes); + return this; + } + + @CanIgnoreReturnValue + public Builder setWorkforcePoolUserProject(String workforcePoolUserProject) { + super.setWorkforcePoolUserProject(workforcePoolUserProject); + return this; + } + + @CanIgnoreReturnValue + public Builder setServiceAccountImpersonationOptions(Map optionsMap) { + super.setServiceAccountImpersonationOptions(optionsMap); + return this; + } + + @CanIgnoreReturnValue + public Builder setUniverseDomain(String universeDomain) { + super.setUniverseDomain(universeDomain); + return this; + } + + @CanIgnoreReturnValue + Builder setEnvironmentProvider(EnvironmentProvider environmentProvider) { + super.setEnvironmentProvider(environmentProvider); + return this; + } + @Override public PluggableAuthCredentials build() { return new PluggableAuthCredentials(this); diff --git a/oauth2_http/java/com/google/auth/oauth2/UrlIdentityPoolSubjectTokenSupplier.java b/oauth2_http/java/com/google/auth/oauth2/UrlIdentityPoolSubjectTokenSupplier.java new file mode 100644 index 000000000..f886bfdd0 --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/UrlIdentityPoolSubjectTokenSupplier.java @@ -0,0 +1,90 @@ +/* + * Copyright 2024 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.oauth2; + +import static com.google.auth.oauth2.FileIdentityPoolSubjectTokenSupplier.parseToken; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.json.JsonObjectParser; +import com.google.auth.http.HttpTransportFactory; +import java.io.IOException; + +/** + * Provider for retrieving subject tokens for {@Link IdentityPoolCredentials} to exchange for GCP + * access tokens. The subject token is retrieved by calling a URL that returns the token. + */ +class UrlIdentityPoolSubjectTokenSupplier implements IdentityPoolSubjectTokenSupplier { + + private static final long serialVersionUID = 4964578313468011844L; + + private final IdentityPoolCredentialSource credentialSource; + private final transient HttpTransportFactory transportFactory; + + /** + * Constructor for UrlIdentityPoolSubjectTokenProvider. + * + * @param credentialSource the credential source to use. + * @param transportFactory the transport factory to use for calling the URL. + */ + UrlIdentityPoolSubjectTokenSupplier( + IdentityPoolCredentialSource credentialSource, HttpTransportFactory transportFactory) { + this.credentialSource = credentialSource; + this.transportFactory = transportFactory; + } + + @Override + public String getSubjectToken() throws IOException { + HttpRequest request = + transportFactory + .create() + .createRequestFactory() + .buildGetRequest(new GenericUrl(credentialSource.credentialLocation)); + request.setParser(new JsonObjectParser(OAuth2Utils.JSON_FACTORY)); + + if (credentialSource.hasHeaders()) { + HttpHeaders headers = new HttpHeaders(); + headers.putAll(credentialSource.headers); + request.setHeaders(headers); + } + + try { + HttpResponse response = request.execute(); + return parseToken(response.getContent(), this.credentialSource); + } catch (IOException e) { + throw new IOException( + String.format("Error getting subject token from metadata server: %s", e.getMessage()), e); + } + } +} diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java index 94dbf5845..aea3f2906 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java @@ -45,7 +45,6 @@ import com.google.api.client.util.Clock; import com.google.auth.TestUtils; import com.google.auth.oauth2.ExternalAccountCredentialsTest.MockExternalAccountCredentialsTransportFactory; -import com.google.common.collect.ImmutableList; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -55,6 +54,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Supplier; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -63,12 +63,14 @@ @RunWith(JUnit4.class) public class AwsCredentialsTest extends BaseSerializationTest { - private static final String STS_URL = "https://sts.googleapis.com"; + private static final String STS_URL = "https://sts.googleapis.com/v1/token"; private static final String AWS_CREDENTIALS_URL = "https://169.254.169.254"; private static final String AWS_CREDENTIALS_URL_WITH_ROLE = "https://169.254.169.254/roleName"; private static final String AWS_REGION_URL = "https://169.254.169.254/region"; private static final String AWS_IMDSV2_SESSION_TOKEN_URL = "https://169.254.169.254/imdsv2"; private static final String AWS_IMDSV2_SESSION_TOKEN = "sessiontoken"; + private static final String DEFAULT_REGIONAL_CREDENTIAL_VERIFICATION_URL = + "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"; private static final String GET_CALLER_IDENTITY_URL = "https://sts.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"; @@ -93,15 +95,17 @@ public class AwsCredentialsTest extends BaseSerializationTest { new AwsCredentialSource(AWS_CREDENTIAL_SOURCE_MAP); private static final AwsCredentials AWS_CREDENTIAL = - (AwsCredentials) - AwsCredentials.newBuilder() - .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) - .setAudience("audience") - .setSubjectTokenType("subjectTokenType") - .setTokenUrl(STS_URL) - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(AWS_CREDENTIAL_SOURCE) - .build(); + AwsCredentials.newBuilder() + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(AWS_CREDENTIAL_SOURCE) + .build(); + + private static final AwsSecurityCredentials programmaticAwsCreds = + new AwsSecurityCredentials("testAccessKey", "testSecretAccessKey", null); @Test public void test_awsCredentialSource() { @@ -121,12 +125,11 @@ public void refreshAccessToken_withoutServiceAccountImpersonation() throws IOExc new MockExternalAccountCredentialsTransportFactory(); AwsCredentials awsCredential = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setTokenUrl(transportFactory.transport.getStsUrl()) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(buildAwsCredentialSource(transportFactory)) - .build(); + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setHttpTransportFactory(transportFactory) + .setCredentialSource(buildAwsCredentialSource(transportFactory)) + .build(); AccessToken accessToken = awsCredential.refreshAccessToken(); @@ -146,17 +149,16 @@ public void refreshAccessToken_withServiceAccountImpersonation() throws IOExcept transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); AwsCredentials awsCredential = - (AwsCredentials) - AwsCredentials.newBuilder() - .setHttpTransportFactory(transportFactory) - .setAudience("audience") - .setSubjectTokenType("subjectTokenType") - .setTokenUrl(transportFactory.transport.getStsUrl()) - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(buildAwsCredentialSource(transportFactory)) - .setServiceAccountImpersonationUrl( - transportFactory.transport.getServiceAccountImpersonationUrl()) - .build(); + AwsCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(buildAwsCredentialSource(transportFactory)) + .setServiceAccountImpersonationUrl( + transportFactory.transport.getServiceAccountImpersonationUrl()) + .build(); AccessToken accessToken = awsCredential.refreshAccessToken(); @@ -177,19 +179,18 @@ public void refreshAccessToken_withServiceAccountImpersonationOptions() throws I transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); AwsCredentials awsCredential = - (AwsCredentials) - AwsCredentials.newBuilder() - .setHttpTransportFactory(transportFactory) - .setAudience("audience") - .setSubjectTokenType("subjectTokenType") - .setTokenUrl(transportFactory.transport.getStsUrl()) - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(buildAwsCredentialSource(transportFactory)) - .setServiceAccountImpersonationUrl( - transportFactory.transport.getServiceAccountImpersonationUrl()) - .setServiceAccountImpersonationOptions( - ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions(2800)) - .build(); + AwsCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(buildAwsCredentialSource(transportFactory)) + .setServiceAccountImpersonationUrl( + transportFactory.transport.getServiceAccountImpersonationUrl()) + .setServiceAccountImpersonationOptions( + ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions(2800)) + .build(); AccessToken accessToken = awsCredential.refreshAccessToken(); @@ -210,6 +211,67 @@ public void refreshAccessToken_withServiceAccountImpersonationOptions() throws I ExternalAccountCredentialsTest.validateMetricsHeader(headers, "aws", true, true); } + @Test + public void refreshAccessTokenProgrammaticRefresh_withoutServiceAccountImpersonation() + throws IOException { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + AwsSecurityCredentialsSupplier supplier = + new TestAwsSecurityCredentialsSupplier("test", programmaticAwsCreds, null); + + AwsCredentials awsCredential = + AwsCredentials.newBuilder() + .setAwsSecurityCredentialsSupplier(supplier) + .setHttpTransportFactory(transportFactory) + .setAudience("audience") + .setTokenUrl(STS_URL) + .setSubjectTokenType("subjectTokenType") + .build(); + + AccessToken accessToken = awsCredential.refreshAccessToken(); + + assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue()); + + // Validate metrics header is set correctly on the sts request. + Map> headers = + transportFactory.transport.getRequests().get(0).getHeaders(); + ExternalAccountCredentialsTest.validateMetricsHeader(headers, "programmatic", false, false); + } + + @Test + public void refreshAccessTokenProgrammaticRefresh_withServiceAccountImpersonation() + throws IOException { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); + + AwsSecurityCredentialsSupplier supplier = + new TestAwsSecurityCredentialsSupplier("test", programmaticAwsCreds, null); + + AwsCredentials awsCredential = + AwsCredentials.newBuilder() + .setAwsSecurityCredentialsSupplier(supplier) + .setHttpTransportFactory(transportFactory) + .setAudience("audience") + .setTokenUrl(STS_URL) + .setSubjectTokenType("subjectTokenType") + .setServiceAccountImpersonationUrl( + transportFactory.transport.getServiceAccountImpersonationUrl()) + .build(); + + AccessToken accessToken = awsCredential.refreshAccessToken(); + + assertEquals( + transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue()); + + // Validate metrics header is set correctly on the sts request. + Map> headers = + transportFactory.transport.getRequests().get(0).getHeaders(); + ExternalAccountCredentialsTest.validateMetricsHeader(headers, "programmatic", true, false); + } + @Test @SuppressWarnings("unchecked") public void retrieveSubjectToken() throws IOException { @@ -217,11 +279,10 @@ public void retrieveSubjectToken() throws IOException { new MockExternalAccountCredentialsTransportFactory(); AwsCredentials awsCredential = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(buildAwsCredentialSource(transportFactory)) - .build(); + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setHttpTransportFactory(transportFactory) + .setCredentialSource(buildAwsCredentialSource(transportFactory)) + .build(); String subjectToken = URLDecoder.decode(awsCredential.retrieveSubjectToken(), "UTF-8"); @@ -262,11 +323,10 @@ public void retrieveSubjectTokenWithSessionTokenUrl() throws IOException { new MockExternalAccountCredentialsTransportFactory(); AwsCredentials awsCredential = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(buildAwsImdsv2CredentialSource(transportFactory)) - .build(); + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setHttpTransportFactory(transportFactory) + .setCredentialSource(buildAwsImdsv2CredentialSource(transportFactory)) + .build(); String subjectToken = URLDecoder.decode(awsCredential.retrieveSubjectToken(), "UTF-8"); @@ -288,7 +348,7 @@ public void retrieveSubjectTokenWithSessionTokenUrl() throws IOException { assertNotNull(headers.get("Authorization")); List requests = transportFactory.transport.getRequests(); - assertEquals(4, requests.size()); + assertEquals(5, requests.size()); // Validate the session token request ValidateRequest( @@ -297,15 +357,17 @@ public void retrieveSubjectTokenWithSessionTokenUrl() throws IOException { new HashMap() { { put( - AwsCredentials.AWS_IMDSV2_SESSION_TOKEN_TTL_HEADER, - AwsCredentials.AWS_IMDSV2_SESSION_TOKEN_TTL); + InternalAwsSecurityCredentialsSupplier.AWS_IMDSV2_SESSION_TOKEN_TTL_HEADER, + InternalAwsSecurityCredentialsSupplier.AWS_IMDSV2_SESSION_TOKEN_TTL); } }); Map sessionTokenHeader = new HashMap() { { - put(AwsCredentials.AWS_IMDSV2_SESSION_TOKEN_HEADER, AWS_IMDSV2_SESSION_TOKEN); + put( + InternalAwsSecurityCredentialsSupplier.AWS_IMDSV2_SESSION_TOKEN_HEADER, + AWS_IMDSV2_SESSION_TOKEN); } }; @@ -313,10 +375,10 @@ public void retrieveSubjectTokenWithSessionTokenUrl() throws IOException { ValidateRequest(requests.get(1), AWS_REGION_URL, sessionTokenHeader); // Validate role request. - ValidateRequest(requests.get(2), AWS_CREDENTIALS_URL, sessionTokenHeader); + ValidateRequest(requests.get(3), AWS_CREDENTIALS_URL, sessionTokenHeader); // Validate security credentials request. - ValidateRequest(requests.get(3), AWS_CREDENTIALS_URL_WITH_ROLE, sessionTokenHeader); + ValidateRequest(requests.get(4), AWS_CREDENTIALS_URL_WITH_ROLE, sessionTokenHeader); } @Test @@ -334,12 +396,11 @@ public void retrieveSubjectToken_imdsv1EnvVariablesSet_metadataServerNotCalled() .setEnv("AWS_SESSION_TOKEN", "awsToken"); AwsCredentials awsCredential = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(buildAwsCredentialSource(transportFactory)) - .setEnvironmentProvider(environmentProvider) - .build(); + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setHttpTransportFactory(transportFactory) + .setCredentialSource(buildAwsCredentialSource(transportFactory)) + .setEnvironmentProvider(environmentProvider) + .build(); String subjectToken = URLDecoder.decode(awsCredential.retrieveSubjectToken(), "UTF-8"); @@ -381,12 +442,11 @@ public void retrieveSubjectToken_imdsv2EnvVariablesSet_metadataServerNotCalled() .setEnv("AWS_SESSION_TOKEN", "awsToken"); AwsCredentials awsCredential = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(buildAwsImdsv2CredentialSource(transportFactory)) - .setEnvironmentProvider(environmentProvider) - .build(); + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setHttpTransportFactory(transportFactory) + .setCredentialSource(buildAwsImdsv2CredentialSource(transportFactory)) + .setEnvironmentProvider(environmentProvider) + .build(); String subjectToken = URLDecoder.decode(awsCredential.retrieveSubjectToken(), "UTF-8"); @@ -422,11 +482,10 @@ public void retrieveSubjectToken_noRegion_expectThrows() { transportFactory.transport.addResponseErrorSequence(response); AwsCredentials awsCredential = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(buildAwsCredentialSource(transportFactory)) - .build(); + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setHttpTransportFactory(transportFactory) + .setCredentialSource(buildAwsCredentialSource(transportFactory)) + .build(); try { awsCredential.retrieveSubjectToken(); @@ -452,11 +511,10 @@ public void retrieveSubjectToken_noRole_expectThrows() { transportFactory.transport.addResponseSequence(true, false); AwsCredentials awsCredential = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(buildAwsCredentialSource(transportFactory)) - .build(); + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setHttpTransportFactory(transportFactory) + .setCredentialSource(buildAwsCredentialSource(transportFactory)) + .build(); try { awsCredential.retrieveSubjectToken(); @@ -485,11 +543,10 @@ public void retrieveSubjectToken_noCredentials_expectThrows() { transportFactory.transport.addResponseSequence(true, true, false); AwsCredentials awsCredential = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(buildAwsCredentialSource(transportFactory)) - .build(); + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setHttpTransportFactory(transportFactory) + .setCredentialSource(buildAwsCredentialSource(transportFactory)) + .build(); try { awsCredential.retrieveSubjectToken(); @@ -521,11 +578,10 @@ public void retrieveSubjectToken_noRegionUrlProvided() { credentialSource.put("regional_cred_verification_url", GET_CALLER_IDENTITY_URL); AwsCredentials awsCredential = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(new AwsCredentialSource(credentialSource)) - .build(); + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setHttpTransportFactory(transportFactory) + .setCredentialSource(new AwsCredentialSource(credentialSource)) + .build(); try { awsCredential.retrieveSubjectToken(); @@ -542,6 +598,115 @@ public void retrieveSubjectToken_noRegionUrlProvided() { assertTrue(requests.isEmpty()); } + @Test + public void retrieveSubjectToken_withProgrammaticRefresh() throws IOException { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + AwsSecurityCredentialsSupplier supplier = + new TestAwsSecurityCredentialsSupplier("test", programmaticAwsCreds, null); + + AwsCredentials awsCredential = + AwsCredentials.newBuilder() + .setAwsSecurityCredentialsSupplier(supplier) + .setHttpTransportFactory(transportFactory) + .setAudience("audience") + .setTokenUrl(STS_URL) + .setSubjectTokenType("subjectTokenType") + .build(); + + String subjectToken = URLDecoder.decode(awsCredential.retrieveSubjectToken(), "UTF-8"); + + JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser(subjectToken); + GenericJson json = parser.parseAndClose(GenericJson.class); + + List> headersList = (List>) json.get("headers"); + Map headers = new HashMap<>(); + for (Map header : headersList) { + headers.put(header.get("key"), header.get("value")); + } + + String expectedCredentialVerificationUrl = + DEFAULT_REGIONAL_CREDENTIAL_VERIFICATION_URL.replace("{region}", "test"); + + assertEquals("POST", json.get("method")); + assertEquals(expectedCredentialVerificationUrl, json.get("url")); + assertEquals(URI.create(expectedCredentialVerificationUrl).getHost(), headers.get("host")); + assertEquals(awsCredential.getAudience(), headers.get("x-goog-cloud-target-resource")); + assertTrue(headers.containsKey("x-amz-date")); + assertNotNull(headers.get("Authorization")); + } + + @Test + public void retrieveSubjectToken_withProgrammaticRefreshSessionToken() throws IOException { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + AwsSecurityCredentials securityCredentialsWithToken = + new AwsSecurityCredentials("accessToken", "secretAccessKey", "token"); + + AwsSecurityCredentialsSupplier supplier = + new TestAwsSecurityCredentialsSupplier("test", securityCredentialsWithToken, null); + + AwsCredentials awsCredential = + AwsCredentials.newBuilder() + .setAwsSecurityCredentialsSupplier(supplier) + .setHttpTransportFactory(transportFactory) + .setAudience("audience") + .setTokenUrl(STS_URL) + .setSubjectTokenType("subjectTokenType") + .build(); + + String subjectToken = URLDecoder.decode(awsCredential.retrieveSubjectToken(), "UTF-8"); + + JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser(subjectToken); + GenericJson json = parser.parseAndClose(GenericJson.class); + + List> headersList = (List>) json.get("headers"); + Map headers = new HashMap<>(); + for (Map header : headersList) { + headers.put(header.get("key"), header.get("value")); + } + + String expectedCredentialVerificationUrl = + DEFAULT_REGIONAL_CREDENTIAL_VERIFICATION_URL.replace("{region}", "test"); + + assertEquals("POST", json.get("method")); + assertEquals(expectedCredentialVerificationUrl, json.get("url")); + assertEquals(URI.create(expectedCredentialVerificationUrl).getHost(), headers.get("host")); + assertEquals("token", headers.get("x-amz-security-token")); + assertEquals(awsCredential.getAudience(), headers.get("x-goog-cloud-target-resource")); + assertTrue(headers.containsKey("x-amz-date")); + assertNotNull(headers.get("Authorization")); + } + + @Test + public void retrieveSubjectToken_withProgrammaticRefreshThrowsError() throws IOException { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + IOException testException = new IOException("test"); + + AwsSecurityCredentialsSupplier supplier = + new TestAwsSecurityCredentialsSupplier("test", null, testException); + + AwsCredentials awsCredential = + AwsCredentials.newBuilder() + .setAwsSecurityCredentialsSupplier(supplier) + .setHttpTransportFactory(transportFactory) + .setAudience("audience") + .setTokenUrl(STS_URL) + .setSubjectTokenType("subjectTokenType") + .build(); + + try { + String subjectToken = URLDecoder.decode(awsCredential.retrieveSubjectToken(), "UTF-8"); + fail("retrieveSubjectToken should not succeed"); + } catch (IOException e) { + assertEquals("test", e.getMessage()); + } + } + @Test public void getAwsSecurityCredentials_fromEnvironmentVariablesNoToken() throws IOException { TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider(); @@ -550,17 +715,16 @@ public void getAwsSecurityCredentials_fromEnvironmentVariablesNoToken() throws I .setEnv("AWS_SECRET_ACCESS_KEY", "awsSecretAccessKey"); AwsCredentials testAwsCredentials = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setEnvironmentProvider(environmentProvider) - .build(); + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setEnvironmentProvider(environmentProvider) + .build(); AwsSecurityCredentials credentials = - testAwsCredentials.getAwsSecurityCredentials(EMPTY_METADATA_HEADERS); + testAwsCredentials.getAwsSecurityCredentialsSupplier().getCredentials(); assertEquals("awsAccessKeyId", credentials.getAccessKeyId()); assertEquals("awsSecretAccessKey", credentials.getSecretAccessKey()); - assertNull(credentials.getToken()); + assertNull(credentials.getSessionToken()); } @Test @@ -583,18 +747,17 @@ public void getAwsSecurityCredentials_fromEnvironmentVariablesWithToken() throws }); AwsCredentials testAwsCredentials = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setEnvironmentProvider(environmentProvider) - .setCredentialSource(credSource) - .build(); + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setEnvironmentProvider(environmentProvider) + .setCredentialSource(credSource) + .build(); AwsSecurityCredentials credentials = - testAwsCredentials.getAwsSecurityCredentials(EMPTY_METADATA_HEADERS); + testAwsCredentials.getAwsSecurityCredentialsSupplier().getCredentials(); assertEquals("awsAccessKeyId", credentials.getAccessKeyId()); assertEquals("awsSecretAccessKey", credentials.getSecretAccessKey()); - assertEquals("awsSessionToken", credentials.getToken()); + assertEquals("awsSessionToken", credentials.getSessionToken()); } @Test @@ -607,17 +770,16 @@ public void getAwsSecurityCredentials_fromEnvironmentVariables_noMetadataServerC .setEnv("AWS_SESSION_TOKEN", "awsSessionToken"); AwsCredentials testAwsCredentials = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setEnvironmentProvider(environmentProvider) - .build(); + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setEnvironmentProvider(environmentProvider) + .build(); AwsSecurityCredentials credentials = - testAwsCredentials.getAwsSecurityCredentials(EMPTY_METADATA_HEADERS); + testAwsCredentials.getAwsSecurityCredentialsSupplier().getCredentials(); assertEquals("awsAccessKeyId", credentials.getAccessKeyId()); assertEquals("awsSecretAccessKey", credentials.getSecretAccessKey()); - assertEquals("awsSessionToken", credentials.getToken()); + assertEquals("awsSessionToken", credentials.getSessionToken()); } @Test @@ -626,18 +788,17 @@ public void getAwsSecurityCredentials_fromMetadataServer() throws IOException { new MockExternalAccountCredentialsTransportFactory(); AwsCredentials awsCredential = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(buildAwsCredentialSource(transportFactory)) - .build(); + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setHttpTransportFactory(transportFactory) + .setCredentialSource(buildAwsCredentialSource(transportFactory)) + .build(); AwsSecurityCredentials credentials = - awsCredential.getAwsSecurityCredentials(EMPTY_METADATA_HEADERS); + awsCredential.getAwsSecurityCredentialsSupplier().getCredentials(); assertEquals("accessKeyId", credentials.getAccessKeyId()); assertEquals("secretAccessKey", credentials.getSecretAccessKey()); - assertEquals("token", credentials.getToken()); + assertEquals("token", credentials.getSessionToken()); List requests = transportFactory.transport.getRequests(); assertEquals(2, requests.size()); @@ -659,14 +820,13 @@ public void getAwsSecurityCredentials_fromMetadataServer_noUrlProvided() { credentialSource.put("regional_cred_verification_url", GET_CALLER_IDENTITY_URL); AwsCredentials awsCredential = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(new AwsCredentialSource(credentialSource)) - .build(); + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setHttpTransportFactory(transportFactory) + .setCredentialSource(new AwsCredentialSource(credentialSource)) + .build(); try { - awsCredential.getAwsSecurityCredentials(EMPTY_METADATA_HEADERS); + awsCredential.getAwsSecurityCredentialsSupplier().getCredentials(); fail("Should not be able to use credential without exception."); } catch (IOException exception) { assertEquals( @@ -688,14 +848,13 @@ public void getAwsRegion_awsRegionEnvironmentVariable() throws IOException { MockExternalAccountCredentialsTransportFactory transportFactory = new MockExternalAccountCredentialsTransportFactory(); AwsCredentials awsCredentials = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(buildAwsCredentialSource(transportFactory)) - .setEnvironmentProvider(environmentProvider) - .build(); + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setHttpTransportFactory(transportFactory) + .setCredentialSource(buildAwsCredentialSource(transportFactory)) + .setEnvironmentProvider(environmentProvider) + .build(); - String region = awsCredentials.getAwsRegion(EMPTY_METADATA_HEADERS); + String region = awsCredentials.getAwsSecurityCredentialsSupplier().getRegion(); // Should attempt to retrieve the region from AWS_REGION env var first. // Metadata server would return us-east-1b. @@ -714,14 +873,13 @@ public void getAwsRegion_awsDefaultRegionEnvironmentVariable() throws IOExceptio MockExternalAccountCredentialsTransportFactory transportFactory = new MockExternalAccountCredentialsTransportFactory(); AwsCredentials awsCredentials = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(buildAwsCredentialSource(transportFactory)) - .setEnvironmentProvider(environmentProvider) - .build(); + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setHttpTransportFactory(transportFactory) + .setCredentialSource(buildAwsCredentialSource(transportFactory)) + .setEnvironmentProvider(environmentProvider) + .build(); - String region = awsCredentials.getAwsRegion(EMPTY_METADATA_HEADERS); + String region = awsCredentials.getAwsSecurityCredentialsSupplier().getRegion(); // Should attempt to retrieve the region from DEFAULT_AWS_REGION before calling the metadata // server. Metadata server would return us-east-1b. @@ -737,13 +895,12 @@ public void getAwsRegion_metadataServer() throws IOException { MockExternalAccountCredentialsTransportFactory transportFactory = new MockExternalAccountCredentialsTransportFactory(); AwsCredentials awsCredentials = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(buildAwsCredentialSource(transportFactory)) - .build(); + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setHttpTransportFactory(transportFactory) + .setCredentialSource(buildAwsCredentialSource(transportFactory)) + .build(); - String region = awsCredentials.getAwsRegion(EMPTY_METADATA_HEADERS); + String region = awsCredentials.getAwsSecurityCredentialsSupplier().getRegion(); // Should retrieve the region from the Metadata server. String expectedRegion = @@ -763,14 +920,13 @@ public void getAwsRegion_metadataServer() throws IOException { @Test public void createdScoped_clonedCredentialWithAddedScopes() throws IOException { AwsCredentials credentials = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) - .setQuotaProjectId("quotaProjectId") - .setClientId("clientId") - .setClientSecret("clientSecret") - .setUniverseDomain("universeDomain") - .build(); + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setUniverseDomain("universeDomain") + .build(); List newScopes = Arrays.asList("scope1", "scope2"); @@ -836,155 +992,26 @@ public void credentialSource_missingRegionalCredVerificationUrl() { } } - @Test - public void shouldUseMetadataServer_withRequiredEnvironmentVariables() { - MockExternalAccountCredentialsTransportFactory transportFactory = - new MockExternalAccountCredentialsTransportFactory(); - - // Add required environment variables. - List regionKeys = ImmutableList.of("AWS_REGION", "AWS_DEFAULT_REGION"); - for (String regionKey : regionKeys) { - TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider(); - // AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are always required. - environmentProvider - .setEnv(regionKey, "awsRegion") - .setEnv("AWS_ACCESS_KEY_ID", "awsAccessKeyId") - .setEnv("AWS_SECRET_ACCESS_KEY", "awsSecretAccessKey"); - AwsCredentials awsCredential = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(buildAwsImdsv2CredentialSource(transportFactory)) - .setEnvironmentProvider(environmentProvider) - .build(); - assertFalse(awsCredential.shouldUseMetadataServer()); - } - } - - @Test - public void shouldUseMetadataServer_missingRegion() { - MockExternalAccountCredentialsTransportFactory transportFactory = - new MockExternalAccountCredentialsTransportFactory(); - - TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider(); - environmentProvider - .setEnv("AWS_ACCESS_KEY_ID", "awsAccessKeyId") - .setEnv("AWS_SECRET_ACCESS_KEY", "awsSecretAccessKey"); - AwsCredentials awsCredential = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(buildAwsImdsv2CredentialSource(transportFactory)) - .setEnvironmentProvider(environmentProvider) - .build(); - assertTrue(awsCredential.shouldUseMetadataServer()); - } - - @Test - public void shouldUseMetadataServer_missingAwsAccessKeyId() { - MockExternalAccountCredentialsTransportFactory transportFactory = - new MockExternalAccountCredentialsTransportFactory(); - - // Add required environment variables. - List regionKeys = ImmutableList.of("AWS_REGION", "AWS_DEFAULT_REGION"); - for (String regionKey : regionKeys) { - TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider(); - // AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are always required. - environmentProvider - .setEnv(regionKey, "awsRegion") - .setEnv("AWS_SECRET_ACCESS_KEY", "awsSecretAccessKey"); - AwsCredentials awsCredential = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(buildAwsImdsv2CredentialSource(transportFactory)) - .setEnvironmentProvider(environmentProvider) - .build(); - assertTrue(awsCredential.shouldUseMetadataServer()); - } - } - - @Test - public void shouldUseMetadataServer_missingAwsSecretAccessKey() { - MockExternalAccountCredentialsTransportFactory transportFactory = - new MockExternalAccountCredentialsTransportFactory(); - - // Add required environment variables. - List regionKeys = ImmutableList.of("AWS_REGION", "AWS_DEFAULT_REGION"); - for (String regionKey : regionKeys) { - TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider(); - // AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are always required. - environmentProvider - .setEnv(regionKey, "awsRegion") - .setEnv("AWS_ACCESS_KEY_ID", "awsAccessKeyId"); - AwsCredentials awsCredential = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(buildAwsImdsv2CredentialSource(transportFactory)) - .setEnvironmentProvider(environmentProvider) - .build(); - assertTrue(awsCredential.shouldUseMetadataServer()); - } - } - - @Test - public void shouldUseMetadataServer_missingAwsSecurityCreds() { - MockExternalAccountCredentialsTransportFactory transportFactory = - new MockExternalAccountCredentialsTransportFactory(); - - // Add required environment variables. - List regionKeys = ImmutableList.of("AWS_REGION", "AWS_DEFAULT_REGION"); - for (String regionKey : regionKeys) { - TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider(); - // AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are always required. - // Not set here. - environmentProvider.setEnv(regionKey, "awsRegion"); - AwsCredentials awsCredential = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(buildAwsImdsv2CredentialSource(transportFactory)) - .setEnvironmentProvider(environmentProvider) - .build(); - assertTrue(awsCredential.shouldUseMetadataServer()); - } - } - - @Test - public void shouldUseMetadataServer_noEnvironmentVars() { - MockExternalAccountCredentialsTransportFactory transportFactory = - new MockExternalAccountCredentialsTransportFactory(); - AwsCredentials awsCredential = - (AwsCredentials) - AwsCredentials.newBuilder(AWS_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(buildAwsImdsv2CredentialSource(transportFactory)) - .build(); - assertTrue(awsCredential.shouldUseMetadataServer()); - } - @Test public void builder_allFields() throws IOException { List scopes = Arrays.asList("scope1", "scope2"); AwsCredentials credentials = - (AwsCredentials) - AwsCredentials.newBuilder() - .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) - .setAudience("audience") - .setSubjectTokenType("subjectTokenType") - .setTokenUrl(STS_URL) - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(AWS_CREDENTIAL_SOURCE) - .setTokenInfoUrl("tokenInfoUrl") - .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) - .setQuotaProjectId("quotaProjectId") - .setClientId("clientId") - .setClientSecret("clientSecret") - .setScopes(scopes) - .setUniverseDomain("universeDomain") - .build(); + AwsCredentials.newBuilder() + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(AWS_CREDENTIAL_SOURCE) + .setTokenInfoUrl("tokenInfoUrl") + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setScopes(scopes) + .setUniverseDomain("universeDomain") + .build(); assertEquals("audience", credentials.getAudience()); assertEquals("subjectTokenType", credentials.getSubjectTokenType()); @@ -1006,22 +1033,23 @@ public void builder_missingUniverseDomain_defaults() throws IOException { List scopes = Arrays.asList("scope1", "scope2"); AwsCredentials credentials = - (AwsCredentials) - AwsCredentials.newBuilder() - .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) - .setAudience("audience") - .setSubjectTokenType("subjectTokenType") - .setTokenUrl(STS_URL) - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(AWS_CREDENTIAL_SOURCE) - .setTokenInfoUrl("tokenInfoUrl") - .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) - .setQuotaProjectId("quotaProjectId") - .setClientId("clientId") - .setClientSecret("clientSecret") - .setScopes(scopes) - .build(); - + AwsCredentials.newBuilder() + .setRegionalCredentialVerificationUrlOverride("https://test.com") + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(AWS_CREDENTIAL_SOURCE) + .setTokenInfoUrl("tokenInfoUrl") + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setScopes(scopes) + .build(); + + assertEquals("https://test.com", credentials.getRegionalCredentialVerificationUrlOverride()); assertEquals("audience", credentials.getAudience()); assertEquals("subjectTokenType", credentials.getSubjectTokenType()); assertEquals(STS_URL, credentials.getTokenUrl()); @@ -1042,22 +1070,21 @@ public void newBuilder_allFields() throws IOException { List scopes = Arrays.asList("scope1", "scope2"); AwsCredentials credentials = - (AwsCredentials) - AwsCredentials.newBuilder() - .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) - .setAudience("audience") - .setSubjectTokenType("subjectTokenType") - .setTokenUrl(STS_URL) - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(AWS_CREDENTIAL_SOURCE) - .setTokenInfoUrl("tokenInfoUrl") - .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) - .setQuotaProjectId("quotaProjectId") - .setClientId("clientId") - .setClientSecret("clientSecret") - .setScopes(scopes) - .setUniverseDomain("universeDomain") - .build(); + AwsCredentials.newBuilder() + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(AWS_CREDENTIAL_SOURCE) + .setTokenInfoUrl("tokenInfoUrl") + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setScopes(scopes) + .setUniverseDomain("universeDomain") + .build(); AwsCredentials newBuilderCreds = AwsCredentials.newBuilder(credentials).build(); assertEquals(credentials.getAudience(), newBuilderCreds.getAudience()); @@ -1081,21 +1108,20 @@ public void newBuilder_noUniverseDomain_defaults() throws IOException { List scopes = Arrays.asList("scope1", "scope2"); AwsCredentials credentials = - (AwsCredentials) - AwsCredentials.newBuilder() - .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) - .setAudience("audience") - .setSubjectTokenType("subjectTokenType") - .setTokenUrl(STS_URL) - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(AWS_CREDENTIAL_SOURCE) - .setTokenInfoUrl("tokenInfoUrl") - .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) - .setQuotaProjectId("quotaProjectId") - .setClientId("clientId") - .setClientSecret("clientSecret") - .setScopes(scopes) - .build(); + AwsCredentials.newBuilder() + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(AWS_CREDENTIAL_SOURCE) + .setTokenInfoUrl("tokenInfoUrl") + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setScopes(scopes) + .build(); AwsCredentials newBuilderCreds = AwsCredentials.newBuilder(credentials).build(); assertEquals(credentials.getAudience(), newBuilderCreds.getAudience()); @@ -1114,27 +1140,114 @@ public void newBuilder_noUniverseDomain_defaults() throws IOException { assertEquals(GOOGLE_DEFAULT_UNIVERSE, newBuilderCreds.getUniverseDomain()); } + @Test + public void builder_defaultRegionalCredentialVerificationUrlOverride() throws IOException { + List scopes = Arrays.asList("scope1", "scope2"); + + AwsSecurityCredentialsSupplier supplier = + new TestAwsSecurityCredentialsSupplier("region", null, null); + + AwsCredentials credentials = + AwsCredentials.newBuilder() + .setAwsSecurityCredentialsSupplier(supplier) + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setScopes(scopes) + .build(); + + assertNull(credentials.getRegionalCredentialVerificationUrlOverride()); + assertEquals( + DEFAULT_REGIONAL_CREDENTIAL_VERIFICATION_URL, + credentials.getRegionalCredentialVerificationUrl()); + } + + @Test + public void builder_supplierAndCredSourceThrows() throws IOException { + List scopes = Arrays.asList("scope1", "scope2"); + + AwsSecurityCredentialsSupplier supplier = + new TestAwsSecurityCredentialsSupplier("region", null, null); + + try { + AwsCredentials credentials = + AwsCredentials.newBuilder() + .setAwsSecurityCredentialsSupplier(supplier) + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(AWS_CREDENTIAL_SOURCE) + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setScopes(scopes) + .build(); + fail("Should not be able to continue without exception."); + } catch (IllegalArgumentException exception) { + assertEquals( + "AwsCredentials cannot have both an awsSecurityCredentialsSupplier and a credentialSource.", + exception.getMessage()); + } + } + + @Test + public void builder_noSupplieOrCredSourceThrows() throws IOException { + List scopes = Arrays.asList("scope1", "scope2"); + + Supplier testSupplier = () -> null; + + try { + AwsCredentials credentials = + AwsCredentials.newBuilder() + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setTokenInfoUrl("tokenInfoUrl") + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setScopes(scopes) + .build(); + fail("Should not be able to continue without exception."); + } catch (IllegalArgumentException exception) { + assertEquals( + "An awsSecurityCredentialsSupplier or a credentialSource must be provided.", + exception.getMessage()); + } + } + @Test public void serialize() throws IOException, ClassNotFoundException { List scopes = Arrays.asList("scope1", "scope2"); AwsCredentials testCredentials = - (AwsCredentials) - AwsCredentials.newBuilder() - .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) - .setAudience("audience") - .setSubjectTokenType("subjectTokenType") - .setTokenUrl(STS_URL) - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(AWS_CREDENTIAL_SOURCE) - .setTokenInfoUrl("tokenInfoUrl") - .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) - .setQuotaProjectId("quotaProjectId") - .setClientId("clientId") - .setClientSecret("clientSecret") - .setUniverseDomain("universeDomain") - .setScopes(scopes) - .build(); + AwsCredentials.newBuilder() + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(AWS_CREDENTIAL_SOURCE) + .setTokenInfoUrl("tokenInfoUrl") + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setUniverseDomain("universeDomain") + .setScopes(scopes) + .build(); AwsCredentials deserializedCredentials = serializeAndDeserialize(testCredentials); assertEquals(testCredentials, deserializedCredentials); @@ -1171,7 +1284,7 @@ private static AwsCredentialSource buildAwsCredentialSource( return new AwsCredentialSource(buildAwsCredentialSourceMap(transportFactory)); } - private static AwsCredentialSource buildAwsImdsv2CredentialSource( + static AwsCredentialSource buildAwsImdsv2CredentialSource( MockExternalAccountCredentialsTransportFactory transportFactory) { Map credentialSourceMap = buildAwsCredentialSourceMap(transportFactory); credentialSourceMap.put( @@ -1211,4 +1324,31 @@ static InputStream writeAwsCredentialsStream(String stsUrl, String regionUrl, St return TestUtils.jsonToInputStream(json); } + + class TestAwsSecurityCredentialsSupplier implements AwsSecurityCredentialsSupplier { + + private String region; + private AwsSecurityCredentials credentials; + private IOException credentialException; + + TestAwsSecurityCredentialsSupplier( + String region, AwsSecurityCredentials credentials, IOException credentialException) { + this.region = region; + this.credentials = credentials; + this.credentialException = credentialException; + } + + @Override + public String getRegion() throws IOException { + return this.region; + } + + @Override + public AwsSecurityCredentials getCredentials() throws IOException { + if (this.credentialException != null) { + throw this.credentialException; + } + return this.credentials; + } + } } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index 2d2c88559..986669c9c 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -44,12 +44,12 @@ import com.google.api.client.util.Clock; import com.google.auth.TestUtils; import com.google.auth.http.HttpTransportFactory; +import com.google.auth.oauth2.ExternalAccountCredentials.SubjectTokenTypes; import com.google.auth.oauth2.ExternalAccountCredentialsTest.TestExternalAccountCredentials.TestCredentialSource; import java.io.ByteArrayInputStream; import java.io.IOException; import java.math.BigDecimal; import java.net.URI; -import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; @@ -65,7 +65,7 @@ @RunWith(JUnit4.class) public class ExternalAccountCredentialsTest extends BaseSerializationTest { - private static final String STS_URL = "https://sts.googleapis.com"; + private static final String STS_URL = "https://sts.googleapis.com/v1/token"; private static final String GOOGLE_DEFAULT_UNIVERSE = "googleapis.com"; private static final Map FILE_CREDENTIAL_SOURCE_MAP = @@ -468,21 +468,6 @@ public void fromJson_nullJson_throws() throws IOException { } } - @Test - public void fromJson_invalidServiceAccountImpersonationUrl_throws() throws IOException { - GenericJson json = buildJsonIdentityPoolCredential(); - json.put("service_account_impersonation_url", "https://iamcredentials.googleapis.com"); - - try { - ExternalAccountCredentials.fromJson(json, OAuth2Utils.HTTP_TRANSPORT_FACTORY); - fail("Exception should be thrown."); - } catch (IllegalArgumentException e) { - assertEquals( - "Unable to determine target principal from service account impersonation URL.", - e.getMessage()); - } - } - @Test public void fromJson_nullTransport_throws() throws IOException { try { @@ -563,6 +548,41 @@ public void constructor_builder() throws IOException { assertNotNull(credentials.getCredentialSource()); } + @Test + public void constructor_builder_defaultTokenUrl() { + HashMap credentialSource = new HashMap<>(); + credentialSource.put("file", "file"); + + ExternalAccountCredentials credentials = + IdentityPoolCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setAudience( + "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setCredentialSource(new TestCredentialSource(credentialSource)) + .build(); + + assertEquals(STS_URL, credentials.getTokenUrl()); + } + + @Test + public void constructor_builder_subjectTokenTypeEnum() { + HashMap credentialSource = new HashMap<>(); + credentialSource.put("file", "file"); + + ExternalAccountCredentials credentials = + IdentityPoolCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setAudience( + "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") + .setSubjectTokenType(SubjectTokenTypes.SAML2) + .setTokenUrl(STS_URL) + .setCredentialSource(new TestCredentialSource(credentialSource)) + .build(); + + assertEquals(SubjectTokenTypes.SAML2.value, credentials.getSubjectTokenType()); + } + @Test public void constructor_builder_invalidTokenUrl() { try { @@ -1004,39 +1024,6 @@ public void exchangeExternalCredentialForAccessToken_withServiceAccountImpersona assertEquals("2800s", query.get("lifetime")); } - @Test - public void exchangeExternalCredentialForAccessToken_withServiceAccountImpersonationOverride() - throws IOException { - transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); - - String serviceAccountEmail = "different@different.iam.gserviceaccount.com"; - ExternalAccountCredentials credential = - ExternalAccountCredentials.fromStream( - IdentityPoolCredentialsTest.writeIdentityPoolCredentialsStream( - transportFactory.transport.getStsUrl(), - transportFactory.transport.getMetadataUrl(), - transportFactory.transport.getServiceAccountImpersonationUrl(), - /* serviceAccountImpersonationOptionsMap= */ null), - transportFactory); - - // Override impersonated credentials. - ExternalAccountCredentials sourceCredentials = - IdentityPoolCredentials.newBuilder((IdentityPoolCredentials) credential) - .setServiceAccountImpersonationUrl(null) - .build(); - credential.overrideImpersonatedCredentials( - new ImpersonatedCredentials.Builder(sourceCredentials, serviceAccountEmail) - .setScopes(new ArrayList<>(sourceCredentials.getScopes())) - .setHttpTransportFactory(transportFactory) - .build()); - - credential.exchangeExternalCredentialForAccessToken( - StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build()); - - assertTrue( - transportFactory.transport.getRequests().get(2).getUrl().contains(serviceAccountEmail)); - } - @Test public void exchangeExternalCredentialForAccessToken_throws() throws IOException { ExternalAccountCredentials credential = @@ -1061,6 +1048,27 @@ public void exchangeExternalCredentialForAccessToken_throws() throws IOException } } + @Test + public void exchangeExternalCredentialForAccessToken_invalidImpersonatedCredentialsThrows() + throws IOException { + GenericJson json = buildJsonIdentityPoolCredential(); + json.put("service_account_impersonation_url", "https://iamcredentials.googleapis.com"); + ExternalAccountCredentials credential = + ExternalAccountCredentials.fromJson(json, transportFactory); + + StsTokenExchangeRequest stsTokenExchangeRequest = + StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build(); + + try { + credential.exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest); + fail("Exception should be thrown."); + } catch (IllegalArgumentException e) { + assertEquals( + "Unable to determine target principal from service account impersonation URL.", + e.getMessage()); + } + } + @Test public void getRequestMetadata_withQuotaProjectId() throws IOException { TestExternalAccountCredentials testCredentials = diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java index 9b4e9760b..8f806cd95 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java @@ -46,6 +46,7 @@ import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.util.GenericData; import com.google.auth.http.HttpCredentialsAdapter; +import com.google.auth.oauth2.ExternalAccountCredentials.SubjectTokenTypes; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; @@ -145,10 +146,62 @@ public void awsCredentials() throws Exception { .setEnv("AWS_REGION", "us-east-2"); AwsCredentials awsCredential = - (AwsCredentials) - AwsCredentials.newBuilder(awsCredentialWithoutEnvProvider) - .setEnvironmentProvider(testEnvironmentProvider) - .build(); + AwsCredentials.newBuilder(awsCredentialWithoutEnvProvider) + .setEnvironmentProvider(testEnvironmentProvider) + .build(); + + callGcs(awsCredential); + } + + /** + * AwsCredentials (AWS Provider): Uses the service account keys to generate a Google ID token + * using the iamcredentials generateIdToken API. Exchanges the OIDC ID token for AWS security keys + * using AWS STS AssumeRoleWithWebIdentity API. These values will be returned as a + * AwsSecurityCredentials object and returned by a Supplier. The Auth library can now call get() + * from the supplier and create a signed request to AWS GetCallerIdentity. This will be used as + * the external subject token to be exchanged for a GCP access token via GCP STS endpoint and then + * to impersonate the original service account key. + */ + @Test + public void awsCredentials_withProgrammaticAuth() throws Exception { + String idToken = generateGoogleIdToken(AWS_AUDIENCE); + + String url = + String.format( + "https://sts.amazonaws.com/?Action=AssumeRoleWithWebIdentity" + + "&Version=2011-06-15&DurationSeconds=3600&RoleSessionName=%s" + + "&RoleArn=%s&WebIdentityToken=%s", + AWS_ROLE_NAME, AWS_ROLE_ARN, idToken); + + HttpRequestFactory requestFactory = new NetHttpTransport().createRequestFactory(); + HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(url)); + + JsonObjectParser parser = new JsonObjectParser(GsonFactory.getDefaultInstance()); + request.setParser(parser); + + HttpResponse response = request.execute(); + String rawXml = response.parseAsString(); + + String awsAccessKeyId = getXmlValueByTagName(rawXml, "AccessKeyId"); + String awsSecretAccessKey = getXmlValueByTagName(rawXml, "SecretAccessKey"); + String awsSessionToken = getXmlValueByTagName(rawXml, "SessionToken"); + + AwsSecurityCredentials credentials = + new AwsSecurityCredentials(awsAccessKeyId, awsSecretAccessKey, awsSessionToken); + + AwsSecurityCredentialsSupplier provider = + new ITAwsSecurityCredentialsProvider("us-east-2", credentials); + AwsCredentials awsCredential = + AwsCredentials.newBuilder() + .setAwsSecurityCredentialsSupplier(provider) + .setSubjectTokenType(SubjectTokenTypes.AWS4) + .setAudience(AWS_AUDIENCE) + .setServiceAccountImpersonationUrl( + String.format( + "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken", + clientEmail)) + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .build(); callGcs(awsCredential); } @@ -193,6 +246,41 @@ public void identityPoolCredentials_withServiceAccountImpersonationOptions() thr assertTrue(minExpirationtime <= tokenExpiry && tokenExpiry <= maxExpirationTime); } + /** + * IdentityPoolCredentials (OIDC provider): Uses the service account to generate a Google ID token + * using the iamcredentials generateIdToken API. This will use the service account client ID as + * the sub field of the token. This OIDC token will be used as the external subject token to be + * exchanged for a GCP access token via GCP STS endpoint and then to impersonate the original + * service account key. Retrieves the OIDC token from a Supplier that returns the subject token + * when get() is called. + */ + @Test + public void identityPoolCredentials_withProgrammaticAuth() throws IOException { + + IdentityPoolSubjectTokenSupplier tokenSupplier = + () -> { + try { + return generateGoogleIdToken(OIDC_AUDIENCE); + } catch (IOException e) { + throw new RuntimeException(e); + } + }; + + IdentityPoolCredentials identityPoolCredentials = + IdentityPoolCredentials.newBuilder() + .setSubjectTokenSupplier(tokenSupplier) + .setAudience(OIDC_AUDIENCE) + .setSubjectTokenType(SubjectTokenTypes.JWT) + .setServiceAccountImpersonationUrl( + String.format( + "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken", + clientEmail)) + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .build(); + + callGcs(identityPoolCredentials); + } + private GenericJson buildIdentityPoolCredentialConfig() throws IOException { String idToken = generateGoogleIdToken(OIDC_AUDIENCE); @@ -363,4 +451,25 @@ private String getXmlValueByTagName(String rawXml, String tagName) { } return null; } + + private class ITAwsSecurityCredentialsProvider implements AwsSecurityCredentialsSupplier { + + private String region; + private AwsSecurityCredentials credentials; + + ITAwsSecurityCredentialsProvider(String region, AwsSecurityCredentials credentials) { + this.region = region; + this.credentials = credentials; + } + + @Override + public String getRegion() { + return this.region; + } + + @Override + public AwsSecurityCredentials getCredentials() { + return this.credentials; + } + } } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java index 26d99b89c..0d88bbc92 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java @@ -59,7 +59,7 @@ @RunWith(JUnit4.class) public class IdentityPoolCredentialsTest extends BaseSerializationTest { - private static final String STS_URL = "https://sts.googleapis.com"; + private static final String STS_URL = "https://sts.googleapis.com/v1/token"; private static final Map FILE_CREDENTIAL_SOURCE_MAP = new HashMap() { @@ -72,16 +72,17 @@ public class IdentityPoolCredentialsTest extends BaseSerializationTest { new IdentityPoolCredentialSource(FILE_CREDENTIAL_SOURCE_MAP); private static final IdentityPoolCredentials FILE_SOURCED_CREDENTIAL = - (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder() - .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) - .setAudience( - "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") - .setSubjectTokenType("subjectTokenType") - .setTokenUrl(STS_URL) - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(FILE_CREDENTIAL_SOURCE) - .build(); + IdentityPoolCredentials.newBuilder() + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience( + "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(FILE_CREDENTIAL_SOURCE) + .build(); + + private static final IdentityPoolSubjectTokenSupplier testProvider = () -> "testSubjectToken"; static class MockExternalAccountCredentialsTransportFactory implements HttpTransportFactory { @@ -97,14 +98,13 @@ public HttpTransport create() { @Test public void createdScoped_clonedCredentialWithAddedScopes() throws IOException { IdentityPoolCredentials credentials = - (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) - .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) - .setQuotaProjectId("quotaProjectId") - .setClientId("clientId") - .setClientSecret("clientSecret") - .setUniverseDomain("universeDomain") - .build(); + IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setUniverseDomain("universeDomain") + .build(); List newScopes = Arrays.asList("scope1", "scope2"); @@ -143,10 +143,9 @@ public void retrieveSubjectToken_fileSourced() throws IOException { new IdentityPoolCredentialSource(credentialSourceMap); IdentityPoolCredentials credentials = - (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) - .setCredentialSource(credentialSource) - .build(); + IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) + .setCredentialSource(credentialSource) + .build(); String subjectToken = credentials.retrieveSubjectToken(); @@ -184,11 +183,10 @@ public void retrieveSubjectToken_fileSourcedWithJsonFormat() throws IOException file.getAbsolutePath()); IdentityPoolCredentials credential = - (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(credentialSource) - .build(); + IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) + .setHttpTransportFactory(transportFactory) + .setCredentialSource(credentialSource) + .build(); String subjectToken = credential.retrieveSubjectToken(); @@ -225,10 +223,9 @@ public void retrieveSubjectToken_noFile_throws() { new IdentityPoolCredentialSource(credentialSourceMap); IdentityPoolCredentials credentials = - (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) - .setCredentialSource(credentialSource) - .build(); + IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) + .setCredentialSource(credentialSource) + .build(); try { credentials.retrieveSubjectToken(); @@ -246,12 +243,11 @@ public void retrieveSubjectToken_urlSourced() throws IOException { new MockExternalAccountCredentialsTransportFactory(); IdentityPoolCredentials credential = - (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource( - buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) - .build(); + IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) + .setHttpTransportFactory(transportFactory) + .setCredentialSource( + buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) + .build(); String subjectToken = credential.retrieveSubjectToken(); @@ -273,11 +269,10 @@ public void retrieveSubjectToken_urlSourcedWithJsonFormat() throws IOException { buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl(), formatMap); IdentityPoolCredentials credential = - (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource(credentialSource) - .build(); + IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) + .setHttpTransportFactory(transportFactory) + .setCredentialSource(credentialSource) + .build(); String subjectToken = credential.retrieveSubjectToken(); @@ -293,12 +288,11 @@ public void retrieveSubjectToken_urlSourcedCredential_throws() { transportFactory.transport.addResponseErrorSequence(response); IdentityPoolCredentials credential = - (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) - .setHttpTransportFactory(transportFactory) - .setCredentialSource( - buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) - .build(); + IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) + .setHttpTransportFactory(transportFactory) + .setCredentialSource( + buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) + .build(); try { credential.retrieveSubjectToken(); @@ -311,24 +305,59 @@ public void retrieveSubjectToken_urlSourcedCredential_throws() { } } + @Test + public void retrieveSubjectToken_provider() throws IOException { + + IdentityPoolCredentials credentials = + IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) + .setCredentialSource(null) + .setSubjectTokenSupplier(testProvider) + .build(); + + String subjectToken = credentials.retrieveSubjectToken(); + + assertEquals(testProvider.getSubjectToken(), subjectToken); + } + + @Test + public void retrieveSubjectToken_providerThrowsError() throws IOException { + IOException testException = new IOException("test"); + + IdentityPoolSubjectTokenSupplier errorProvider = + () -> { + throw testException; + }; + IdentityPoolCredentials credentials = + IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) + .setCredentialSource(null) + .setSubjectTokenSupplier(errorProvider) + .build(); + + try { + String subjectToken = credentials.retrieveSubjectToken(); + fail("retrieveSubjectToken should fail."); + } catch (IOException e) { + assertEquals("test", e.getMessage()); + } + } + @Test public void refreshAccessToken_withoutServiceAccountImpersonation() throws IOException { MockExternalAccountCredentialsTransportFactory transportFactory = new MockExternalAccountCredentialsTransportFactory(); IdentityPoolCredentials credential = - (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder() - .setAudience( - "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") - .setSubjectTokenType("subjectTokenType") - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(FILE_CREDENTIAL_SOURCE) - .setTokenUrl(transportFactory.transport.getStsUrl()) - .setHttpTransportFactory(transportFactory) - .setCredentialSource( - buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) - .build(); + IdentityPoolCredentials.newBuilder() + .setAudience( + "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(FILE_CREDENTIAL_SOURCE) + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setHttpTransportFactory(transportFactory) + .setCredentialSource( + buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) + .build(); AccessToken accessToken = credential.refreshAccessToken(); @@ -346,16 +375,15 @@ public void refreshAccessToken_internalOptionsSet() throws IOException { new MockExternalAccountCredentialsTransportFactory(); IdentityPoolCredentials credential = - (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) - .setWorkforcePoolUserProject("userProject") - .setAudience( - "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") - .setTokenUrl(transportFactory.transport.getStsUrl()) - .setHttpTransportFactory(transportFactory) - .setCredentialSource( - buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) - .build(); + IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) + .setWorkforcePoolUserProject("userProject") + .setAudience( + "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setHttpTransportFactory(transportFactory) + .setCredentialSource( + buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) + .build(); AccessToken accessToken = credential.refreshAccessToken(); @@ -381,19 +409,18 @@ public void refreshAccessToken_withServiceAccountImpersonation() throws IOExcept transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); IdentityPoolCredentials credential = - (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder() - .setAudience( - "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") - .setSubjectTokenType("subjectTokenType") - .setTokenInfoUrl("tokenInfoUrl") - .setServiceAccountImpersonationUrl( - transportFactory.transport.getServiceAccountImpersonationUrl()) - .setTokenUrl(transportFactory.transport.getStsUrl()) - .setHttpTransportFactory(transportFactory) - .setCredentialSource( - buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) - .build(); + IdentityPoolCredentials.newBuilder() + .setAudience( + "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenInfoUrl("tokenInfoUrl") + .setServiceAccountImpersonationUrl( + transportFactory.transport.getServiceAccountImpersonationUrl()) + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setHttpTransportFactory(transportFactory) + .setCredentialSource( + buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) + .build(); AccessToken accessToken = credential.refreshAccessToken(); @@ -413,21 +440,20 @@ public void refreshAccessToken_withServiceAccountImpersonationOptions() throws I transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); IdentityPoolCredentials credential = - (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder() - .setAudience( - "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") - .setSubjectTokenType("subjectTokenType") - .setTokenInfoUrl("tokenInfoUrl") - .setTokenUrl(transportFactory.transport.getStsUrl()) - .setHttpTransportFactory(transportFactory) - .setServiceAccountImpersonationUrl( - transportFactory.transport.getServiceAccountImpersonationUrl()) - .setCredentialSource( - buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) - .setServiceAccountImpersonationOptions( - ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions(2800)) - .build(); + IdentityPoolCredentials.newBuilder() + .setAudience( + "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenInfoUrl("tokenInfoUrl") + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setHttpTransportFactory(transportFactory) + .setServiceAccountImpersonationUrl( + transportFactory.transport.getServiceAccountImpersonationUrl()) + .setCredentialSource( + buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) + .setServiceAccountImpersonationOptions( + ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions(2800)) + .build(); AccessToken accessToken = credential.refreshAccessToken(); @@ -448,6 +474,63 @@ public void refreshAccessToken_withServiceAccountImpersonationOptions() throws I ExternalAccountCredentialsTest.validateMetricsHeader(headers, "url", true, true); } + @Test + public void refreshAccessToken_Provider() throws IOException { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); + IdentityPoolCredentials credential = + IdentityPoolCredentials.newBuilder() + .setSubjectTokenSupplier(testProvider) + .setAudience( + "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenInfoUrl("tokenInfoUrl") + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setHttpTransportFactory(transportFactory) + .build(); + + AccessToken accessToken = credential.refreshAccessToken(); + + assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue()); + + // Validate metrics header is set correctly on the sts request. + Map> headers = + transportFactory.transport.getRequests().get(0).getHeaders(); + ExternalAccountCredentialsTest.validateMetricsHeader(headers, "programmatic", false, false); + } + + @Test + public void refreshAccessToken_providerWithServiceAccountImpersonation() throws IOException { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); + IdentityPoolCredentials credential = + IdentityPoolCredentials.newBuilder() + .setSubjectTokenSupplier(testProvider) + .setAudience( + "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenInfoUrl("tokenInfoUrl") + .setServiceAccountImpersonationUrl( + transportFactory.transport.getServiceAccountImpersonationUrl()) + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setHttpTransportFactory(transportFactory) + .build(); + + AccessToken accessToken = credential.refreshAccessToken(); + + assertEquals( + transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue()); + + // Validate metrics header is set correctly on the sts request. + Map> headers = + transportFactory.transport.getRequests().get(0).getHeaders(); + ExternalAccountCredentialsTest.validateMetricsHeader(headers, "programmatic", true, false); + } + @Test public void refreshAccessToken_workforceWithServiceAccountImpersonation() throws IOException { MockExternalAccountCredentialsTransportFactory transportFactory = @@ -455,18 +538,17 @@ public void refreshAccessToken_workforceWithServiceAccountImpersonation() throws transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); IdentityPoolCredentials credential = - (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) - .setAudience( - "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") - .setTokenUrl(transportFactory.transport.getStsUrl()) - .setServiceAccountImpersonationUrl( - transportFactory.transport.getServiceAccountImpersonationUrl()) - .setHttpTransportFactory(transportFactory) - .setCredentialSource( - buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) - .setWorkforcePoolUserProject("userProject") - .build(); + IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) + .setAudience( + "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setServiceAccountImpersonationUrl( + transportFactory.transport.getServiceAccountImpersonationUrl()) + .setHttpTransportFactory(transportFactory) + .setCredentialSource( + buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) + .setWorkforcePoolUserProject("userProject") + .build(); AccessToken accessToken = credential.refreshAccessToken(); @@ -492,20 +574,19 @@ public void refreshAccessToken_workforceWithServiceAccountImpersonationOptions() transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); IdentityPoolCredentials credential = - (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) - .setAudience( - "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") - .setTokenUrl(transportFactory.transport.getStsUrl()) - .setServiceAccountImpersonationUrl( - transportFactory.transport.getServiceAccountImpersonationUrl()) - .setHttpTransportFactory(transportFactory) - .setCredentialSource( - buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) - .setWorkforcePoolUserProject("userProject") - .setServiceAccountImpersonationOptions( - ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions(2800)) - .build(); + IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) + .setAudience( + "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setServiceAccountImpersonationUrl( + transportFactory.transport.getServiceAccountImpersonationUrl()) + .setHttpTransportFactory(transportFactory) + .setCredentialSource( + buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) + .setWorkforcePoolUserProject("userProject") + .setServiceAccountImpersonationOptions( + ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions(2800)) + .build(); AccessToken accessToken = credential.refreshAccessToken(); @@ -677,21 +758,20 @@ public void builder_allFields() throws IOException { List scopes = Arrays.asList("scope1", "scope2"); IdentityPoolCredentials credentials = - (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder() - .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) - .setAudience("audience") - .setSubjectTokenType("subjectTokenType") - .setTokenUrl(STS_URL) - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(FILE_CREDENTIAL_SOURCE) - .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) - .setQuotaProjectId("quotaProjectId") - .setClientId("clientId") - .setClientSecret("clientSecret") - .setScopes(scopes) - .setUniverseDomain("universeDomain") - .build(); + IdentityPoolCredentials.newBuilder() + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(FILE_CREDENTIAL_SOURCE) + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setScopes(scopes) + .setUniverseDomain("universeDomain") + .build(); assertEquals("audience", credentials.getAudience()); assertEquals("subjectTokenType", credentials.getSubjectTokenType()); @@ -708,6 +788,28 @@ public void builder_allFields() throws IOException { assertEquals("universeDomain", credentials.getUniverseDomain()); } + @Test + public void builder_subjectTokenSupplier() { + List scopes = Arrays.asList("scope1", "scope2"); + + IdentityPoolCredentials credentials = + IdentityPoolCredentials.newBuilder() + .setSubjectTokenSupplier(testProvider) + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setScopes(scopes) + .build(); + + assertEquals(testProvider, credentials.getIdentityPoolSubjectTokenSupplier()); + } + @Test public void builder_invalidWorkforceAudiences_throws() { List invalidAudiences = @@ -747,41 +849,76 @@ public void builder_invalidWorkforceAudiences_throws() { public void builder_emptyWorkforceUserProjectWithWorkforceAudience() { // No exception should be thrown. IdentityPoolCredentials credentials = - (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder() - .setWorkforcePoolUserProject("") - .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) - .setAudience( - "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") - .setSubjectTokenType("subjectTokenType") - .setTokenUrl(STS_URL) - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(FILE_CREDENTIAL_SOURCE) - .setQuotaProjectId("quotaProjectId") - .build(); + IdentityPoolCredentials.newBuilder() + .setWorkforcePoolUserProject("") + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience( + "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(FILE_CREDENTIAL_SOURCE) + .setQuotaProjectId("quotaProjectId") + .build(); assertTrue(credentials.isWorkforcePoolConfiguration()); } @Test + public void builder_supplierAndCredSourceThrows() throws IOException { + try { + IdentityPoolCredentials credentials = + IdentityPoolCredentials.newBuilder() + .setSubjectTokenSupplier(testProvider) + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setCredentialSource(FILE_CREDENTIAL_SOURCE) + .build(); + fail("Should not be able to continue without exception."); + } catch (IllegalArgumentException exception) { + assertEquals( + "IdentityPoolCredentials cannot have both a subjectTokenSupplier and a credentialSource.", + exception.getMessage()); + } + } + + @Test + public void builder_noSupplierOrCredSourceThrows() throws IOException { + + try { + IdentityPoolCredentials credentials = + IdentityPoolCredentials.newBuilder() + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .build(); + fail("Should not be able to continue without exception."); + } catch (IllegalArgumentException exception) { + assertEquals( + "A subjectTokenSupplier or a credentialSource must be provided.", exception.getMessage()); + } + } + public void builder_missingUniverseDomain_defaults() throws IOException { List scopes = Arrays.asList("scope1", "scope2"); IdentityPoolCredentials credentials = - (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder() - .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) - .setAudience("audience") - .setSubjectTokenType("subjectTokenType") - .setTokenUrl(STS_URL) - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(FILE_CREDENTIAL_SOURCE) - .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) - .setQuotaProjectId("quotaProjectId") - .setClientId("clientId") - .setClientSecret("clientSecret") - .setScopes(scopes) - .build(); + IdentityPoolCredentials.newBuilder() + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(FILE_CREDENTIAL_SOURCE) + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setScopes(scopes) + .build(); assertEquals("audience", credentials.getAudience()); assertEquals("subjectTokenType", credentials.getSubjectTokenType()); @@ -803,23 +940,22 @@ public void newBuilder_allFields() throws IOException { List scopes = Arrays.asList("scope1", "scope2"); IdentityPoolCredentials credentials = - (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder() - .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) - .setAudience( - "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") - .setSubjectTokenType("subjectTokenType") - .setTokenUrl(STS_URL) - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(FILE_CREDENTIAL_SOURCE) - .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) - .setQuotaProjectId("quotaProjectId") - .setClientId("clientId") - .setClientSecret("clientSecret") - .setScopes(scopes) - .setWorkforcePoolUserProject("workforcePoolUserProject") - .setUniverseDomain("universeDomain") - .build(); + IdentityPoolCredentials.newBuilder() + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience( + "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(FILE_CREDENTIAL_SOURCE) + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setScopes(scopes) + .setWorkforcePoolUserProject("workforcePoolUserProject") + .setUniverseDomain("universeDomain") + .build(); IdentityPoolCredentials newBuilderCreds = IdentityPoolCredentials.newBuilder(credentials).build(); @@ -846,22 +982,21 @@ public void newBuilder_noUniverseDomain_defaults() throws IOException { List scopes = Arrays.asList("scope1", "scope2"); IdentityPoolCredentials credentials = - (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder() - .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) - .setAudience( - "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") - .setSubjectTokenType("subjectTokenType") - .setTokenUrl(STS_URL) - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(FILE_CREDENTIAL_SOURCE) - .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) - .setQuotaProjectId("quotaProjectId") - .setClientId("clientId") - .setClientSecret("clientSecret") - .setScopes(scopes) - .setWorkforcePoolUserProject("workforcePoolUserProject") - .build(); + IdentityPoolCredentials.newBuilder() + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience( + "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(FILE_CREDENTIAL_SOURCE) + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setScopes(scopes) + .setWorkforcePoolUserProject("workforcePoolUserProject") + .build(); IdentityPoolCredentials newBuilderCreds = IdentityPoolCredentials.newBuilder(credentials).build(); @@ -886,14 +1021,13 @@ public void newBuilder_noUniverseDomain_defaults() throws IOException { @Test public void serialize() throws IOException, ClassNotFoundException { IdentityPoolCredentials testCredentials = - (IdentityPoolCredentials) - IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) - .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) - .setQuotaProjectId("quotaProjectId") - .setClientId("clientId") - .setClientSecret("clientSecret") - .setUniverseDomain("universeDomain") - .build(); + IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setUniverseDomain("universeDomain") + .build(); IdentityPoolCredentials deserializedCredentials = serializeAndDeserialize(testCredentials); assertEquals(testCredentials, deserializedCredentials); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/InternalAwsSecurityCredentialsSupplierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/InternalAwsSecurityCredentialsSupplierTest.java new file mode 100644 index 000000000..f95bd4cab --- /dev/null +++ b/oauth2_http/javatests/com/google/auth/oauth2/InternalAwsSecurityCredentialsSupplierTest.java @@ -0,0 +1,165 @@ +/* + * Copyright 2024 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.oauth2; + +import static com.google.auth.oauth2.AwsCredentialsTest.buildAwsImdsv2CredentialSource; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.auth.oauth2.ExternalAccountCredentialsTest.MockExternalAccountCredentialsTransportFactory; +import com.google.common.collect.ImmutableList; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link InternalAwsSecurityCredentialsSupplier}. */ +@RunWith(JUnit4.class) +public class InternalAwsSecurityCredentialsSupplierTest { + @Test + public void shouldUseMetadataServer_withRequiredEnvironmentVariables() { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + // Add required environment variables. + List regionKeys = ImmutableList.of("AWS_REGION", "AWS_DEFAULT_REGION"); + for (String regionKey : regionKeys) { + TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider(); + // AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are always required. + environmentProvider + .setEnv(regionKey, "awsRegion") + .setEnv("AWS_ACCESS_KEY_ID", "awsAccessKeyId") + .setEnv("AWS_SECRET_ACCESS_KEY", "awsSecretAccessKey"); + InternalAwsSecurityCredentialsSupplier supplier = + new InternalAwsSecurityCredentialsSupplier( + buildAwsImdsv2CredentialSource(transportFactory), + environmentProvider, + transportFactory); + assertFalse(supplier.shouldUseMetadataServer()); + } + } + + @Test + public void shouldUseMetadataServer_missingRegion() { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider(); + environmentProvider + .setEnv("AWS_ACCESS_KEY_ID", "awsAccessKeyId") + .setEnv("AWS_SECRET_ACCESS_KEY", "awsSecretAccessKey"); + InternalAwsSecurityCredentialsSupplier supplier = + new InternalAwsSecurityCredentialsSupplier( + buildAwsImdsv2CredentialSource(transportFactory), + environmentProvider, + transportFactory); + assertTrue(supplier.shouldUseMetadataServer()); + } + + @Test + public void shouldUseMetadataServer_missingAwsAccessKeyId() { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + // Add required environment variables. + List regionKeys = ImmutableList.of("AWS_REGION", "AWS_DEFAULT_REGION"); + for (String regionKey : regionKeys) { + TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider(); + // AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are always required. + environmentProvider + .setEnv(regionKey, "awsRegion") + .setEnv("AWS_SECRET_ACCESS_KEY", "awsSecretAccessKey"); + InternalAwsSecurityCredentialsSupplier supplier = + new InternalAwsSecurityCredentialsSupplier( + buildAwsImdsv2CredentialSource(transportFactory), + environmentProvider, + transportFactory); + assertTrue(supplier.shouldUseMetadataServer()); + } + } + + @Test + public void shouldUseMetadataServer_missingAwsSecretAccessKey() { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + // Add required environment variables. + List regionKeys = ImmutableList.of("AWS_REGION", "AWS_DEFAULT_REGION"); + for (String regionKey : regionKeys) { + TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider(); + // AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are always required. + environmentProvider + .setEnv(regionKey, "awsRegion") + .setEnv("AWS_ACCESS_KEY_ID", "awsAccessKeyId"); + InternalAwsSecurityCredentialsSupplier supplier = + new InternalAwsSecurityCredentialsSupplier( + buildAwsImdsv2CredentialSource(transportFactory), + environmentProvider, + transportFactory); + assertTrue(supplier.shouldUseMetadataServer()); + } + } + + @Test + public void shouldUseMetadataServer_missingAwsSecurityCreds() { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + // Add required environment variables. + List regionKeys = ImmutableList.of("AWS_REGION", "AWS_DEFAULT_REGION"); + for (String regionKey : regionKeys) { + TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider(); + // AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are always required. + // Not set here. + environmentProvider.setEnv(regionKey, "awsRegion"); + InternalAwsSecurityCredentialsSupplier supplier = + new InternalAwsSecurityCredentialsSupplier( + buildAwsImdsv2CredentialSource(transportFactory), + environmentProvider, + transportFactory); + assertTrue(supplier.shouldUseMetadataServer()); + } + } + + @Test + public void shouldUseMetadataServer_noEnvironmentVars() { + TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider(); + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + InternalAwsSecurityCredentialsSupplier supplier = + new InternalAwsSecurityCredentialsSupplier( + buildAwsImdsv2CredentialSource(transportFactory), + environmentProvider, + transportFactory); + assertTrue(supplier.shouldUseMetadataServer()); + } +} diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java index a8208fb65..84e0c5606 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java @@ -69,7 +69,7 @@ public class MockExternalAccountCredentialsTransport extends MockHttpTransport { private static final String AWS_REGION_URL = "https://169.254.169.254/region"; private static final String AWS_IMDSV2_SESSION_TOKEN_URL = "https://169.254.169.254/imdsv2"; private static final String METADATA_SERVER_URL = "https://www.metadata.google.com"; - private static final String STS_URL = "https://sts.googleapis.com"; + private static final String STS_URL = "https://sts.googleapis.com/v1/token"; private static final String SUBJECT_TOKEN = "subjectToken"; private static final String TOKEN_TYPE = "Bearer"; diff --git a/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java index a63801c0a..003a393d7 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java @@ -40,7 +40,6 @@ import com.google.auth.TestUtils; import com.google.auth.http.HttpTransportFactory; import com.google.auth.oauth2.ExecutableHandler.ExecutableOptions; -import com.google.auth.oauth2.ExternalAccountCredentials.CredentialSource; import java.io.IOException; import java.io.InputStream; import java.io.NotSerializableException; @@ -63,16 +62,15 @@ public class PluggableAuthCredentialsTest extends BaseSerializationTest { private static final String STS_URL = "https://sts.googleapis.com"; private static final PluggableAuthCredentials CREDENTIAL = - (PluggableAuthCredentials) - PluggableAuthCredentials.newBuilder() - .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) - .setAudience( - "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") - .setSubjectTokenType("subjectTokenType") - .setTokenUrl(STS_URL) - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(buildCredentialSource()) - .build(); + PluggableAuthCredentials.newBuilder() + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience( + "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(buildCredentialSource()) + .build(); static class MockExternalAccountCredentialsTransportFactory implements HttpTransportFactory { @@ -109,12 +107,11 @@ public void retrieveSubjectToken_shouldPassAllOptionsToHandler() throws IOExcept }; PluggableAuthCredentials credential = - (PluggableAuthCredentials) - PluggableAuthCredentials.newBuilder(CREDENTIAL) - .setExecutableHandler(executableHandler) - .setCredentialSource(buildCredentialSource(command, timeout, outputFile)) - .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) - .build(); + PluggableAuthCredentials.newBuilder(CREDENTIAL) + .setExecutableHandler(executableHandler) + .setCredentialSource(buildCredentialSource(command, timeout, outputFile)) + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .build(); String subjectToken = credential.retrieveSubjectToken(); @@ -150,12 +147,11 @@ public void retrieveSubjectToken_shouldPassMinimalOptionsToHandler() throws IOEx }; PluggableAuthCredentials credential = - (PluggableAuthCredentials) - PluggableAuthCredentials.newBuilder(CREDENTIAL) - .setExecutableHandler(executableHandler) - .setCredentialSource( - buildCredentialSource(command, /* timeoutMs= */ null, /* outputFile= */ null)) - .build(); + PluggableAuthCredentials.newBuilder(CREDENTIAL) + .setExecutableHandler(executableHandler) + .setCredentialSource( + buildCredentialSource(command, /* timeoutMs= */ null, /* outputFile= */ null)) + .build(); String subjectToken = credential.retrieveSubjectToken(); @@ -185,12 +181,11 @@ public void refreshAccessToken_withoutServiceAccountImpersonation() throws IOExc transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); PluggableAuthCredentials credential = - (PluggableAuthCredentials) - PluggableAuthCredentials.newBuilder(CREDENTIAL) - .setExecutableHandler(options -> "pluggableAuthToken") - .setTokenUrl(transportFactory.transport.getStsUrl()) - .setHttpTransportFactory(transportFactory) - .build(); + PluggableAuthCredentials.newBuilder(CREDENTIAL) + .setExecutableHandler(options -> "pluggableAuthToken") + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setHttpTransportFactory(transportFactory) + .build(); AccessToken accessToken = credential.refreshAccessToken(); @@ -215,18 +210,17 @@ public void refreshAccessToken_withServiceAccountImpersonation() throws IOExcept transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); PluggableAuthCredentials credential = - (PluggableAuthCredentials) - PluggableAuthCredentials.newBuilder() - .setAudience( - "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") - .setSubjectTokenType("subjectTokenType") - .setTokenInfoUrl("tokenInfoUrl") - .setTokenUrl(transportFactory.transport.getStsUrl()) - .setCredentialSource(buildCredentialSource()) - .setServiceAccountImpersonationUrl( - transportFactory.transport.getServiceAccountImpersonationUrl()) - .setHttpTransportFactory(transportFactory) - .build(); + PluggableAuthCredentials.newBuilder() + .setAudience( + "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenInfoUrl("tokenInfoUrl") + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setCredentialSource(buildCredentialSource()) + .setServiceAccountImpersonationUrl( + transportFactory.transport.getServiceAccountImpersonationUrl()) + .setHttpTransportFactory(transportFactory) + .build(); credential = PluggableAuthCredentials.newBuilder(credential) @@ -257,20 +251,19 @@ public void refreshAccessToken_withServiceAccountImpersonationOptions() throws I transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); PluggableAuthCredentials credential = - (PluggableAuthCredentials) - PluggableAuthCredentials.newBuilder() - .setAudience( - "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") - .setSubjectTokenType("subjectTokenType") - .setTokenInfoUrl("tokenInfoUrl") - .setTokenUrl(transportFactory.transport.getStsUrl()) - .setCredentialSource(buildCredentialSource()) - .setServiceAccountImpersonationUrl( - transportFactory.transport.getServiceAccountImpersonationUrl()) - .setServiceAccountImpersonationOptions( - ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions(2800)) - .setHttpTransportFactory(transportFactory) - .build(); + PluggableAuthCredentials.newBuilder() + .setAudience( + "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenInfoUrl("tokenInfoUrl") + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setCredentialSource(buildCredentialSource()) + .setServiceAccountImpersonationUrl( + transportFactory.transport.getServiceAccountImpersonationUrl()) + .setServiceAccountImpersonationOptions( + ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions(2800)) + .setHttpTransportFactory(transportFactory) + .build(); credential = PluggableAuthCredentials.newBuilder(credential) @@ -402,26 +395,25 @@ public void pluggableAuthCredentialSource_missingExecutableCommandField_throws() public void builder_allFields() throws IOException { List scopes = Arrays.asList("scope1", "scope2"); - CredentialSource source = buildCredentialSource(); + PluggableAuthCredentialSource source = buildCredentialSource(); ExecutableHandler handler = options -> "Token"; PluggableAuthCredentials credentials = - (PluggableAuthCredentials) - PluggableAuthCredentials.newBuilder() - .setExecutableHandler(handler) - .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) - .setAudience("audience") - .setSubjectTokenType("subjectTokenType") - .setTokenUrl(STS_URL) - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(source) - .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) - .setQuotaProjectId("quotaProjectId") - .setClientId("clientId") - .setClientSecret("clientSecret") - .setScopes(scopes) - .setUniverseDomain("universeDomain") - .build(); + PluggableAuthCredentials.newBuilder() + .setExecutableHandler(handler) + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(source) + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setScopes(scopes) + .setUniverseDomain("universeDomain") + .build(); assertEquals(handler, credentials.getExecutableHandler()); assertEquals("audience", credentials.getAudience()); @@ -443,25 +435,24 @@ public void builder_allFields() throws IOException { public void builder_missingUniverseDomain_defaults() throws IOException { List scopes = Arrays.asList("scope1", "scope2"); - CredentialSource source = buildCredentialSource(); + PluggableAuthCredentialSource source = buildCredentialSource(); ExecutableHandler handler = options -> "Token"; PluggableAuthCredentials credentials = - (PluggableAuthCredentials) - PluggableAuthCredentials.newBuilder() - .setExecutableHandler(handler) - .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) - .setAudience("audience") - .setSubjectTokenType("subjectTokenType") - .setTokenUrl(STS_URL) - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(source) - .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) - .setQuotaProjectId("quotaProjectId") - .setClientId("clientId") - .setClientSecret("clientSecret") - .setScopes(scopes) - .build(); + PluggableAuthCredentials.newBuilder() + .setExecutableHandler(handler) + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(source) + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setScopes(scopes) + .build(); assertEquals(handler, credentials.getExecutableHandler()); assertEquals("audience", credentials.getAudience()); @@ -483,26 +474,25 @@ public void builder_missingUniverseDomain_defaults() throws IOException { public void newBuilder_allFields() throws IOException { List scopes = Arrays.asList("scope1", "scope2"); - CredentialSource source = buildCredentialSource(); + PluggableAuthCredentialSource source = buildCredentialSource(); ExecutableHandler handler = options -> "Token"; PluggableAuthCredentials credentials = - (PluggableAuthCredentials) - PluggableAuthCredentials.newBuilder() - .setExecutableHandler(handler) - .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) - .setAudience("audience") - .setSubjectTokenType("subjectTokenType") - .setTokenUrl(STS_URL) - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(source) - .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) - .setQuotaProjectId("quotaProjectId") - .setClientId("clientId") - .setClientSecret("clientSecret") - .setScopes(scopes) - .setUniverseDomain("universeDomain") - .build(); + PluggableAuthCredentials.newBuilder() + .setExecutableHandler(handler) + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(source) + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setScopes(scopes) + .setUniverseDomain("universeDomain") + .build(); PluggableAuthCredentials newBuilderCreds = PluggableAuthCredentials.newBuilder(credentials).build(); @@ -526,25 +516,24 @@ public void newBuilder_allFields() throws IOException { public void newBuilder_noUniverseDomain_defaults() throws IOException { List scopes = Arrays.asList("scope1", "scope2"); - CredentialSource source = buildCredentialSource(); + PluggableAuthCredentialSource source = buildCredentialSource(); ExecutableHandler handler = options -> "Token"; PluggableAuthCredentials credentials = - (PluggableAuthCredentials) - PluggableAuthCredentials.newBuilder() - .setExecutableHandler(handler) - .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) - .setAudience("audience") - .setSubjectTokenType("subjectTokenType") - .setTokenUrl(STS_URL) - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(source) - .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) - .setQuotaProjectId("quotaProjectId") - .setClientId("clientId") - .setClientSecret("clientSecret") - .setScopes(scopes) - .build(); + PluggableAuthCredentials.newBuilder() + .setExecutableHandler(handler) + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(source) + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setScopes(scopes) + .build(); PluggableAuthCredentials newBuilderCreds = PluggableAuthCredentials.newBuilder(credentials).build(); @@ -567,15 +556,14 @@ public void newBuilder_noUniverseDomain_defaults() throws IOException { @Test public void createdScoped_clonedCredentialWithAddedScopes() throws IOException { PluggableAuthCredentials credentials = - (PluggableAuthCredentials) - PluggableAuthCredentials.newBuilder(CREDENTIAL) - .setExecutableHandler(options -> "pluggableAuthToken") - .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) - .setQuotaProjectId("quotaProjectId") - .setClientId("clientId") - .setClientSecret("clientSecret") - .setUniverseDomain("universeDomain") - .build(); + PluggableAuthCredentials.newBuilder(CREDENTIAL) + .setExecutableHandler(options -> "pluggableAuthToken") + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setUniverseDomain("universeDomain") + .build(); List newScopes = Arrays.asList("scope1", "scope2"); @@ -601,25 +589,24 @@ public void createdScoped_clonedCredentialWithAddedScopes() throws IOException { @Test public void serialize() throws IOException, ClassNotFoundException { PluggableAuthCredentials testCredentials = - (PluggableAuthCredentials) - PluggableAuthCredentials.newBuilder(CREDENTIAL) - .setExecutableHandler(options -> "pluggableAuthToken") - .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) - .setQuotaProjectId("quotaProjectId") - .setClientId("clientId") - .setClientSecret("clientSecret") - .setUniverseDomain("universeDomain") - .build(); + PluggableAuthCredentials.newBuilder(CREDENTIAL) + .setExecutableHandler(options -> "pluggableAuthToken") + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setUniverseDomain("universeDomain") + .build(); // PluggableAuthCredentials are not serializable assertThrows(NotSerializableException.class, () -> serializeAndDeserialize(testCredentials)); } - private static CredentialSource buildCredentialSource() { + private static PluggableAuthCredentialSource buildCredentialSource() { return buildCredentialSource("command", null, null); } - private static CredentialSource buildCredentialSource( + private static PluggableAuthCredentialSource buildCredentialSource( String command, @Nullable String timeoutMs, @Nullable String outputFile) { Map source = new HashMap<>(); Map executable = new HashMap<>();