Skip to content

Commit

Permalink
feat: Open More Endpoints for Customization (#1721)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
danielbankhead authored Jan 26, 2024
1 parent 7e9876e commit effbf87
Show file tree
Hide file tree
Showing 11 changed files with 222 additions and 206 deletions.
48 changes: 27 additions & 21 deletions src/auth/baseexternalclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,13 @@ export const EXPIRATION_TIME_OFFSET = 5 * 60 * 1000;
* 3. external_Account => non-GCP service (eg. AWS, Azure, K8s)
*/
export const EXTERNAL_ACCOUNT_TYPE = 'external_account';
/** Cloud resource manager URL used to retrieve project information. */
/**
* Cloud resource manager URL used to retrieve project information.
*
* @deprecated use {@link BaseExternalAccountClient.cloudResourceManagerURL} instead
**/
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/.+';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const pkg = require('../../../package.json');
Expand Down Expand Up @@ -88,6 +89,12 @@ export interface BaseExternalAccountClientOptions
client_id?: string;
client_secret?: string;
workforce_pool_user_project?: string;
scopes?: string[];
/**
* @example
* https://cloudresourcemanager.googleapis.com/v1/projects/
**/
cloud_resource_manager_url?: string | URL;
}

/**
Expand Down Expand Up @@ -150,6 +157,13 @@ export abstract class BaseExternalAccountClient extends AuthClient {
public projectNumber: string | null;
private readonly configLifetimeRequested: boolean;
protected credentialSourceType?: string;
/**
* @example
* ```ts
* new URL('https://cloudresourcemanager.googleapis.com/v1/projects/');
* ```
*/
protected cloudResourceManagerURL: URL | string;
/**
* Instantiate a BaseExternalAccountClient instance using the provided JSON
* object loaded from an external account credentials file.
Expand Down Expand Up @@ -195,6 +209,11 @@ export abstract class BaseExternalAccountClient extends AuthClient {
serviceAccountImpersonation
).get('token_lifetime_seconds');

this.cloudResourceManagerURL = new URL(
opts.get('cloud_resource_manager_url') ||
`https://cloudresourcemanager.${this.universeDomain}/v1/projects/`
);

if (clientId) {
this.clientAuth = {
confidentialClientType: 'basic',
Expand All @@ -204,22 +223,11 @@ export abstract class BaseExternalAccountClient extends AuthClient {
}

this.stsCredential = new sts.StsCredentials(tokenUrl, this.clientAuth);
// Default OAuth scope. This could be overridden via public property.
this.scopes = [DEFAULT_OAUTH_SCOPE];
this.scopes = opts.get('scopes') || [DEFAULT_OAUTH_SCOPE];
this.cachedAccessToken = null;
this.audience = opts.get('audience');
this.subjectTokenType = subjectTokenType;
this.workforcePoolUserProject = workforcePoolUserProject;
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.'
);
}
this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl;
this.serviceAccountImpersonationLifetime =
serviceAccountImpersonationLifetime;
Expand Down Expand Up @@ -360,7 +368,7 @@ export abstract class BaseExternalAccountClient extends AuthClient {
const headers = await this.getRequestHeaders();
const response = await this.transporter.request<ProjectInfo>({
headers,
url: `${CLOUD_RESOURCE_MANAGER}${projectNumber}`,
url: `${this.cloudResourceManagerURL.toString()}${projectNumber}`,
responseType: 'json',
});
this.projectId = response.data.projectId;
Expand Down Expand Up @@ -576,11 +584,9 @@ export abstract class BaseExternalAccountClient extends AuthClient {
// be normalized.
if (typeof this.scopes === 'string') {
return [this.scopes];
} else if (typeof this.scopes === 'undefined') {
return [DEFAULT_OAUTH_SCOPE];
} else {
return this.scopes;
}

return this.scopes || [DEFAULT_OAUTH_SCOPE];
}

private getMetricsHeaderValue(): string {
Expand Down
20 changes: 17 additions & 3 deletions src/auth/downscopedclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ const STS_REQUEST_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
* The requested token exchange subject_token_type: rfc8693#section-2.1
*/
const STS_SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
/** The STS access token exchange end point. */
const STS_ACCESS_TOKEN_URL = 'https://sts.googleapis.com/v1/token';

/**
* The maximum number of access boundary rules a Credential Access Boundary
Expand Down Expand Up @@ -75,6 +73,13 @@ export interface CredentialAccessBoundary {
accessBoundary: {
accessBoundaryRules: AccessBoundaryRule[];
};
/**
* An optional STS access token exchange endpoint.
*
* @example
* 'https://sts.googleapis.com/v1/token'
*/
tokenURL?: string | URL;
}

/** Defines an upper bound of permissions on a particular resource. */
Expand Down Expand Up @@ -135,6 +140,12 @@ export class DownscopedClient extends AuthClient {
quotaProjectId?: string
) {
super({...additionalOptions, quotaProjectId});

// extract and remove `tokenURL` as it is not officially a part of the credentialAccessBoundary
this.credentialAccessBoundary = {...credentialAccessBoundary};
const tokenURL = this.credentialAccessBoundary.tokenURL;
delete this.credentialAccessBoundary.tokenURL;

// Check 1-10 Access Boundary Rules are defined within Credential Access
// Boundary.
if (
Expand Down Expand Up @@ -162,7 +173,10 @@ export class DownscopedClient extends AuthClient {
}
}

this.stsCredential = new sts.StsCredentials(STS_ACCESS_TOKEN_URL);
this.stsCredential = new sts.StsCredentials(
tokenURL || `https://sts.${this.universeDomain}/v1/token`
);

this.cachedDownscopedAccessToken = null;
}

Expand Down
25 changes: 18 additions & 7 deletions src/auth/googleauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1073,9 +1073,20 @@ export class GoogleAuth<T extends AuthClient = JSONClient> {
* Sign the given data with the current private key, or go out
* to the IAM API to sign it.
* @param data The data to be signed.
* @param endpoint A custom endpoint to use.
*
* @example
* ```
* sign('data', 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/');
* ```
*/
async sign(data: string): Promise<string> {
async sign(data: string, endpoint?: string): Promise<string> {
const client = await this.getClient();
const universe = await this.getUniverseDomain();

endpoint =
endpoint ||
`https://iamcredentials.${universe}/v1/projects/-/serviceAccounts/`;

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

return this.signBlob(crypto, creds.client_email, data);
return this.signBlob(crypto, creds.client_email, data, endpoint);
}

private async signBlob(
crypto: Crypto,
emailOrUniqueId: string,
data: string
data: string,
endpoint: string
): Promise<string> {
const url =
'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/' +
`${emailOrUniqueId}:signBlob`;
const url = new URL(endpoint + `${emailOrUniqueId}:signBlob`);
const res = await this.request<SignBlobResponse>({
method: 'POST',
url,
url: url.href,
data: {
payload: crypto.encodeBase64StringUtf8(data),
},
});

return res.data.signedBlob;
}
}
Expand Down
Loading

0 comments on commit effbf87

Please sign in to comment.