Skip to content

Commit

Permalink
feat!: integrates external_accounts with GoogleAuth and ADC (#1052)
Browse files Browse the repository at this point in the history
  • Loading branch information
bojeil-google authored Sep 9, 2020
1 parent d759b09 commit 291652a
Show file tree
Hide file tree
Showing 9 changed files with 767 additions and 51 deletions.
90 changes: 88 additions & 2 deletions src/auth/authclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,112 @@
// limitations under the License.

import {EventEmitter} from 'events';
import {GaxiosOptions, GaxiosPromise} from 'gaxios';
import {GaxiosOptions, GaxiosPromise, GaxiosResponse} from 'gaxios';

import {DefaultTransporter} from '../transporters';
import {Credentials} from './credentials';
import {Headers} from './oauth2client';

/**
* Defines the root interface for all clients that generate credentials
* for calling Google APIs. All clients should implement this interface.
*/
export interface CredentialsClient {
/**
* The project ID corresponding to the current credentials if available.
*/
projectId?: string | null;

/**
* The expiration threshold in milliseconds before forcing token refresh.
*/
eagerRefreshThresholdMillis: number;

/**
* Whether to force refresh on failure when making an authorization request.
*/
forceRefreshOnFailure: boolean;

/**
* @return A promise that resolves with the current GCP access token
* response. If the current credential is expired, a new one is retrieved.
*/
getAccessToken(): Promise<{
token?: string | null;
res?: GaxiosResponse | null;
}>;

/**
* The main authentication interface. It takes an optional url which when
* present is the endpoint> being accessed, and returns a Promise which
* resolves with authorization header fields.
*
* The result has the form:
* { Authorization: 'Bearer <access_token_value>' }
* @param url The URI being authorized.
*/
getRequestHeaders(url?: string): Promise<Headers>;

/**
* Provides an alternative Gaxios request implementation with auth credentials
*/
request<T>(opts: GaxiosOptions): GaxiosPromise<T>;

/**
* Sets the auth credentials.
*/
setCredentials(credentials: Credentials): void;

/**
* Subscribes a listener to the tokens event triggered when a token is
* generated.
*
* @param event The tokens event to subscribe to.
* @param listener The listener that triggers on event trigger.
* @return The current client instance.
*/
on(event: 'tokens', listener: (tokens: Credentials) => void): this;
}

export declare interface AuthClient {
on(event: 'tokens', listener: (tokens: Credentials) => void): this;
}

export abstract class AuthClient extends EventEmitter {
export abstract class AuthClient
extends EventEmitter
implements CredentialsClient {
protected quotaProjectId?: string;
transporter = new DefaultTransporter();
credentials: Credentials = {};
projectId?: string | null;
eagerRefreshThresholdMillis = 5 * 60 * 1000;
forceRefreshOnFailure = false;

/**
* Provides an alternative Gaxios request implementation with auth credentials
*/
abstract request<T>(opts: GaxiosOptions): GaxiosPromise<T>;

/**
* The main authentication interface. It takes an optional url which when
* present is the endpoint> being accessed, and returns a Promise which
* resolves with authorization header fields.
*
* The result has the form:
* { Authorization: 'Bearer <access_token_value>' }
* @param url The URI being authorized.
*/
abstract getRequestHeaders(url?: string): Promise<Headers>;

/**
* @return A promise that resolves with the current GCP access token
* response. If the current credential is expired, a new one is retrieved.
*/
abstract getAccessToken(): Promise<{
token?: string | null;
res?: GaxiosResponse | null;
}>;

/**
* Sets the auth credentials.
*/
Expand Down
22 changes: 17 additions & 5 deletions src/auth/baseexternalclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ const DEFAULT_OAUTH_SCOPE = 'https://www.googleapis.com/auth/cloud-platform';
* Offset to take into account network delays and server clock skews.
*/
export const EXPIRATION_TIME_OFFSET = 5 * 60 * 1000;
/** The credentials JSON file type for external account clients. */
/**
* The credentials JSON file type for external account clients.
* There are 3 types of JSON configs:
* 1. authorized_user => Google end user credential
* 2. service_account => Google service account credential
* 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. */
export const CLOUD_RESOURCE_MANAGER =
Expand Down Expand Up @@ -112,14 +118,14 @@ export abstract class BaseExternalAccountClient extends AuthClient {
*/
public scopes?: string | string[];
private cachedAccessToken: CredentialsWithResponse | null;
private eagerRefreshThresholdMillis: number;
private forceRefreshOnFailure: boolean;
protected readonly audience: string;
private readonly subjectTokenType: string;
private readonly serviceAccountImpersonationUrl?: string;
private readonly stsCredential: sts.StsCredentials;
public projectId: string | null;
public projectNumber: string | null;
public readonly eagerRefreshThresholdMillis: number;
public readonly forceRefreshOnFailure: boolean;

/**
* Instantiate a BaseExternalAccountClient instance using the provided JSON
Expand Down Expand Up @@ -467,9 +473,15 @@ export abstract class BaseExternalAccountClient extends AuthClient {
/**
* @return The list of scopes for the requested GCP access token.
*/
private getScopesArray(): string[] | undefined {
private getScopesArray(): string[] {
// Since scopes can be provided as string or array, the type should
// be normalized.
return typeof this.scopes === 'string' ? [this.scopes] : this.scopes;
if (typeof this.scopes === 'string') {
return [this.scopes];
} else if (typeof this.scopes === 'undefined') {
return [DEFAULT_OAUTH_SCOPE];
} else {
return this.scopes;
}
}
}
10 changes: 8 additions & 2 deletions src/auth/externalclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@
import {RefreshOptions} from './oauth2client';
import {
BaseExternalAccountClient,
BaseExternalAccountClientOptions,
// This is the identifier in the JSON config for the type of credential.
// This string constant indicates that an external account client should be
// instantiated.
// There are 3 types of JSON configs:
// 1. authorized_user => Google end user credential
// 2. service_account => Google service account credential
// 3. external_Account => non-GCP service (eg. AWS, Azure, K8s)
EXTERNAL_ACCOUNT_TYPE,
} from './baseexternalclient';
import {
Expand Down Expand Up @@ -55,7 +61,7 @@ export class ExternalAccountClient {
* provided do not correspond to an external account credential.
*/
static fromJSON(
options: BaseExternalAccountClientOptions,
options: ExternalAccountClientOptions,
additionalOptions?: RefreshOptions
): BaseExternalAccountClient | null {
if (options && options.type === EXTERNAL_ACCOUNT_TYPE) {
Expand Down
Loading

0 comments on commit 291652a

Please sign in to comment.