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: adding configurable token lifespan support #1441

Merged
merged 8 commits into from
Aug 23, 2022
Merged
31 changes: 28 additions & 3 deletions .readme-partials.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,8 @@ body: |-
projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$AWS_PROVIDER_ID \
--service-account $SERVICE_ACCOUNT_EMAIL \
--aws \
# Optional argument for specifying the duration of the service account access token.
# --service-account-token-lifetime-seconds $TOKEN_LIFETIME \
--output-file /path/to/generated/config.json
```

Expand All @@ -359,9 +361,12 @@ body: |-
- `$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.

This will generate the configuration file in the specified output file.

The `service-account-token-lifetime-seconds` flag is optional. If not provided, this defaults to one hour. 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

If you want to use the AWS IMDSv2 flow, you can add the field below to the credential_source in your AWS ADC configuration file:
"imdsv2_session_token_url": "http://169.254.169.254/latest/api/token"
The gcloud create-cred-config command will be updated to support this soon.
Expand Down Expand Up @@ -390,6 +395,8 @@ body: |-
projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$AZURE_PROVIDER_ID \
--service-account $SERVICE_ACCOUNT_EMAIL \
--azure \
# Optional argument for specifying the duration of the service account access token.
# --service-account-token-lifetime-seconds $TOKEN_LIFETIME \
--output-file /path/to/generated/config.json
```

Expand All @@ -398,9 +405,12 @@ body: |-
- `$POOL_ID`: The workload identity pool ID.
- `$AZURE_PROVIDER_ID`: The Azure 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.

This will generate the configuration file in the specified output file.

The `service-account-token-lifetime-seconds` flag is optional. If not provided, this defaults to one hour. 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.

You can now [start using the Auth library](#using-external-identities) to call Google Cloud resources from Azure.

### Accessing resources from an OIDC identity provider
Expand Down Expand Up @@ -435,6 +445,8 @@ body: |-
# Optional argument for the field that contains the OIDC credential.
# This is required for json.
# --credential-source-field-name "id_token" \
# Optional argument for specifying the duration of the service account access token.
# --service-account-token-lifetime-seconds $TOKEN_LIFETIME \
--output-file /path/to/generated/config.json
```

Expand All @@ -444,9 +456,12 @@ body: |-
- `$OIDC_PROVIDER_ID`: The OIDC provider ID.
- `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate.
- `$PATH_TO_OIDC_ID_TOKEN`: The file path where the OIDC token will be retrieved from.

- `$TOKEN_LIFETIME`: The desired lifetime duration of the service account access token in seconds.

This will generate the configuration file in the specified output file.

The `service-account-token-lifetime-seconds` flag is optional. If not provided, this defaults to one hour. 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.

**URL-sourced credentials**
For URL-sourced credentials, a local server needs to host a GET endpoint to return the OIDC token. The response can be in plain text or JSON.
Additional required request headers can also be specified.
Expand All @@ -465,6 +480,8 @@ body: |-
# Optional argument for the field that contains the OIDC credential.
# This is required for json.
# --credential-source-field-name "id_token" \
# Optional argument for specifying the duration of the service account access token.
# --service-account-token-lifetime-seconds $TOKEN_LIFETIME \
--output-file /path/to/generated/config.json
```

Expand All @@ -475,6 +492,9 @@ body: |-
- `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate.
- `$URL_TO_GET_OIDC_TOKEN`: The URL of the local server endpoint to call to retrieve the OIDC token.
- `$HEADER_KEY` and `$HEADER_VALUE`: The additional header key/value pairs to pass along the GET request to `$URL_TO_GET_OIDC_TOKEN`, e.g. `Metadata-Flavor=Google`.
- `$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. 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.

#### Using Executable-sourced credentials with OIDC and SAML

Expand Down Expand Up @@ -502,6 +522,8 @@ body: |-
# Optional argument for the absolute path to the executable output file.
# See below on how this argument impacts the library behaviour.
# --executable-output-file=$EXECUTABLE_OUTPUT_FILE \
# Optional argument for specifying the duration of the service account access token.
# --service-account-token-lifetime-seconds $TOKEN_LIFETIME \
--output-file /path/to/generated/config.json
```
Where the following variables need to be substituted:
Expand All @@ -511,6 +533,7 @@ body: |-
- `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate.
- `$SUBJECT_TOKEN_TYPE`: The subject token type.
- `$EXECUTABLE_COMMAND`: The full command to run, including arguments. Must be an absolute path to the program.
- `$TOKEN_LIFETIME`: The desired lifetime duration of the service account access token in seconds.

The `--executable-timeout-millis` flag is optional. This is the duration for which
the auth library will wait for the executable to finish, in milliseconds.
Expand All @@ -527,6 +550,8 @@ body: |-
this location. The format of contents in the file should match the JSON format
expected by the executable shown below.

The `service-account-token-lifetime-seconds` flag is optional. If not provided, this defaults to one hour. 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.

To retrieve the 3rd party token, the library will call the executable
using the command specified. The executable's output must adhere to the response format
specified below. It must output the response to stdout.
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