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
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
);
});
});
9 changes: 9 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,8 @@ 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 +518,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