Skip to content

Commit effbf87

Browse files
authored
feat: Open More Endpoints for Customization (#1721)
* feat: Allow custom STS Access Token URL for Downscoped Clients * feat: Open More Endpoints for Customization * docs: Update * docs: Update * docs: Update * chore: remove unrelated * refactor: base endpoints on universe domain
1 parent 7e9876e commit effbf87

11 files changed

+222
-206
lines changed

src/auth/baseexternalclient.ts

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,13 @@ export const EXPIRATION_TIME_OFFSET = 5 * 60 * 1000;
5353
* 3. external_Account => non-GCP service (eg. AWS, Azure, K8s)
5454
*/
5555
export const EXTERNAL_ACCOUNT_TYPE = 'external_account';
56-
/** Cloud resource manager URL used to retrieve project information. */
56+
/**
57+
* Cloud resource manager URL used to retrieve project information.
58+
*
59+
* @deprecated use {@link BaseExternalAccountClient.cloudResourceManagerURL} instead
60+
**/
5761
export const CLOUD_RESOURCE_MANAGER =
5862
'https://cloudresourcemanager.googleapis.com/v1/projects/';
59-
/** The workforce audience pattern. */
60-
const WORKFORCE_AUDIENCE_PATTERN =
61-
'//iam\\.googleapis\\.com/locations/[^/]+/workforcePools/[^/]+/providers/.+';
6263

6364
// eslint-disable-next-line @typescript-eslint/no-var-requires
6465
const pkg = require('../../../package.json');
@@ -88,6 +89,12 @@ export interface BaseExternalAccountClientOptions
8889
client_id?: string;
8990
client_secret?: string;
9091
workforce_pool_user_project?: string;
92+
scopes?: string[];
93+
/**
94+
* @example
95+
* https://cloudresourcemanager.googleapis.com/v1/projects/
96+
**/
97+
cloud_resource_manager_url?: string | URL;
9198
}
9299

93100
/**
@@ -150,6 +157,13 @@ export abstract class BaseExternalAccountClient extends AuthClient {
150157
public projectNumber: string | null;
151158
private readonly configLifetimeRequested: boolean;
152159
protected credentialSourceType?: string;
160+
/**
161+
* @example
162+
* ```ts
163+
* new URL('https://cloudresourcemanager.googleapis.com/v1/projects/');
164+
* ```
165+
*/
166+
protected cloudResourceManagerURL: URL | string;
153167
/**
154168
* Instantiate a BaseExternalAccountClient instance using the provided JSON
155169
* object loaded from an external account credentials file.
@@ -195,6 +209,11 @@ export abstract class BaseExternalAccountClient extends AuthClient {
195209
serviceAccountImpersonation
196210
).get('token_lifetime_seconds');
197211

212+
this.cloudResourceManagerURL = new URL(
213+
opts.get('cloud_resource_manager_url') ||
214+
`https://cloudresourcemanager.${this.universeDomain}/v1/projects/`
215+
);
216+
198217
if (clientId) {
199218
this.clientAuth = {
200219
confidentialClientType: 'basic',
@@ -204,22 +223,11 @@ export abstract class BaseExternalAccountClient extends AuthClient {
204223
}
205224

206225
this.stsCredential = new sts.StsCredentials(tokenUrl, this.clientAuth);
207-
// Default OAuth scope. This could be overridden via public property.
208-
this.scopes = [DEFAULT_OAUTH_SCOPE];
226+
this.scopes = opts.get('scopes') || [DEFAULT_OAUTH_SCOPE];
209227
this.cachedAccessToken = null;
210228
this.audience = opts.get('audience');
211229
this.subjectTokenType = subjectTokenType;
212230
this.workforcePoolUserProject = workforcePoolUserProject;
213-
const workforceAudiencePattern = new RegExp(WORKFORCE_AUDIENCE_PATTERN);
214-
if (
215-
this.workforcePoolUserProject &&
216-
!this.audience.match(workforceAudiencePattern)
217-
) {
218-
throw new Error(
219-
'workforcePoolUserProject should not be set for non-workforce pool ' +
220-
'credentials.'
221-
);
222-
}
223231
this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl;
224232
this.serviceAccountImpersonationLifetime =
225233
serviceAccountImpersonationLifetime;
@@ -360,7 +368,7 @@ export abstract class BaseExternalAccountClient extends AuthClient {
360368
const headers = await this.getRequestHeaders();
361369
const response = await this.transporter.request<ProjectInfo>({
362370
headers,
363-
url: `${CLOUD_RESOURCE_MANAGER}${projectNumber}`,
371+
url: `${this.cloudResourceManagerURL.toString()}${projectNumber}`,
364372
responseType: 'json',
365373
});
366374
this.projectId = response.data.projectId;
@@ -576,11 +584,9 @@ export abstract class BaseExternalAccountClient extends AuthClient {
576584
// be normalized.
577585
if (typeof this.scopes === 'string') {
578586
return [this.scopes];
579-
} else if (typeof this.scopes === 'undefined') {
580-
return [DEFAULT_OAUTH_SCOPE];
581-
} else {
582-
return this.scopes;
583587
}
588+
589+
return this.scopes || [DEFAULT_OAUTH_SCOPE];
584590
}
585591

586592
private getMetricsHeaderValue(): string {

src/auth/downscopedclient.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@ const STS_REQUEST_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
3939
* The requested token exchange subject_token_type: rfc8693#section-2.1
4040
*/
4141
const STS_SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
42-
/** The STS access token exchange end point. */
43-
const STS_ACCESS_TOKEN_URL = 'https://sts.googleapis.com/v1/token';
4442

