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: add workforce config support. #1251

Merged
merged 15 commits into from
Sep 28, 2021
44 changes: 37 additions & 7 deletions src/auth/baseexternalclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export const EXTERNAL_ACCOUNT_TYPE = 'external_account';
/** Cloud resource manager URL used to retrieve project information. */
export const CLOUD_RESOURCE_MANAGER =
'https://cloudresourcemanager.googleapis.com/v1/projects/';
/** The workforce audience pattern. */
const WORKFORCE_AUDIENCE_PATTERN =
'//iam.googleapis.com/locations/[^/]+/workforcePools/[^/]+/providers/.+';

/**
* Base external account credentials json interface.
Expand All @@ -71,6 +74,7 @@ export interface BaseExternalAccountClientOptions {
client_id?: string;
client_secret?: string;
quota_project_id?: string;
workforce_pool_user_project?: string;
xil222 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down Expand Up @@ -127,6 +131,8 @@ export abstract class BaseExternalAccountClient extends AuthClient {
private readonly subjectTokenType: string;
private readonly serviceAccountImpersonationUrl?: string;
private readonly stsCredential: sts.StsCredentials;
private readonly clientAuth?: ClientAuthentication;
private readonly workforcePoolUserProject?: string;
public projectId: string | null;
public projectNumber: string | null;
public readonly eagerRefreshThresholdMillis: number;
Expand All @@ -152,7 +158,7 @@ export abstract class BaseExternalAccountClient extends AuthClient {
`received "${options.type}"`
);
}
const clientAuth = options.client_id
this.clientAuth = options.client_id
? ({
confidentialClientType: 'basic',
clientId: options.client_id,
Expand All @@ -162,13 +168,27 @@ export abstract class BaseExternalAccountClient extends AuthClient {
if (!this.validateGoogleAPIsUrl('sts', options.token_url)) {
throw new Error(`"${options.token_url}" is not a valid token url.`);
}
this.stsCredential = new sts.StsCredentials(options.token_url, clientAuth);
this.stsCredential = new sts.StsCredentials(
options.token_url,
this.clientAuth
);
// Default OAuth scope. This could be overridden via public property.
this.scopes = [DEFAULT_OAUTH_SCOPE];
this.cachedAccessToken = null;
this.audience = options.audience;
this.subjectTokenType = options.subject_token_type;
this.quotaProjectId = options.quota_project_id;
this.workforcePoolUserProject = options.workforce_pool_user_project;
xil222 marked this conversation as resolved.
Show resolved Hide resolved
const workforceAudiencePattern = new RegExp(WORKFORCE_AUDIENCE_PATTERN);
if (
this.workforcePoolUserProject &&
!this.audience.match(workforceAudiencePattern)
) {
throw new Error(
'workforcePoolUserProject should not be set for non-workforce pool ' +
'credentials.'
);
}
if (
typeof options.service_account_impersonation_url !== 'undefined' &&
!this.validateGoogleAPIsUrl(
Expand Down Expand Up @@ -290,8 +310,9 @@ export abstract class BaseExternalAccountClient extends AuthClient {

/**
* @return A promise that resolves with the project ID corresponding to the
* current workload identity pool. When not determinable, this resolves with
* null.
* current workload identity pool or current workforce pool if
* determinable. For workforce pool credential, it returns the project ID
* corresponding to the workforcePoolUserProject.
* This is introduced to match the current pattern of using the Auth
* library:
* const projectId = await auth.getProjectId();
Expand All @@ -303,15 +324,16 @@ export abstract class BaseExternalAccountClient extends AuthClient {
* https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes
*/
async getProjectId(): Promise<string | null> {
const projectNumber = this.projectNumber || this.workforcePoolUserProject;
if (this.projectId) {
// Return previously determined project ID.
return this.projectId;
} else if (this.projectNumber) {
} else if (projectNumber) {
// Preferable not to use request() to avoid retrial policies.
const headers = await this.getRequestHeaders();
const response = await this.transporter.request<ProjectInfo>({
headers,
url: `${CLOUD_RESOURCE_MANAGER}${this.projectNumber}`,
url: `${CLOUD_RESOURCE_MANAGER}${projectNumber}`,
responseType: 'json',
});
this.projectId = response.data.projectId;
Expand Down Expand Up @@ -401,8 +423,16 @@ export abstract class BaseExternalAccountClient extends AuthClient {
};

// Exchange the external credentials for a GCP access token.
// Client auth is prioritized over passing the workforcePoolUserProject
// parameter for STS token exchange.
const additionalOptions =
!this.clientAuth && this.workforcePoolUserProject
? {userProject: this.workforcePoolUserProject}
: undefined;
const stsResponse = await this.stsCredential.exchangeToken(
stsCredentialsOptions
stsCredentialsOptions,
undefined,
additionalOptions
);

if (this.serviceAccountImpersonationUrl) {
Expand Down
3 changes: 2 additions & 1 deletion src/auth/identitypoolclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ export class IdentityPoolClient extends BaseExternalAccountClient {
* Instantiate an IdentityPoolClient instance using the provided JSON
* object loaded from an external account credentials file.
* An error is thrown if the credential is not a valid file-sourced or
* url-sourced credential.
* url-sourced credential or a workforce pool user project is provided
* with a non workforce audience.
* @param options The external account options object typically loaded
* from the external account JSON credential file.
* @param additionalOptions Optional additional behavior customization
Expand Down
Loading