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 configurable token lifetime support #982

Merged
merged 13 commits into from Sep 7, 2022
28 changes: 28 additions & 0 deletions README.md
Expand Up @@ -29,6 +29,7 @@ credentials as well as utility methods to create them and to get Application Def
* [Accessing resources from Azure](#access-resources-from-microsoft-azure)
* [Accessing resources from an OIDC identity provider](#accessing-resources-from-an-oidc-identity-provider)
* [Accessing resources using Executable-sourced credentials](#using-executable-sourced-credentials-with-oidc-and-saml)
* [Configurable Token Lifetime](#configurable-token-lifetime)
* [Workforce Identity Federation](#workforce-identity-federation)
* [Accessing resources using an OIDC or SAML 2.0 identity provider](#accessing-resources-using-an-oidc-or-saml-20-identity-provider)
* [Accessing resources using Executable-sourced credentials](#using-executable-sourced-workforce-credentials-with-oidc-and-saml)
Expand Down Expand Up @@ -467,6 +468,33 @@ credentials unless they do not meet your specific requirements.
You can now [use the Auth library](#using-external-identities) to call Google Cloud
resources from an OIDC or SAML provider.

#### Configurable Token Lifetime
When creating a credential configuration with workload identity federation using service account impersonation, you can provide an optional argument to configure the service account access token lifetime.

To generate the configuration with configurable token lifetime, run the following command (this example uses an AWS configuration, but the token lifetime can be configured for all workload identity federation providers):
```bash
# Generate an AWS configuration file with configurable token lifetime.
gcloud iam workload-identity-pools create-cred-config \
projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$AWS_PROVIDER_ID \
--service-account $SERVICE_ACCOUNT_EMAIL \
--aws \
--output-file /path/to/generated/config.json \
--service-account-token-lifetime-seconds $TOKEN_LIFETIME
```

Where the following variables need to be substituted:
- `$PROJECT_NUMBER`: The Google Cloud project number.
- `$POOL_ID`: The workload identity pool ID.
- `$AWS_PROVIDER_ID`: The AWS provider ID.
- `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate.
- `$TOKEN_LIFETIME`: The desired lifetime duration of the service account access token in seconds.

The `service-account-token-lifetime-seconds` flag is optional. If not provided, this defaults to one hour.
The minimum allowed value is 600 (10 minutes) and the maximum allowed value is 43200 (12 hours).
If a lifetime greater than one hour is required, the service account must be added as an allowed value in an Organization Policy that enforces the `constraints/iam.allowServiceAccountCredentialLifetimeExtension` constraint.
aeitzman marked this conversation as resolved.
Show resolved Hide resolved

aeitzman marked this conversation as resolved.
Show resolved Hide resolved
Note that configuring a short lifetime (e.g. 10 minutes) will result in the library initiating the entire token exchange flow every 10 minutes, which will call the 3rd party token provider even if the 3rd party token is not expired.

### Workforce Identity Federation

[Workforce identity federation](https://cloud.google.com/iam/docs/workforce-identity-federation) lets you use an
Expand Down
Expand Up @@ -43,11 +43,13 @@
import com.google.common.base.MoreObjects;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
Expand All @@ -73,6 +75,49 @@ abstract static class CredentialSource {
}
}

/**
* Encapsulates the service account impersonation options portion of the configuration for
* ExternalAccountCredentials.
*
* <p>If token_lifetime_seconds is not specified, the library will default to a 1-hour lifetime.
*
* <pre>
* Sample configuration:
* {
* ...
* "service_account_impersonation": {
* "token_lifetime_seconds": 2800
* }
* }
* </pre>
*/
public static final class ServiceAccountImpersonationOptions {
aeitzman marked this conversation as resolved.
Show resolved Hide resolved
aeitzman marked this conversation as resolved.
Show resolved Hide resolved
private static final int DEFAULT_TOKEN_LIFETIME_SECONDS = 3600;
private static final String TOKEN_LIFETIME_SECONDS_KEY = "token_lifetime_seconds";

private final int lifetime;

ServiceAccountImpersonationOptions(Map<String, Object> optionsMap) {
aeitzman marked this conversation as resolved.
Show resolved Hide resolved
if (!optionsMap.containsKey(TOKEN_LIFETIME_SECONDS_KEY)) {
lifetime = DEFAULT_TOKEN_LIFETIME_SECONDS;
return;
}

Object timeout = optionsMap.get(TOKEN_LIFETIME_SECONDS_KEY);
aeitzman marked this conversation as resolved.
Show resolved Hide resolved
if (timeout instanceof BigDecimal) {
lifetime = ((BigDecimal) timeout).intValue();
} else if (optionsMap.get(TOKEN_LIFETIME_SECONDS_KEY) instanceof Integer) {
lifetime = (int) timeout;
} else {
lifetime = Integer.parseInt((String) timeout);
}
}

int getLifetime() {
return lifetime;
}
}

private static final String CLOUD_PLATFORM_SCOPE =
"https://www.googleapis.com/auth/cloud-platform";

Expand All @@ -85,6 +130,7 @@ abstract static class CredentialSource {
private final String tokenUrl;
private final CredentialSource credentialSource;
private final Collection<String> scopes;
private final ServiceAccountImpersonationOptions serviceAccountImpersonationOptions;

@Nullable private final String tokenInfoUrl;
@Nullable private final String serviceAccountImpersonationUrl;
Expand Down Expand Up @@ -194,6 +240,8 @@ protected ExternalAccountCredentials(
this.environmentProvider =
environmentProvider == null ? SystemEnvironmentProvider.getInstance() : environmentProvider;
this.workforcePoolUserProject = null;
this.serviceAccountImpersonationOptions =
new ServiceAccountImpersonationOptions(new HashMap<String, Object>());

validateTokenUrl(tokenUrl);
if (serviceAccountImpersonationUrl != null) {
Expand Down Expand Up @@ -230,6 +278,10 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder)
builder.environmentProvider == null
? SystemEnvironmentProvider.getInstance()
: builder.environmentProvider;
this.serviceAccountImpersonationOptions =
builder.serviceAccountImpersonationOptions == null
? new ServiceAccountImpersonationOptions(new HashMap<String, Object>())
: builder.serviceAccountImpersonationOptions;

this.workforcePoolUserProject = builder.workforcePoolUserProject;
if (workforcePoolUserProject != null && !isWorkforcePoolConfiguration()) {
Expand Down Expand Up @@ -275,7 +327,7 @@ ImpersonatedCredentials buildImpersonatedCredentials() {
.setHttpTransportFactory(transportFactory)
.setTargetPrincipal(targetPrincipal)
.setScopes(new ArrayList<>(scopes))
.setLifetime(3600) // 1 hour in seconds
.setLifetime(this.serviceAccountImpersonationOptions.lifetime)
.setIamEndpointOverride(serviceAccountImpersonationUrl)
.build();
}
Expand Down Expand Up @@ -375,6 +427,14 @@ static ExternalAccountCredentials fromJson(
String clientSecret = (String) json.get("client_secret");
String quotaProjectId = (String) json.get("quota_project_id");
String userProject = (String) json.get("workforce_pool_user_project");
Map<String, Object> impersonationOptionsMap =
(Map<String, Object>) json.get("service_account_impersonation");

ServiceAccountImpersonationOptions serviceAccountImpersonationOptions = null;
if (impersonationOptionsMap != null) {
serviceAccountImpersonationOptions =
new ServiceAccountImpersonationOptions(impersonationOptionsMap);
}

if (isAwsCredential(credentialSourceMap)) {
return AwsCredentials.newBuilder()
Expand All @@ -388,6 +448,7 @@ static ExternalAccountCredentials fromJson(
.setQuotaProjectId(quotaProjectId)
.setClientId(clientId)
.setClientSecret(clientSecret)
.setServiceAccountImpersonationOptions(serviceAccountImpersonationOptions)
.build();
} else if (isPluggableAuthCredential(credentialSourceMap)) {
return PluggableAuthCredentials.newBuilder()
Expand All @@ -402,6 +463,7 @@ static ExternalAccountCredentials fromJson(
.setClientId(clientId)
.setClientSecret(clientSecret)
.setWorkforcePoolUserProject(userProject)
.setServiceAccountImpersonationOptions(serviceAccountImpersonationOptions)
.build();
}
return IdentityPoolCredentials.newBuilder()
Expand All @@ -416,6 +478,7 @@ static ExternalAccountCredentials fromJson(
.setClientId(clientId)
.setClientSecret(clientSecret)
.setWorkforcePoolUserProject(userProject)
.setServiceAccountImpersonationOptions(serviceAccountImpersonationOptions)
.build();
}

Expand Down Expand Up @@ -539,6 +602,11 @@ public String getWorkforcePoolUserProject() {
return workforcePoolUserProject;
}

@Nullable
public ServiceAccountImpersonationOptions getServiceAccountImpersonationOptions() {
return serviceAccountImpersonationOptions;
}

EnvironmentProvider getEnvironmentProvider() {
return environmentProvider;
}
Expand Down Expand Up @@ -625,6 +693,7 @@ public abstract static class Builder extends GoogleCredentials.Builder {
@Nullable protected String clientSecret;
@Nullable protected Collection<String> scopes;
@Nullable protected String workforcePoolUserProject;
@Nullable protected ServiceAccountImpersonationOptions serviceAccountImpersonationOptions;

protected Builder() {}

Expand All @@ -642,6 +711,7 @@ protected Builder(ExternalAccountCredentials credentials) {
this.scopes = credentials.scopes;
this.environmentProvider = credentials.environmentProvider;
this.workforcePoolUserProject = credentials.workforcePoolUserProject;
this.serviceAccountImpersonationOptions = credentials.serviceAccountImpersonationOptions;
}

/** Sets the HTTP transport factory, creates the transport used to get access tokens. */
Expand Down Expand Up @@ -733,6 +803,13 @@ public Builder setWorkforcePoolUserProject(String workforcePoolUserProject) {
return this;
}

/** Sets the optional service account impersonation options. */
aeitzman marked this conversation as resolved.
Show resolved Hide resolved
public Builder setServiceAccountImpersonationOptions(
aeitzman marked this conversation as resolved.
Show resolved Hide resolved
ServiceAccountImpersonationOptions serviceAccountImpersonationOptions) {
this.serviceAccountImpersonationOptions = serviceAccountImpersonationOptions;
return this;
}

Builder setEnvironmentProvider(EnvironmentProvider environmentProvider) {
this.environmentProvider = environmentProvider;
return this;
Expand Down
Expand Up @@ -42,6 +42,7 @@
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
import com.google.auth.TestUtils;
import com.google.auth.oauth2.AwsCredentials.AwsCredentialSource;
import com.google.auth.oauth2.ExternalAccountCredentials.ServiceAccountImpersonationOptions;
import com.google.auth.oauth2.ExternalAccountCredentialsTest.MockExternalAccountCredentialsTransportFactory;
import java.io.IOException;
import java.io.InputStream;
Expand Down Expand Up @@ -142,6 +143,41 @@ public void refreshAccessToken_withServiceAccountImpersonation() throws IOExcept
transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue());
}

@Test
public void refreshAccessToken_withServiceAccountImpersonationOptions() throws IOException {
MockExternalAccountCredentialsTransportFactory transportFactory =
new MockExternalAccountCredentialsTransportFactory();

transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime());

AwsCredentials awsCredential =
(AwsCredentials)
AwsCredentials.newBuilder(AWS_CREDENTIAL)
.setTokenUrl(transportFactory.transport.getStsUrl())
.setServiceAccountImpersonationUrl(
transportFactory.transport.getServiceAccountImpersonationUrl())
.setHttpTransportFactory(transportFactory)
.setCredentialSource(buildAwsCredentialSource(transportFactory))
.setServiceAccountImpersonationOptions(
new ServiceAccountImpersonationOptions(
ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions(
2800)))
aeitzman marked this conversation as resolved.
Show resolved Hide resolved
.build();

AccessToken accessToken = awsCredential.refreshAccessToken();

assertEquals(
transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue());

// Validate that default lifetime was set correctly on the request.
GenericJson query =
OAuth2Utils.JSON_FACTORY
.createJsonParser(transportFactory.transport.getLastRequest().getContentAsString())
.parseAndClose(GenericJson.class);

assertEquals("2800s", query.get("lifetime"));
}

@Test
public void retrieveSubjectToken() throws IOException {
MockExternalAccountCredentialsTransportFactory transportFactory =
Expand Down