4543
/**
4644
* The maximum number of access boundary rules a Credential Access Boundary
@@ -75,6 +73,13 @@ export interface CredentialAccessBoundary {
7573
accessBoundary: {
7674
accessBoundaryRules: AccessBoundaryRule[];
7775
};
76+
/**
77+
* An optional STS access token exchange endpoint.
78+
*
79+
* @example
80+
* 'https://sts.googleapis.com/v1/token'
81+
*/
82+
tokenURL?: string | URL;
7883
}
7984

8085
/** Defines an upper bound of permissions on a particular resource. */
@@ -135,6 +140,12 @@ export class DownscopedClient extends AuthClient {
135140
quotaProjectId?: string
136141
) {
137142
super({...additionalOptions, quotaProjectId});
143+
144+
// extract and remove `tokenURL` as it is not officially a part of the credentialAccessBoundary
145+
this.credentialAccessBoundary = {...credentialAccessBoundary};
146+
const tokenURL = this.credentialAccessBoundary.tokenURL;
147+
delete this.credentialAccessBoundary.tokenURL;
148+
138149
// Check 1-10 Access Boundary Rules are defined within Credential Access
139150
// Boundary.
140151
if (
@@ -162,7 +173,10 @@ export class DownscopedClient extends AuthClient {
162173
}
163174
}
164175

165-
this.stsCredential = new sts.StsCredentials(STS_ACCESS_TOKEN_URL);
176+
this.stsCredential = new sts.StsCredentials(
177+
tokenURL || `https://sts.${this.universeDomain}/v1/token`
178+
);
179+
166180
this.cachedDownscopedAccessToken = null;
167181
}
168182

src/auth/googleauth.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,9 +1073,20 @@ export class GoogleAuth<T extends AuthClient = JSONClient> {
10731073
* Sign the given data with the current private key, or go out
10741074
* to the IAM API to sign it.
10751075
* @param data The data to be signed.
1076+
* @param endpoint A custom endpoint to use.
1077+
*
1078+
* @example
1079+
* ```
1080+
* sign('data', 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/');
1081+
* ```
10761082
*/
1077-
async sign(data: string): Promise<string> {
1083+
async sign(data: string, endpoint?: string): Promise<string> {
10781084
const client = await this.getClient();
1085+
const universe = await this.getUniverseDomain();
1086+
1087+
endpoint =
1088+
endpoint ||
1089+
`https://iamcredentials.${universe}/v1/projects/-/serviceAccounts/`;
10791090

10801091
if (client instanceof Impersonated) {
10811092
const signed = await client.sign(data);
@@ -1093,24 +1104,24 @@ export class GoogleAuth<T extends AuthClient = JSONClient> {
10931104
throw new Error('Cannot sign data without `client_email`.');
10941105
}
10951106

1096-
return this.signBlob(crypto, creds.client_email, data);
1107+
return this.signBlob(crypto, creds.client_email, data, endpoint);
10971108
}
10981109

10991110
private async signBlob(
11001111
crypto: Crypto,
11011112
emailOrUniqueId: string,
1102-
data: string
1113+
data: string,
1114+
endpoint: string
11031115
): Promise<string> {
1104-
const url =
1105-
'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/' +
1106-
`${emailOrUniqueId}:signBlob`;
1116+
const url = new URL(endpoint + `${emailOrUniqueId}:signBlob`);
11071117
const res = await this.request<SignBlobResponse>({
11081118
method: 'POST',
1109-
url,
1119+
url: url.href,
11101120
data: {
11111121
payload: crypto.encodeBase64StringUtf8(data),
11121122
},
11131123
});
1124+
11141125
return res.data.signedBlob;
11151126
}
11161127
}

0 commit comments

Comments
 (0)