Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adds support for user defined subject token suppliers in AWSCredentials and IdentityPoolCredentials #1336

Merged
merged 44 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b21127a
feat: adds programmatic auth credentials for identity pool and aws cr…
aeitzman Nov 27, 2023
e6457d9
feat: add quality of life improvements for building external account …
aeitzman Nov 27, 2023
448a8ec
fix: formatting
aeitzman Nov 27, 2023
59eb856
fix: add formatting
aeitzman Nov 27, 2023
f2ab1a2
Merge remote-tracking branch 'upstream/main' into fix_builders
aeitzman Nov 27, 2023
0495b7f
Adds @CanIgnoreReturnValue on new builder methods
aeitzman Nov 27, 2023
8d12e07
Merge remote-tracking branch 'upstream/main' into programmatic-auth
aeitzman Nov 27, 2023
a8b2f92
Change test for impersonated credentials
aeitzman Dec 1, 2023
616fb13
formatting
aeitzman Dec 1, 2023
b2552eb
adding id_token type
aeitzman Dec 1, 2023
d32e19c
Merge branch 'fix_builders' into programmatic-auth
aeitzman Dec 1, 2023
6726160
formatting
aeitzman Dec 1, 2023
2fc4f99
Update oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java
aeitzman Dec 5, 2023
e5a9c59
PR comments
aeitzman Dec 5, 2023
bfb83fa
Added header value constants
aeitzman Dec 5, 2023
6e7a975
Merge branch 'main' into programmatic-auth
lsirac Dec 6, 2023
164ac25
updating java doc
aeitzman Dec 7, 2023
a257e55
adding integration tests
aeitzman Dec 7, 2023
f09adfa
fix tests
aeitzman Dec 7, 2023
97946b3
fix tests, add javadoc, and format
aeitzman Dec 7, 2023
eb08391
PR review comments
aeitzman Dec 11, 2023
5ae2645
Update oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java
aeitzman Dec 12, 2023
61f6ae5
PR comments
aeitzman Dec 12, 2023
e43d708
changing to aws_region instead of region to clarify usage and keep re…
aeitzman Dec 13, 2023
bd4604f
Merge branch 'main' into programmatic-auth
aeitzman Dec 18, 2023
08bde82
Merge branch 'main' into programmatic-auth
lsirac Dec 20, 2023
4a22e08
Adding Aws Security Credential Providers
aeitzman Jan 8, 2024
f40743e
Merge remote-tracking branch 'upstream/main' into programmatic-auth
aeitzman Jan 8, 2024
f480b84
Adding identity pool providers
aeitzman Jan 8, 2024
188b803
PR comments
aeitzman Jan 9, 2024
e69108e
fix test
aeitzman Jan 9, 2024
4f3e253
refactoring to expose rename provider to supplier and expose it publicly
aeitzman Jan 10, 2024
c22a4e9
Merge branch 'main' into programmatic-auth
aeitzman Jan 10, 2024
dd659c4
formatting
aeitzman Jan 10, 2024
9cd8d96
Merge branch 'main' into programmatic-auth
lsirac Jan 12, 2024
8963d68
updating codeowners
aeitzman Jan 17, 2024
f4cadd2
Merge branch 'main' into programmatic-auth
lsirac Jan 18, 2024
20c2f7d
make subject token supplier interface public
aeitzman Jan 19, 2024
abd462b
Making AwsSecurityCredentials public and change name to sessionToken
aeitzman Jan 22, 2024
9835df7
lint
aeitzman Jan 22, 2024
d06eb36
Merge branch 'main' into programmatic-auth
aeitzman Jan 23, 2024
d59fb92
Merge remote-tracking branch 'upstream/main' into programmatic-auth
aeitzman Jan 25, 2024
4371463
fix tests
aeitzman Jan 25, 2024
5b21010
lint
aeitzman Jan 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
412 changes: 227 additions & 185 deletions oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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;
import java.util.function.Supplier;

/**
* Provider for retrieving AWS Security credentials for {@Link AwsCredentials} to exchange for GCP
* access tokens.
*/
abstract class AwsSecurityCredentialsProvider implements Serializable {
aeitzman marked this conversation as resolved.
Show resolved Hide resolved

/**
* Gets the AWS region to use.
*
* @return the AWS region that should be used for the credential.
* @throws IOException
*/
abstract String getRegion() throws IOException;

/**
* Gets AWS security credentials.
*
* @return valid AWS security credentials that can be exchanged for a GCP access token.
* @throws IOException
*/
abstract AwsSecurityCredentials getCredentials() throws IOException;

/**
* Gets the metrics header value that should be used for the sts request.
*
* @return the metrics header value.
*/
abstract String getMetricsHeaderValue();

/**
* Gets the Aws security credential supplier.
*
* @return the Supplier used to retrieve the AWS security credentials.
*/
abstract Supplier<AwsSecurityCredentials> getSupplier();
aeitzman marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,22 +66,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<String, Object> 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;
Expand All @@ -103,11 +96,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;
Copy link
Member

Choose a reason for hiding this comment

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

I understand it was technically not final before, could you please remind why? Does it change multiple times? if once - it can be final and can make the rest of the logic simpler

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The issue with having impersonated credentials final is that the ImpersonatedCredentials object is built by passing the current credential as the SourceCredential. If we have it final, the Impersonated Credential needs to get created in the parent constructor before the child constructor runs, which means the credential object that gets passed as the source credential is not fully instantiated. As a series of steps, the exact problem is:

