Skip to content

Commit

Permalink
feat: adding configurable token lifespan support (#1441)
Browse files Browse the repository at this point in the history
* feat: adding support for configurable token lifespan

* fix: fixing lint error and adding documentation

* fix: changing readme

* fix: removing unintentional whitespace

* fix: minor readme edits

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

Co-authored-by: Daniel Bankhead <danielbankhead@google.com>
Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com>
Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
  • Loading branch information
4 people authored Aug 23, 2022
1 parent 4436e6c commit 178e3b8
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 1 deletion.
28 changes: 28 additions & 0 deletions .readme-partials.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,34 @@ body: |-
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.
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.
### Using External Identities
External identities (AWS, Azure and OIDC-based providers) can be used with `Application Default Credentials`.
Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,34 @@ 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.

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.

### Using External Identities

External identities (AWS, Azure and OIDC-based providers) can be used with `Application Default Credentials`.
Expand Down
38 changes: 37 additions & 1 deletion samples/test/externalclient.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ const {assert} = require('chai');
const {describe, it, before, afterEach} = require('mocha');
const fs = require('fs');
const {promisify} = require('util');
const {GoogleAuth, DefaultTransporter} = require('google-auth-library');
const {
GoogleAuth,
DefaultTransporter,
IdentityPoolClient,
} = require('google-auth-library');
const os = require('os');
const path = require('path');
const http = require('http');
Expand Down Expand Up @@ -472,4 +476,36 @@ describe('samples for external-account', () => {
// Confirm expected script output.
assert.match(output, /DNS Info:/);
});

it('should acquire access token with service account impersonation options', async () => {
// Create file-sourced configuration JSON file.
// The created OIDC token will be used as the subject token and will be
// retrieved from a file location.
const config = {
type: 'external_account',
audience: AUDIENCE_OIDC,
subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',
token_url: 'https://sts.googleapis.com/v1/token',
service_account_impersonation_url:
'https://iamcredentials.googleapis.com/v1/projects/' +
`-/serviceAccounts/${clientEmail}:generateAccessToken`,
service_account_impersonation: {
token_lifetime_seconds: 2800,
},
credential_source: {
file: oidcTokenFilePath,
},
};
await writeFile(oidcTokenFilePath, oidcToken);
const client = new IdentityPoolClient(config);

const minExpireTime = new Date().getTime() + (2800 * 1000 - 5 * 1000);
const maxExpireTime = new Date().getTime() + (2800 * 1000 + 5 * 1000);
const token = await client.getAccessToken();
const actualExpireTime = new Date(token.res.data.expireTime).getTime();

assert.isTrue(
minExpireTime <= actualExpireTime && actualExpireTime <= maxExpireTime
);
});
});
10 changes: 10 additions & 0 deletions src/auth/baseexternalclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ const DEFAULT_OAUTH_SCOPE = 'https://www.googleapis.com/auth/cloud-platform';
const GOOGLE_APIS_DOMAIN_PATTERN = '\\.googleapis\\.com$';
/** The variable portion pattern in a Google APIs domain. */
const VARIABLE_PORTION_PATTERN = '[^\\.\\s\\/\\\\]+';
/** Default impersonated token lifespan in seconds.*/
const DEFAULT_TOKEN_LIFESPAN = 3600;

/**
* Offset to take into account network delays and server clock skews.
Expand Down Expand Up @@ -69,6 +71,9 @@ export interface BaseExternalAccountClientOptions {
audience: string;
subject_token_type: string;
service_account_impersonation_url?: string;
service_account_impersonation?: {
token_lifetime_seconds?: number;
};
token_url: string;
token_info_url?: string;
client_id?: string;
Expand Down Expand Up @@ -130,6 +135,7 @@ export abstract class BaseExternalAccountClient extends AuthClient {
protected readonly audience: string;
protected readonly subjectTokenType: string;
private readonly serviceAccountImpersonationUrl?: string;
private readonly serviceAccountImpersonationLifetime?: number;
private readonly stsCredential: sts.StsCredentials;
private readonly clientAuth?: ClientAuthentication;
private readonly workforcePoolUserProject?: string;
Expand Down Expand Up @@ -203,6 +209,9 @@ export abstract class BaseExternalAccountClient extends AuthClient {
}
this.serviceAccountImpersonationUrl =
options.service_account_impersonation_url;
this.serviceAccountImpersonationLifetime =
options.service_account_impersonation?.token_lifetime_seconds ??
DEFAULT_TOKEN_LIFESPAN;
// As threshold could be zero,
// eagerRefreshThresholdMillis || EXPIRATION_TIME_OFFSET will override the
// zero value.
Expand Down Expand Up @@ -510,6 +519,7 @@ export abstract class BaseExternalAccountClient extends AuthClient {
},
data: {
scope: this.getScopesArray(),
lifetime: this.serviceAccountImpersonationLifetime + 's',
},
responseType: 'json',
};
Expand Down
4 changes: 4 additions & 0 deletions test/externalclienthelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ interface NockMockGenerateAccessToken {
token: string;
response: IamGenerateAccessTokenResponse | CloudRequestError;
scopes: string[];
lifetime?: number;
}

const defaultLifetime = 3600;
const defaultProjectNumber = '123456';
const poolId = 'POOL_ID';
const providerId = 'PROVIDER_ID';
Expand Down Expand Up @@ -86,6 +88,8 @@ export function mockGenerateAccessToken(
saPath,
{
scope: nockMockGenerateAccessToken.scopes,
lifetime:
(nockMockGenerateAccessToken.lifetime ?? defaultLifetime) + 's',
},
{
reqheaders: {
Expand Down
54 changes: 54 additions & 0 deletions test/test.baseexternalclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1623,6 +1623,60 @@ describe('BaseExternalAccountClient', () => {
});
scopes.forEach(scope => scope.done());
});

it('should use provided token lifespan', async () => {
const scopes: nock.Scope[] = [];
scopes.push(
mockStsTokenExchange([
{
statusCode: 200,
response: stsSuccessfulResponse,
request: {
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
audience,
scope: 'https://www.googleapis.com/auth/cloud-platform',
requested_token_type:
'urn:ietf:params:oauth:token-type:access_token',
subject_token: 'subject_token_0',
subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',
},
},
])
);
scopes.push(
mockGenerateAccessToken([
{
statusCode: 200,
response: saSuccessResponse,
token: stsSuccessfulResponse.access_token,
lifetime: 2800,
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
},
])
);

const externalAccountOptionsWithSATokenLifespan = Object.assign(
{
service_account_impersonation: {
token_lifetime_seconds: 2800,
},
},
externalAccountOptionsWithSA
);

const client = new TestExternalAccountClient(
externalAccountOptionsWithSATokenLifespan
);
const actualResponse = await client.getAccessToken();

// Confirm raw GaxiosResponse appended to response.
assertGaxiosResponsePresent(actualResponse);
delete actualResponse.res;
assert.deepStrictEqual(actualResponse, {
token: saSuccessResponse.accessToken,
});
scopes.forEach(scope => scope.done());
});
});
});

Expand Down

0 comments on commit 178e3b8

Please sign in to comment.