  1. AwsCredentials constructor is called via the builder
  2. constructor super() is called, going to the External account credentials constructor
  3. in the parent constructor, buildImpersonatedCredentials is called
  4. the ImpersonatedCredentials object is built, but the source credential passed doesn't have any of the AwsCredentials specific builder variables set (AwsSecurityCredentialsSupplier for example) because the rest of the AwsCredentials constructor logic has not run yet, we are still in the parent constructor.

Since the source credential passed to the impersonated credential isn't actually correct here we have 1 of two options if we want any child class specific builder setters
1: Set the impersonatedCredentialOverride in the child constructor and just use that, ignoring the "final" impersonatedCredentials. This is what we do for executableCredentials, but the issue is that its kind of a weird pattern that we just have the unused and incorrect impersonatedCredentials object attached to the credential, and that it means we can't do any input validation on the child credential.
2: Make impersonatedCredentials not final, and just set it when we use it the first time when retrieving an access token. This lets us get rid of the override, and makes it so the impersonatedCredential is never built with the incorrect source credential.


private EnvironmentProvider environmentProvider;

Expand Down Expand Up @@ -223,8 +212,6 @@ protected ExternalAccountCredentials(
}

this.metricsHandler = new ExternalAccountMetricsHandler(this);

this.impersonatedCredentials = buildImpersonatedCredentials();
}

/**
Expand All @@ -242,12 +229,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)
Expand Down Expand Up @@ -276,8 +263,6 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder)
builder.metricsHandler == null
? new ExternalAccountMetricsHandler(this)
: builder.metricsHandler;

this.impersonatedCredentials = buildImpersonatedCredentials();
aeitzman marked this conversation as resolved.
Show resolved Hide resolved
}

ImpersonatedCredentials buildImpersonatedCredentials() {
Expand Down Expand Up @@ -315,10 +300,6 @@ ImpersonatedCredentials buildImpersonatedCredentials() {
.build();
}

void overrideImpersonatedCredentials(ImpersonatedCredentials credentials) {
this.impersonatedCredentialsOverride = credentials;
}

@Override
public void getRequestMetadata(
URI uri, Executor executor, final RequestMetadataCallback callback) {
Expand Down Expand Up @@ -478,6 +459,10 @@ private static boolean isAwsCredential(Map<String, Object> 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.
*
Expand All @@ -488,10 +473,10 @@ private static boolean isAwsCredential(Map<String, Object> 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) {
if (this.shouldBuildImpersonatedCredential()) {
this.impersonatedCredentials = this.buildImpersonatedCredentials();
}
if (impersonatedCredentials != null) {
aeitzman marked this conversation as resolved.
Show resolved Hide resolved
return impersonatedCredentials.refreshAccessToken();
}

Expand Down Expand Up @@ -791,6 +776,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.
*
Expand Down Expand Up @@ -943,4 +941,30 @@ Builder setEnvironmentProvider(EnvironmentProvider environmentProvider) {

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<String, Object> credentialSourceMap) {
checkNotNull(credentialSourceMap);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class ExternalAccountMetricsHandler implements java.io.Serializable {

private final boolean configLifetime;
private final boolean saImpersonation;
private String credentialSourceType;
private ExternalAccountCredentials credentials;
TimurSadykov marked this conversation as resolved.
Show resolved Hide resolved

/**
* Constructor for the external account metrics handler.
Expand All @@ -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;
}

/**
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* 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.IdentityPoolCredentials.FILE_METRICS_HEADER_VALUE;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Paths;
import java.util.function.Supplier;

/**
* Internal provider for retrieving subject tokens for {@Link IdentityPoolCredentials} to exchange
* for GCP access tokens via a local file.
*/
class FileIdentityPoolSubjectTokenProvider extends IdentityPoolSubjectTokenProvider {

private final long serialVersionUID = 2475549052347431992L;

private final IdentityPoolCredentialSource credentialSource;

/**
* Constructor for FileIdentitySubjectTokenProvider
*
* @param credentialSource the credential source to use.
*/
FileIdentityPoolSubjectTokenProvider(IdentityPoolCredentialSource credentialSource) {
this.credentialSource = credentialSource;
}

@Override
String getSubjectToken() throws IOException {
return this.retrieveSubjectTokenFromCredentialFile();
}

@Override
String getMetricsHeaderValue() {
return FILE_METRICS_HEADER_VALUE;
}

@Override
Supplier<String> getSupplier() {
return () -> {
try {
return this.getSubjectToken();
} catch (IOException e) {
throw new RuntimeException(e);
}
};
}

private String retrieveSubjectTokenFromCredentialFile() 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);
}
}
}
Loading